Skip to main content
Auth0 is a hosted identity platform with a deep enterprise feature set, Rules, Actions, Hooks, Organizations, and Machine-to-Machine (M2M) clients. theAuth is open source, self-hosted, and models AI agents as a first-class entity next to human users. The trade is real: you give up Auth0’s hosted Universal Login and rules marketplace, you take back full control of your token issuer, your cookie domain, your audit trail, and your bill. When the switch makes sense:
  • Your M2M client count (or Auth0 MAU) has outgrown what the tier price pays for.
  • You are building AI agents and Auth0’s per-connection M2M model does not give you delegation chains, per-agent rate caps, trust scoring, or cost attribution.
  • You want an MCP OAuth 2.1 server wired into the same auth instance rather than a second system.
  • You want GDPR data export tooling and self-hosted data residency on the same platform that issues your tokens. theAuth gives you the building blocks, it does not certify your deployment against any regulation.
When to wait:
  • You rely on Auth0’s Universal Login branding, device enrolment flow, or the Guardian MFA app. theAuth ships headless building blocks, not a hosted page.
  • Your Rules or Actions logic runs heavy third-party integrations (Okta, Duo, Risk signals). You can port that logic into your own route handlers, but it is not a copy-paste job.
  • You are on Enterprise with dedicated support SLAs. We do not have a 24/7 hotline.

Concepts map

Server setup

Role-based access is handled by the built-in policy engine (theauth.policy.evaluate), there is no separate rbac plugin to opt into, see Policy engine. The MCP OAuth 2.1 server is not a plugins: [...] entry and not just a config key: you build it with createMcpModule from @glinr/theauth/mcp and pass it to the adapter, see MCP. theauth.org exists only when you pass org: {...} to createTheAuth; the organization() plugin on its own only adds the HTTP endpoints.

Mounting the handler

Auth0 hides its auth server behind the hosted domain. theAuth exposes an explicit handler on your own domain. Pick your framework adapter.
Hono, Express, Fastify, SvelteKit, Nuxt, Astro, NestJS, SolidStart, and TanStack Start all have their own adapters with the same shape.

Client code

Wrap the root in <TheAuthProvider>, then the hooks work the same way they do in the Auth0 SDK.

Rules and Actions

Auth0 Rules ran in a VM on their side. Actions replaced them with a saner runtime. theAuth has no equivalent of the post-login trigger: there is no sign-in, sign-up, or session hook. The hooks option on createTheAuth is for agents only, and the real hooks are beforeAuthorize, afterAuthorize, beforeAgentCreate, afterAgentCreate, onAgentRevoke, and onViolation (see Lifecycle hooks). For the common “add a custom claim to the token” Action, use the customClaims option of the JWT session module:
customClaims receives { id, email, name } and returns extra claims for the access token. Anything else an Action did after login (provisioning, notifications, blocking a sign-in) goes in your own route handler around theauth.username.signIn. For agents, hooks.beforeAuthorize can veto a call by returning { allow: false, reason }:

Machine-to-Machine clients

Auth0’s M2M clients issue long-lived tokens against an API. They have a flat scope list, no delegation, no per-client rate cap.
Two differences to call out:
  1. Rotation is atomic. Old token dies the moment the new one is issued. No gap.
  2. Per-agent rate caps (maxCallsPerHour), approval gates (requireApproval), time windows (timeWindow), and IP allowlists (ipAllowlist) are part of the permission’s constraints, not a separate rule.

User data migration

