KavachOS is open source. Cloud launching soon.
kavachOS

03/MIGRATION

Migrating from Auth0 to kavachOSa step-by-step guide

Export your users, swap the SDK, preserve live sessions. Eighty percent less cost without a single logged-out user.

GD

Gagan Deep Singh

Founder, GLINCKER

Published

April 14, 20269 min read

Our Auth0 bill hit $812 last October. We had 38,000 MAU on the Business plan, a handful of machine-to-machine tokens for internal tooling, and absolutely zero use of the enterprise features we were paying to subsidize. The team decided to look elsewhere. What followed was three weeks of research and two days of actual migration work.

This post is the guide I wish I had found during that research phase. It covers exporting users from Auth0, mapping their schema to kavachOS, swapping the SDK in your app, keeping existing sessions valid through a co-sign period, running the cutover checklist, and what changed after the switch.

80%

Cost reduction

From $812/mo to $159/mo for 38k MAU

0

Users logged out

Session co-sign period covered all live tokens

2 days

Total migration time

Including testing and rollback plan


01

Exporting users from Auth0

Auth0 does not give you a direct CSV export from the dashboard. You use their Management API. The export job endpoint is rate-limited but you can pull 50,000 users in one request with the right fields. Here is the call:

bash
# Get a Management API token first
curl -X POST https://YOUR_DOMAIN.auth0.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "YOUR_M2M_CLIENT_ID",
    "client_secret": "YOUR_M2M_CLIENT_SECRET",
    "audience": "https://YOUR_DOMAIN.auth0.com/api/v2/",
    "grant_type": "client_credentials"
  }'
bash
# Start the export job
curl -X POST https://YOUR_DOMAIN.auth0.com/api/v2/jobs/users-exports \
  -H "Authorization: Bearer YOUR_MGMT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "YOUR_CONNECTION_ID",
    "format": "json",
    "fields": [
      {"name": "user_id"},
      {"name": "email"},
      {"name": "name"},
      {"name": "given_name"},
      {"name": "family_name"},
      {"name": "email_verified"},
      {"name": "created_at"},
      {"name": "last_login"},
      {"name": "logins_count"},
      {"name": "app_metadata"},
      {"name": "user_metadata"}
    ]
  }'

The job runs asynchronously. Poll the job status endpoint until it returns completed, then download the NDJSON file from the URL in the response. Each line is one user.


02

Mapping Auth0 schema to kavachOS

Auth0 and kavachOS use different field names. The mapping is mostly straightforward. Here is a Node.js script that transforms the export file:

typescriptscripts/transform-users.ts
import { createReadStream, createWriteStream } from 'fs';
import { createInterface } from 'readline';

interface Auth0User {
  user_id: string;
  email: string;
  name?: string;
  given_name?: string;
  family_name?: string;
  email_verified: boolean;
  created_at: string;
  last_login?: string;
  app_metadata?: Record<string, unknown>;
  user_metadata?: Record<string, unknown>;
}

interface KavachUser {
  externalId: string;
  email: string;
  displayName?: string;
  firstName?: string;
  lastName?: string;
  emailVerified: boolean;
  createdAt: string;
  lastSignedInAt?: string;
  // merge app_metadata and user_metadata into a single metadata object
  metadata?: Record<string, unknown>;
}

function transformUser(auth0: Auth0User): KavachUser {
  return {
    externalId: auth0.user_id,
    email: auth0.email,
    displayName: auth0.name,
    firstName: auth0.given_name,
    lastName: auth0.family_name,
    emailVerified: auth0.email_verified,
    createdAt: auth0.created_at,
    lastSignedInAt: auth0.last_login,
    metadata: {
      ...auth0.app_metadata,
      ...auth0.user_metadata,
    },
  };
}

const rl = createInterface({ input: createReadStream('auth0-export.ndjson') });
const out = createWriteStream('kavach-import.ndjson');

rl.on('line', (line) => {
  const auth0 = JSON.parse(line) as Auth0User;
  out.write(JSON.stringify(transformUser(auth0)) + '\n');
});

rl.on('close', () => {
  out.end();
  console.log('Transform complete.');
});

The externalId field is important. Store the original Auth0 user ID there. This lets you handle any edge case where your database still has Auth0 IDs in foreign keys without a full DB migration on day one.


03

Swapping the SDK in your app

Here is what the swap looks like side by side. The surface area changes more than the concepts: sessions, user objects, and token verification all exist in both libraries.

Auth0 SDK

// Session check

import { getSession } from '@auth0/nextjs-auth0';

const session = await getSession();

// Protect route

import { withApiAuthRequired } from '@auth0/nextjs-auth0';

export default withApiAuthRequired(handler);

kavachOS SDK

// Session check

import { kavach } from '@/lib/kavach';

const session = await kavach.sessions.get(token);

// Protect route

const{ valid } = await kavach.sessions.verify(token);

