Migrate from Auth0
This guide walks you through replacing Auth0 with TryMellon passkey-first authentication. The migration is additive: your existing users keep working while new users register passkeys. You run both in parallel until you’re ready to cut over.
What changes conceptually
| Auth0 | TryMellon |
|---|---|
| Social login + password as primary | Passkey (WebAuthn) as primary |
| Auth0 manages the session cookie | You receive a sessionToken, set your own session |
| JWT signed by Auth0 | JWT signed by TryMellon, validated via GET /v1/sessions/validate |
| MAU-based pricing | Per-tenant pricing — your users don’t drive your bill |
| SDK wraps redirects | SDK runs in-page — no redirect to an external domain |
| Auth0 user IDs | TryMellon user IDs + your externalUserId |
Step 1 — Install and configure the SDK
npm install @trymellon/js
// auth.ts
import { TryMellon } from '@trymellon/js';
const result = TryMellon.create({
appId: 'YOUR_APP_ID', // from TryMellon dashboard
publishableKey: 'cli_xxxx', // safe for the browser
});
if (!result.ok) throw result.error;
export const mellon = result.value;
Step 2 — Import your existing users
Use the migration import endpoint to register your Auth0 users in TryMellon. Each user gets a TryMellon record linked to your externalUserId (your existing Auth0 user ID). No passkey is required at import time.
curl -X POST https://api.trymellonauth.com/v1/migration/users/import \
-H "Authorization: Bearer $CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"users": [
{ "external_user_id": "auth0|abc123", "email": "user@example.com" },
{ "external_user_id": "auth0|def456", "email": "other@example.com" }
]
}'
Import up to 100 users per batch. For large datasets, use the NDJSON streaming mode — send Accept: application/x-ndjson and process the response line-by-line.
Step 3 — Send passkey enrollment invitations
After import, invite users to register a passkey via email. The SDK and the enrollment link handle the WebAuthn flow — users click the link in their email, their browser prompts them to create a passkey, and they’re done.
# Bulk enrollment links (up to 50 per call)
curl -X POST https://api.trymellonauth.com/v1/migration/users/enrollment-links \
-H "Authorization: Bearer $CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"external_user_ids": ["auth0|abc123", "auth0|def456"],
"redirect_url": "https://yourapp.com/dashboard"
}'
The response contains per-user enrollment URLs you can include in your email drip.
Step 4 — Add passkey login alongside Auth0
During the transition period, show passkey login to users who have enrolled and fall back to Auth0 for those who haven’t. Use getStatus() to detect passkey support, then check if the user has an active passkey in TryMellon.
// Login page (React example)
import { mellon } from './auth';
async function handleLogin(externalUserId: string) {
const status = await mellon.getStatus();
if (!status.isPasskeySupported) {
// Fall back to Auth0
return redirectToAuth0();
}
const result = await mellon.signIn({ externalUserId });
if (!result.ok) {
if (result.error.code === 'PASSKEY_NOT_FOUND') {
// User hasn't enrolled yet — fall back to Auth0
return redirectToAuth0();
}
throw result.error;
}
const { sessionToken } = result.value;
await setSessionOnServer(sessionToken);
window.location.href = '/dashboard';
}
Step 5 — Validate sessions on the backend
Replace Auth0’s jwt.verify() + JWKS with a single HTTP call:
// Node.js — protected route middleware
async function requireAuth(req, res, next) {
const token = req.headers['authorization']?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'No token' });
const resp = await fetch('https://api.trymellonauth.com/v1/sessions/validate', {
headers: { Authorization: `Bearer ${token}` },
});
if (!resp.ok) return res.status(401).json({ error: 'Invalid session' });
const body = await resp.json();
// body = { ok: true, data: { valid, user_id, external_user_id, tenant_id, app_id } }
if (!body.data.valid) return res.status(401).json({ error: 'Session expired' });
req.user = {
userId: body.data.user_id,
externalUserId: body.data.external_user_id, // your Auth0 user ID
};
next();
}
The external_user_id is your Auth0 user ID — use it to look up the user in your database without any schema changes.
Session tokens are JWTs with a 24-hour TTL. They are not consumed on validation — you can validate the same token multiple times within its TTL.
Step 6 — Enable email OTP fallback
For users on devices that don’t support WebAuthn (older Android, some enterprise desktops), enable the email OTP fallback so they’re never locked out:
// If passkey fails with NOT_SUPPORTED, fall back to OTP
const result = await mellon.signIn({ externalUserId });
if (!result.ok && result.error.code === 'NOT_SUPPORTED') {
await mellon.otp.send({ userId: externalUserId, email: userEmail });
// Prompt user to enter the code from their email
const verifyResult = await mellon.otp.verify({ userId: externalUserId, code });
if (verifyResult.ok) {
const { sessionToken } = verifyResult.value;
await setSessionOnServer(sessionToken);
}
}
Step 7 — Cut over and disable Auth0
Once >90% of active users have enrolled a passkey:
- Remove the Auth0 SDK and social login buttons.
- Remove the Auth0 JWKS validation from your backend — replace with TryMellon session validation (Step 5).
- Keep the email OTP fallback for the remaining users without passkeys.
- Set a sunset date for Auth0 passwords — send a final enrollment email to non-enrolled users.
Mapping Auth0 concepts to TryMellon
| Auth0 | TryMellon equivalent |
|---|---|
sub (user ID) | external_user_id (your ID) + user_id (TryMellon ID) |
access_token | sessionToken (JWT, validated via /v1/sessions/validate) |
refresh_token | Not needed — 24h TTL, user re-authenticates with passkey (instant) |
| Auth0 Rules / Actions | Webhooks (auth.registered, auth.authenticated) |
| Auth0 Universal Login | Your login page + TryMellon SDK in-page (no redirect) |
| Management API | TryMellon admin API (/v1/users, /v1/applications) |
| Organizations (Enterprise) | Tenants (native multi-tenant — each org is a tenant) |
| MFA enrollment | Passkey registration (passkey IS MFA — phishing-resistant by design) |
Common issues
PASSKEY_NOT_FOUND during authentication
The user hasn’t registered a passkey yet. Fall back to Auth0 or email OTP and send an enrollment invitation.
CHALLENGE_MISMATCH during registration
Usually a clock skew issue between server and client, or the challenge expired (15-minute TTL). Retry the registration flow.
User’s external_user_id not matching
Make sure you pass exactly the same string used at import time. Auth0 IDs include the prefix (auth0|, google-oauth2|) — import and authenticate with the full ID.
CORS errors calling the API
The SDK calls api.trymellonauth.com directly from the browser using your allowed origin. If you see CORS errors, verify your domain is listed in Allowed Origins for your application in the dashboard. Backend validation calls (e.g. GET /v1/sessions/validate) should always go from your server — never expose your clientSecret in the browser.