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:
# 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"
}'# 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:
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.
// Session check
import { getSession } from '@auth0/nextjs-auth0';
const session = await getSession();
// Protect route
import { withApiAuthRequired } from '@auth0/nextjs-auth0';
export default withApiAuthRequired(handler);
// 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:
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
Start
Quickstart
Install kavachOS, wire up your first route, and issue your first token in under five minutes.
Start
Configuration
Environment variables, adapter options, and the config shape every kavachOS app needs.
Reference
Framework adapters
Drop-in adapters for Hono, Express, Next.js, Fastify, Nuxt, SvelteKit, Astro, and NestJS.
Reference
Next.js adapter
Wire kavachOS into the Next.js App Router with middleware, route handlers, and React hooks.
Read next
- Comparison
Best open source auth libraries for AI agents (2026)
I needed auth for 50 agents talking to MCP servers. Most libraries assume you are building a login page. Here is what actually worked.
12 min read - Engineering
Why AI agents need their own auth
User auth was never designed for software that makes API calls while you sleep. Here is what is different.
5 min read
Share this post
Get started
Try kavachOS Cloud free
Free up to 1,000 MAU. No credit card required.