Auth0 exports users via the Management API bulk export, which delivers JSON lines. theAuth users live in your own Postgres, SQLite, MySQL, or D1, in the theauth_users table that createTheAuth creates for you. Do not create your own users table.
There is no user import API (theauth.auth.importUser does not exist), and theAuth cannot verify Auth0 bcrypt hashes. Password verification in the username module is PBKDF2 only, in the format pbkdf2:<iterations>:<saltHex>:<hashHex> (SHA-256). A bcrypt hash stored in theauth_username_accounts.password_hash is simply rejected at sign-in. Plan for a password reset or a lazy rehash that you write yourself, both shown below.
Three things to know about the shape of the data:
  • Human users are rows in theauth_users (id, email, name, email_verified, metadata, force_password_reset, timestamps).
  • Password credentials are rows in theauth_username_accounts (user_id, username, password_hash). Sign-in looks up the username (lowercased by default) and does not apply the sign-up pattern check, so storing the lowercased email as the username lets users keep signing in with their email.
  • Social links are rows in theauth_oauth_accounts (provider, provider_account_id, access_token which is NOT NULL, so use an empty string for imported links).
A small Node script that inserts directly with SQL is enough. It keeps each user’s bcrypt hash in your own legacy_password_hashes table and writes an unusable placeholder into theAuth, with force_password_reset set so nobody can sign in until they either reset or are rehashed:
Choose one of two ways to let password users back in:
  1. Forced reset (simplest). Configure the passwordReset module on createTheAuth (it needs resetUrl and sendResetEmail), then send each user through POST /auth/forgot-password. The placeholder account row is what lets the reset flow find them, and a successful reset clears force_password_reset.
  2. Lazy rehash in your own sign-in handler. Verify the bcrypt hash yourself, write a PBKDF2 hash in theAuth’s format, then call theauth.username.signIn:
After a grace period, drop legacy_password_hashes and force a reset for anyone who never signed in. Social-only users have no hash: wire up the same provider in the oauth plugin before the cutover, and insert their theauth_oauth_accounts link (Auth0 ids look like google-oauth2|1234, the part after the pipe is the provider_account_id).

Cutover plan

  1. Stand up theAuth next to Auth0. Point at the same user database schema. Do not turn off Auth0 yet.
  2. Import users. Validate a sample with a test sign-in.
  3. Point a single route (for example /api/admin) at the theAuth handler. Keep everything else on Auth0.
  4. Watch the audit log for authorization failures. Fix mismatches.
  5. Expand the theAuth-guarded surface one route at a time. M2M clients cut over last, after you have confirmed rate caps and approval gates in staging.
  6. Final DNS or reverse-proxy switch: all auth traffic hits theAuth. Decommission the Auth0 tenant after the grace period.

Rollback

Keep the Auth0 tenant active for at least one full session-refresh cycle after the cutover (default 7 days). If you need to revert:
  1. Flip the reverse proxy or middleware back to Auth0.
  2. Users stay signed in using Auth0 cookies. New sessions come from Auth0 again.
  3. theAuth sessions remain valid locally until they expire. No user-facing disruption.

What Auth0 does that theAuth does not (yet)

  • Hosted Universal Login. theAuth ships React, Vue, and Svelte components you host yourself.
  • Guardian push-MFA mobile app. TOTP and passkeys are supported, push MFA is not.
  • Attack Protection suite (bot detection, breached-password detection on their edge). theAuth has HIBP integration and IP rate limiting, but no managed bot detection.
  • A hosted B2B customer-admin console. theAuth has an embeddable @glinr/theauth-dashboard React component for operators, not a hosted SaaS console.
If any of those are hard requirements, keep Auth0 on the books and read the competitor notes before you commit to a switch.

Runnable example

A minimal, in-memory version of the AFTER patterns lives at examples/migrate-from-auth0. It pulls @glinr/theauth via the monorepo workspace:* protocol so the script stays pinned to the same version the docs describe, and the smoke test runs in CI on every push.

Next steps

Agent identity

Model M2M clients as agents with delegation and trust scoring.

MCP OAuth 2.1

The authorization server Auth0 does not ship.

Lifecycle hooks

The real hook points (agent authorization and creation) that replace per-agent Actions.

Audit trail

Tenant logs, queryable and exportable, written for every authorize() call.
Last modified on October 7, 2026