Clerk vs Auth0 vs a gateway auth API for a small team
A five-person team's decision comes down to how much UI you want to own. An honest comparison, including the cases where buying the specialist wins.
For a five-person team the choice isn’t really about auth features — all three options will log a user in. It’s about which half of the problem you want to own. Clerk hands you the front end and the back end; Auth0 hands you a configurable identity platform; Infrai hands you plain HTTP endpoints for sessions, users, OAuth and consent on the same key as your email, queues and storage.
Pick on that axis and the decision takes an afternoon instead of a sprint.
What each one actually gives you
curl -sS "https://api.infrai.cc/v1/auth/oauth/providers" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"providers": [
{"id": "google", "ready": true},
{"id": "github", "ready": true},
{"id": "apple", "ready": true},
{"id": "facebook", "ready": true}
],
"consent_brand": "Infrai Account"
}
}
That’s the whole social-login setup on this surface: four providers, already live, no app registration, no client secrets in your config. The trade is in the last field — the consent screen says Infrai Account, not your company’s name.
| Decision factor | Clerk | Auth0 | Infrai auth |
|---|---|---|---|
| Drop-in UI components | yes, its main selling point | hosted pages, themeable | none — you build the forms |
| Provider app registration | optional | your own credentials | none; consent shows Infrai’s brand |
| Enterprise SSO / SAML | paid tiers | strong, mature | not part of this surface |
| Organisations and invites | first-class | first-class | your own model on user metadata |
| Offline token verification | yes | yes | yes, EdDSA via /v1/auth/token/jwks |
| Email, SMS, queue, storage on the same key | no | no | yes |
| Rate card shape | per MAU | per MAU, tiered | per MAU; routes free per call |
Read the rows, not the column totals. Three of them are decisive on their own.
Buy the specialist when the UI is the product
If your sign-up screen is the thing you’d most like to not build this month, buy Clerk. Its components handle the code input, the resend timer, the error states, the account page and the organisation switcher, and assembling those from HTTP endpoints is weeks you could spend on whatever your product actually does. That’s not a grudging admission — for a consumer app with a small team it’s usually the right call.
If enterprise buyers are already asking for SAML, SCIM and a self-serve SSO setup page, buy Auth0. It has a decade of that, and the shape of those requirements doesn’t reward improvisation.
Either way, you’d be better off paying than rebuilding.
Choose the gateway when auth is one line on a longer list
Here’s the case nobody makes in a vendor comparison, because no single-purpose vendor can make it.
A signup in a real product isn’t a login: it’s a login, then a welcome email, then a queued provisioning job, then an avatar upload, then an analytics event, then an error capture when one of those misbehaves, and eventually a finance question about which customer cost what. With specialists that’s six accounts, six keys, six invoices, six rate-limit regimes and six status pages to watch. Here it’s POST /v1/auth/email/verify, POST /v1/email/send, POST /v1/queue/publish, PUT /v1/storage/object/put/{bucket}/{key}, POST /v1/analytics/track and POST /v1/errors/capture — one credential, one bill, one usage view.
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const headers = { authorization: `Bearer ${KEY}`, "content-type": "application/json" };
// One credential, three services, no second onboarding.
export async function onboardNewUser({ email, code }) {
const verified = await fetch(`${API}/v1/auth/email/verify`, {
method: "POST",
headers,
body: JSON.stringify({ email, code }),
}).then((r) => r.json());
if (!verified.ok) throw new Error(verified.error?.code ?? "verify_failed");
await fetch(`${API}/v1/email/send`, {
method: "POST",
headers,
body: JSON.stringify({ to: email, subject: "Welcome aboard", html: "<p>You're in.</p>" }),
});
await fetch(`${API}/v1/queue/publish`, {
method: "POST",
headers,
body: JSON.stringify({ queue: "provisioning", payload: { user_id: verified.data.user_id } }),
});
return verified.data.user_id;
}
Three services, one import, no SDKs.
The migration question, honestly
Ask it before you sign anywhere: what does leaving look like?
On this surface the call sites are plain REST and the session token is a standard EdDSA JWT verified against a published JWKS, so replacing it means changing URLs and a verifier config — no SDK woven through your request path. Password hashes are argon2id, which is portable in principle. Auth0 and Clerk both support bulk user export too; the difference is how much framework-specific code you wrote around them.
What none of the three will do is merge two accounts a user already created by signing up twice. That work is always yours, and it’s cheaper to get identity resolution right on day one with POST /v1/auth/identity/resolve than to clean up later.
How to actually decide
Spend an hour, not a week. Sign up for each, and implement exactly one flow end to end: email login, a session verified on a protected route, and a logout that closes every device. The one that takes you an hour is your answer, and the one whose failure modes you can explain afterwards is the safer answer.
For cost, don’t compare headline MAU rates — compare definitions. Ask each vendor what counts as active, whether dormant stored users bill, whether service accounts count, and where the free tier cliffs. Auth routes here report billing_class: free in discovery with identity metered per MAU; read your own figure from GET /v1/account/usage and the live per-route rate from GET /v1/discovery (verified 2026-09-21). Platform rates drift downward over time, so a live read beats any table — including the one above.