Documentation

Integrate a custom-domain app

AdminUpdated Sep 14, 2026

Getting a custom-domain app to log in means getting four independent things right — CORS, the session cookie, the email claim, and your relying party's token verification. This is the end-to-end golden path, in order, with the exact setting name and how to confirm each is actually on. When you finish, the one-call login-readiness probe confirms the whole chain.

The shape of it

Your app and Atlas's Frontend API live on different subdomains of the same domain — for example the app on acme.com / app.acme.com, and the FAPI on auth.acme.com. The SDK, running on your app's origin, calls the FAPI cross-origin and reads a __session cookie back. That's why CORS and cookie scope both matter: they're what let those two subdomains talk.

Step 1 — point your FAPI subdomain at Atlas

In Customization → Domains, add a Frontend API domain (e.g. auth.acme.com), publish the CNAME Atlas gives you, and Verify. Atlas provisions TLS automatically. See Custom domains for the full walkthrough.

Step 2 — allow your app's origins

In Customization → Paths & origins, add every web origin your app is served from — https://acme.com, https://www.acme.com. The SDK calls the FAPI cross-origin, so an origin that isn't listed is refused by the browser (a CORS error) and sign-in can't complete from it.

Origins on the same registrable domain as a verified custom FAPI host are accepted automatically, so you don't have to list every subdomain — but list the exact ones your app uses so the readiness checks can confirm them.

> A missing CORS header often looks like "only email/password is broken" — because a Google/social button running in an Atlas-hosted iframe still works while the SDK's own fetch is blocked. If social works but email/password doesn't, suspect CORS first.

Step 3 — turn on the email claim (why: relying parties provision by email)

Your app provisions (creates/links) a local user on their first login, and to do that it needs the user's email. Atlas puts it in the session token when session.includeEmailClaim is on.

This now defaults ON for new instances. Confirm it — the __session JWT should carry an email claim. If it's off, either turn it on (PATCH /v1/instance with auth_config.session.includeEmailClaim: true) or read the email from the BAPI user object instead (Step 5).

> With no email anywhere, the user signs into Atlas but your app can't provision them, so your API rejects the session — login silently fails for every not-yet-linked user. This is the single most common custom-domain login blocker.

Step 4 — confirm the cookie is parent-scoped

Because your app and FAPI are different subdomains, the __session cookie must be scoped to your registrable domain (Domain=.acme.com) so both receive it. Atlas does this by default for custom domains (session.crossSubdomainCookies), and the domain wizard shows the effective cookie_domain and a first-party check per origin. See Cookies across your subdomains.

Step 5 — provision on your side

On first login, verify the token (Step 6 wiring) and provision the user. Get the email from either:

  • the `email` claim in the __session JWT (Step 3), or

  • the BAPI user object: GET /v1/users/:id returns Clerk-shaped email_addresses: [{ id, email_address, verification, verified, primary }] and primary_email_address_id.

Step 6 — verify with one call

Run the login-readiness probe against your app's origin:

curl "https://auth.acme.com/v1/diagnostics/login_readiness?origin=https://acme.com" \
  -H "Authorization: Bearer sk_live_…"

It returns each hop with its effective runtime value:

{
  "object": "login_readiness",
  "origin": "https://acme.com",
  "ready": true,
  "checks": [
    { "name": "cors",             "pass": true, "observed": { "exact_match": true, "same_site_custom_domain": true } },
    { "name": "session_cookie",   "pass": true, "observed": { "effective_cookie_domain": ".acme.com" } },
    { "name": "email_provisioning","pass": true, "observed": { "include_email_claim": true } },
    { "name": "token_signing",    "pass": true, "observed": { "issuer": "https://auth.acme.com", "jwks_keys": 1 } }
  ]
}

A red hop names the exact fix. ready: true across all four means a browser sign-in from that origin will complete end-to-end.

Step 7 — wire relying-party verification

Verify the __session JWT on your backend against Atlas's JWKS:

  • JWKS URL: https://auth.acme.com/.well-known/jwks.json

  • Issuer: https://auth.acme.com

  • Algorithms: ['RS256']

  • Clock tolerance: allow a few seconds — the access token TTL is short (jwtTtlSeconds, default 60s) and refreshes via tokenStrategy: refresh.

See What's in the session token for the full claim set.

Was this page helpful?