TryMellon
Navigation

Migrate from Auth0

Step-by-step guide to replace Auth0 with TryMellon passkey-first authentication.

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

Auth0TryMellon
Social login + password as primaryPasskey (WebAuthn) as primary
Auth0 manages the session cookieYou receive a sessionToken, set your own session
JWT signed by Auth0JWT signed by TryMellon, validated via GET /v1/sessions/validate
MAU-based pricingPer-tenant pricing — your users don’t drive your bill
SDK wraps redirectsSDK runs in-page — no redirect to an external domain
Auth0 user IDsTryMellon 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": "[email protected]" },
      { "external_user_id": "auth0|def456", "email": "[email protected]" }
    ]
  }'

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:

  1. Remove the Auth0 SDK and social login buttons.
  2. Remove the Auth0 JWKS validation from your backend — replace with TryMellon session validation (Step 5).
  3. Keep the email OTP fallback for the remaining users without passkeys.
  4. Set a sunset date for Auth0 passwords — send a final enrollment email to non-enrolled users.

Mapping Auth0 concepts to TryMellon

Auth0TryMellon equivalent
sub (user ID)external_user_id (your ID) + user_id (TryMellon ID)
access_tokensessionToken (JWT, validated via /v1/sessions/validate)
refresh_tokenNot needed — 24h TTL, user re-authenticates with passkey (instant)
Auth0 Rules / ActionsWebhooks (auth.registered, auth.authenticated)
Auth0 Universal LoginYour login page + TryMellon SDK in-page (no redirect)
Management APITryMellon admin API (/v1/users, /v1/applications)
Organizations (Enterprise)Tenants (native multi-tenant — each org is a tenant)
MFA enrollmentPasskey 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.