Guide · 20 minutes · Node.js
Account Binding & Recovery
Most products can't drop passwords overnight. Let users add Digi-ID to the account they already have, sign in with it, and keep a second wallet as a recovery key. Passwords can then be retired one user at a time.
This guide builds on Add Digi-ID login to a web app; read that first for the challenge, callback and polling basics.
1. Store addresses, not secrets
A user can link several wallets. Each Digi-ID address belongs to exactly one account.
CREATE TABLE user_digiids (
address VARCHAR(40) PRIMARY KEY, -- site-specific Digi-ID address
user_id BIGINT NOT NULL REFERENCES users(id),
label VARCHAR(60) NOT NULL DEFAULT 'My wallet',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP NULL
);
CREATE INDEX user_digiids_user ON user_digiids (user_id);2. Give every challenge a purpose
The same callback serves two flows. Record why the challenge was issued, and for linking, who asked for it:
app.post('/digiid/challenge', (req, res) => {
const purpose = req.body?.purpose === 'link' ? 'link' : 'login';
if (purpose === 'link' && !req.session.userId) return res.status(401).end();
const nonce = crypto.randomBytes(16).toString('hex');
pending.set(nonce, {
purpose,
sessionId: req.sessionID,
userId: purpose === 'link' ? req.session.userId : null,
expires: Date.now() + 90_000
});
res.json({ nonce, uri: new DigiID({ nonce, callback: CALLBACK }).uri });
});The callback handler stays exactly as in the web-app guide: verify the URI and signature, then record challenge.address. The difference is what happens when the browser polls.
3. Finish the flow in the status handler
app.get('/digiid/status', async (req, res) => {
const nonce = String(req.query.nonce);
const challenge = pending.get(nonce);
if (!challenge || challenge.sessionId !== req.sessionID) return res.status(404).json({ state: 'unknown' });
if (!challenge.address) return res.json({ state: challenge.expires < Date.now() ? 'expired' : 'pending' });
pending.delete(nonce);
const owner = await db.findUserIdByDigiId(challenge.address);
if (challenge.purpose === 'link') {
if (owner && owner !== challenge.userId) return res.json({ state: 'error', reason: 'already linked to another account' });
if (!owner) await db.linkDigiId(challenge.userId, challenge.address);
return res.json({ state: 'linked' });
}
if (!owner) return res.json({ state: 'error', reason: 'no account uses this wallet yet' });
await db.touchDigiId(challenge.address);
req.session.regenerate(() => {
req.session.userId = owner;
res.json({ state: 'verified' });
});
});4. Settings page: link, list, unlink
- Link: a “Add Digi-ID” button requests a challenge with
purpose: 'link'and shows the QR code. - List: show each linked wallet's label, the first and last characters of the address and when it was last used.
- Unlink: require a fresh sign-in (password or another linked wallet) before removing an address, and never let a user remove their last way in.
5. Migrate away from passwords
- Show “Sign in with Digi-ID” next to the password form.
- After a password sign-in, suggest linking a wallet.
- Once two wallets are linked, offer to remove the password entirely.
- Keep your existing email-based recovery as a last resort, but require it to link a new wallet rather than set a password.
6. Second devices and recovery
- Same wallet, new phone: restoring the wallet's recovery phrase on another device derives the same address for your site. Nothing changes on your side.
- A different wallet: pair it as a second device by signing in with the first one and running the link flow from step 2. Both addresses now open the same account.
- Lost device, phrase backed up: restore the wallet and sign in as before.
- Lost device and phrase: sign in with another linked wallet and unlink the lost one. Only fall back to email recovery when no linked wallet is left, and have it end in linking a new wallet.
- Notify on changes: email the user whenever a wallet is linked or unlinked, so an unexpected pairing is noticed quickly.
Pitfalls
- Addresses are specific to your callback URL. Changing the URL (including its path) gives every user a new address and breaks their link — pick one and keep it.
- Don't auto-create an account when an unknown address signs in during a link flow; that's how accounts get hijacked.
- A lost recovery phrase means a lost key. Encourage a second wallet, and treat email recovery as high-risk.
Next: Nonces & Replay Prevention.