Account Linking
Link existing user accounts to Muzl, enabling passwordless sign-in for users who already have accounts in your app.
What is Account Linking?
Account linking lets users with existing accounts (email/password) connect their Muzl identity. After linking, they can sign in with either:
- Their original method (email/password)
- Muzl (passkey/biometric)
Use case: You have an app with existing users. You want to add Muzl as a sign-in option without forcing users to create new accounts.
Quick Start (Using SDK)
Add Database Column
ALTER TABLE users ADD COLUMN muzl_service_id TEXT UNIQUE;
ALTER TABLE users ADD COLUMN muzl_linked_at TIMESTAMPTZ;Add "Link with Muzl" Button
In your settings page (where user is already logged in):
<button onclick="linkMuzlAccount()">Link with Muzl</button>
<script>
function linkMuzlAccount() {
Muzl.linkAccount();
}
</script>Handle Callback
In your callback page (same as sign-in callback):
Muzl.handleCallback()
.then(user => {
if (user.flowType === 'link') {
// Store Service ID for current logged-in user
return fetch('/api/users/link-muzl', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ serviceId: user.serviceId })
})
.then(res => {
if (!res.ok) throw new Error('Failed to link account');
return res.json();
});
}
})
.then(() => {
window.location.href = '/settings?linked=true';
})
.catch(error => {
console.error('Account linking failed:', error);
window.location.href = '/settings?error=link_failed';
});Store Service ID (Backend)
// POST /api/users/link-muzl
app.post('/api/users/link-muzl', async (req, res) => {
try {
const { serviceId } = req.body;
const userId = req.session.userId; // Current logged-in user
if (!userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!serviceId) {
return res.status(400).json({ error: 'Service ID required' });
}
// Check if already linked
const existing = await db.user.findUnique({
where: { id: userId }
});
if (existing?.muzl_service_id) {
return res.status(400).json({ error: 'Account already linked' });
}
await db.user.update({
where: { id: userId },
data: {
muzl_service_id: serviceId,
muzl_linked_at: new Date()
}
});
res.json({ success: true });
} catch (error) {
console.error('Link account error:', error);
res.status(500).json({ error: 'Failed to link account' });
}
});Manual Implementation (Server-Side)
If you prefer server-side token exchange:
Step 1: Initiate Linking Flow
app.get('/settings/link-muzl', async (req, res) => {
if (!req.user) return res.redirect('/login');
// Generate PKCE parameters
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto.createHash('sha256')
.update(codeVerifier).digest('base64url');
const state = crypto.randomBytes(32).toString('base64url');
// Store in session
req.session.muzlLink = { codeVerifier, state, userId: req.user.id };
// Build authorization URL with link_account=true
const authUrl = new URL('https://muzl.app/oauth/authorize');
authUrl.searchParams.set('client_id', process.env.MUZL_CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/oauth/muzl/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('link_account', 'true'); // ← This flag
res.redirect(authUrl.toString());
});Step 2: Handle Callback
app.get('/oauth/muzl/callback', async (req, res) => {
const { code, state, error } = req.query;
const stored = req.session.muzlLink;
if (error || !stored || stored.state !== state) {
return res.redirect('/settings?error=link_failed');
}
// Exchange code for tokens
const tokenRes = await fetch('https://muzl.app/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://yourapp.com/oauth/muzl/callback',
client_id: process.env.MUZL_CLIENT_ID,
client_secret: process.env.MUZL_CLIENT_SECRET,
code_verifier: stored.codeVerifier
})
});
const tokens = await tokenRes.json();
// Get Service ID
const userInfoRes = await fetch('https://muzl.app/oauth/userinfo', {
headers: { 'Authorization': `Bearer ${tokens.access_token}` }
});
const userInfo = await userInfoRes.json();
const serviceId = userInfo.sub; // Service ID
// Store Service ID
await db.user.update({
where: { id: stored.userId },
data: {
muzl_service_id: serviceId,
muzl_linked_at: new Date()
}
});
delete req.session.muzlLink;
res.redirect('/settings?linked=true');
});How It Works
- User clicks "Link with Muzl" while logged into your app
- Your app redirects to Muzl with
link_account=trueparameter - User authenticates with Muzl (passkey/biometric)
- Muzl returns authorization code to your callback
- Your app exchanges code for tokens (same as regular OAuth)
- Your app gets Service ID from
/oauth/userinfo - Your app stores Service ID linked to the user's existing account
That's it! The user can now sign in with Muzl and your app will recognize them as the same user.
After Linking: Handling Muzl Sign-In
When a user signs in with Muzl (after linking), check if the Service ID matches a linked account:
// After OAuth completes
const userInfo = await fetch('https://muzl.app/oauth/userinfo', {
headers: { 'Authorization': `Bearer ${accessToken}` }
}).then(r => r.json());
const serviceId = userInfo.sub;
// Check if linked account exists
const user = await db.user.findUnique({
where: { muzl_service_id: serviceId }
});
if (user) {
// Log them in as existing user
req.session.userId = user.id;
res.redirect('/dashboard');
} else {
// New user - create account or show signup
res.redirect('/signup');
}Key Differences from Regular Sign-In
| Regular Sign-In | Account Linking |
|---|---|
| User not logged in | User already logged in |
| Creates new account | Links to existing account |
No link_account parameter | link_account=true parameter |
flowType: 'signin' | flowType: 'link' |
Troubleshooting
"User not found when signing in with Muzl"
- Make sure you stored the Service ID correctly
- Check that
muzl_service_idcolumn exists and has the right value
"Linking flow doesn't work"
- Verify
link_account=trueis in the authorization URL - Check that user is logged in before initiating linking
"Service ID already exists"
- This means the account is already linked. You can skip the linking flow or show a message.
Muzl® is a registered trademark of Muzl LLC.
© 2025 Muzl LLC. All rights reserved.