if (!valid) return 401;

Auth0 wraps everything in higher-order functions. kavachOS uses explicit calls. Both approaches are fine. The explicit style makes it easier to add permission checks alongside the session check, which is where you end up anyway once agents are involved. See the adapters guide for drop-in examples for Next.js, Express, Hono, and Fastify.


04

Keeping existing sessions valid

This is the part most migration guides skip. You have users with live Auth0 sessions. Their tokens are Auth0 JWTs signed with Auth0's JWKS. When you switch to kavachOS, those tokens will not verify against kavachOS's JWKS. The user gets a 401 and sees a login screen. Not great.

The fix is a co-sign period: a short window where your server accepts both Auth0 tokens and kavachOS tokens. You check the issuer claim in the JWT header and route accordingly:

typescriptlib/verify-token.ts
import { kavach } from '@/lib/kavach';
import { createRemoteJWKSet, jwtVerify } from 'jose';

const AUTH0_JWKS = createRemoteJWKSet(
  new URL('https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json')
);

const AUTH0_ISSUER = 'https://YOUR_DOMAIN.auth0.com/';

// Set a hard cutoff date 30 days from your migration start
const AUTH0_SUNSET = new Date('2026-05-14T00:00:00Z');

export async function verifyToken(token: string) {
  // Decode the header without verifying to check the issuer
  const [, payloadB64] = token.split('.');
  const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());

  const isAuth0 = payload.iss === AUTH0_ISSUER;
  const isPastSunset = new Date() > AUTH0_SUNSET;

  if (isAuth0 && isPastSunset) {
    // Co-sign period is over. Force re-login.
    return { valid: false, reason: 'auth0-token-expired' };
  }

  if (isAuth0) {
    // Verify against Auth0's JWKS during the co-sign period
    try {
      const { payload: verified } = await jwtVerify(token, AUTH0_JWKS, {
        issuer: AUTH0_ISSUER,
      });
      return {
        valid: true,
        userId: verified.sub as string,
        source: 'auth0' as const,
      };
    } catch {
      return { valid: false, reason: 'auth0-verify-failed' };
    }
  }

  // kavachOS token
  const result = await kavach.sessions.verify(token);
  return {
    valid: result.valid,
    userId: result.userId,
    source: 'kavachos' as const,
  };
}

Set the sunset date 30 days out. That is enough time for almost all users to get a new kavachOS session through normal login activity. When a user with an Auth0 token makes a request during the co-sign period, you can silently issue them a kavachOS session in the same response. See the Next.js adapter guide for how to mint and rotate tokens server-side.


05

Cutover checklist

Run through this list before flipping the switch. Most items take under five minutes each. The ones that take longer are marked.

  • Import transformed users via kavach.users.bulkImport(). Verify the count matches your Auth0 export.
  • Test passkey and magic link flows against imported user accounts in staging.
  • Deploy the co-sign middleware and verify it accepts an Auth0 JWT from a test account.
  • Update your OAuth provider callback URLs in GitHub, Google, and any other connected apps to point at kavachOS. (~20 min)
  • Replace the Auth0 SDK calls in your app with kavachOS equivalents. Run your test suite.
  • Set AUTH0_SUNSET to 30 days from today and deploy.
  • Monitor error rates on the token verification endpoint for 24 hours. Watch for unexpected 401 spikes.
  • Cancel your Auth0 subscription after the sunset date passes and error rates stay flat.

06

What stopped hurting after the switch

The bill went from $812 to $159 a month. That was the obvious one. But three other things changed that we did not expect.

First, the dashboard. Auth0's dashboard is built for enterprise identity teams. Our three-person team used maybe 15% of it. kavachOS shows us what we actually care about: active sessions, recent auth events, per-user audit trails. Nothing more.

Second, agent support. We had been faking agent identity with long-lived M2M tokens from Auth0. Each token had no expiry, no parent user, no permission scope beyond the broad roles we had defined years earlier. Migrating to kavachOS let us replace all of those with proper agent identities that expire, trace back to users, and carry narrow scopes. One of those M2M tokens had been quietly over-permissioned for 18 months.

Third, the OAuth provider setup. Adding a new OAuth provider in Auth0 required navigating three separate settings pages and waiting for a cache flush that sometimes took 10 minutes. In kavachOS it is two fields in the dashboard and it is live immediately.

The migration was not zero effort. Two days of focused work plus three weeks of preparation is real time. But the preparation was mostly reading, not coding. The coding part was two days because the SDK surface is small and the adapters handle the boilerplate. If you are on Auth0 and spending more than $200 a month, the numbers probably work for you too.

Topics

  • #Auth0 migration
  • #Auth0 alternative
  • #migrate from Auth0
  • #kavachOS migration
  • #auth library replacement

Keep going in the docs

Read next

Share this post

Get started

Try kavachOS Cloud free

Free up to 1,000 MAU. No credit card required.