Three-tier domains
t.ai, todayai.dev, today.ai — development / preview / production. How URL resolution branches on environment in packages/auth-client.
The app targets three sets of domains, mapped 1:1 to the three environments:
| Environment | Root domain | Auth origin | API origin |
|---|---|---|---|
development | t.ai | auth.t.ai | api.t.ai |
preview | todayai.dev | auth.todayai.dev | api.todayai.dev |
production | today.ai | auth.today.ai | api.today.ai |
The mapping lives in
packages/auth-client/src/urls.ts:
export const DOMAINS: Record<AppEnvironment, string> = {
development: 't.ai',
preview: 'todayai.dev',
production: 'today.ai',
}Every app that needs to know "what auth server / API origin should I talk to" goes through one of the resolvers in that file. Never hardcode the URL.
The four resolvers
resolveDomain(env?) // 't.ai' / 'todayai.dev' / 'today.ai'
resolveAuthBaseUrl(env?) // 'https://auth.<domain>'
resolveApiBaseUrl(env?) // 'https://api.<domain>'
resolveAppBaseUrl(env?) // 'https://<domain>' (the web app itself)Each resolver checks for an explicit env override before falling back to the computed default:
export function resolveAuthBaseUrl(env?: Env): string {
if (env) {
return (
env.OIDC_INTERNAL_AUTHORITY || // server-side, internal route
env.NEXT_PUBLIC_OIDC_AUTHORITY || // client-side, public URL
`https://auth.${resolveDomain(env)}`
)
}
return (
process.env.OIDC_INTERNAL_AUTHORITY ||
process.env.NEXT_PUBLIC_OIDC_AUTHORITY ||
`https://auth.${resolveDomain()}`
)
}The override chain exists so local dev can point at an in-cluster service
(http://localhost:4010) without falsifying the environment label, and so
preview deploys can target a different auth server (e.g. a staging
deployment) without redeploying the web app.
Why the dual code paths
Each function has the literal-process.env branch and the function-parameter
branch:
if (env) { /* test path */ }
return process.env.NEXT_PUBLIC_API_URL || ... // production pathThat looks redundant, but the production path must read
process.env.NEXT_PUBLIC_* as literal expressions for webpack's
DefinePlugin (and Turbopack's equivalent) to inline the value at build
time. Reading through a function-parameter shadow (env.NEXT_PUBLIC_API_URL)
defeats inlining; the client bundle ships with undefined instead of the
real URL. See Environment variables for the
inlining mechanism in detail.
When you need to override
The override env vars, in priority order:
| Variable | Effect |
|---|---|
OIDC_INTERNAL_AUTHORITY | Server-side auth origin (BFF proxy target) |
NEXT_PUBLIC_OIDC_AUTHORITY | Client-side auth origin (what the browser sees) |
NEXT_PUBLIC_API_URL | API origin (both server and client) |
NEXT_PUBLIC_APP_URL | Web app's own origin (used for callback URL construction) |
apps/web/.env.development (committed) sets the local-stack defaults:
NEXT_PUBLIC_OIDC_AUTHORITY=https://auth.t.ai
OIDC_INTERNAL_AUTHORITY=http://localhost:4010
NEXT_PUBLIC_API_URL=https://api.t.aiapps/web/.env.development.local (gitignored) overrides these for remote-dev
mode (pointing at *.todayai.dev). See
Workflow → Local development for the
mode-switching mechanics.
How resolveEnvironment decides
if (NODE_ENV === 'development') return 'development'
if ((VERCEL_ENV ?? NEXT_PUBLIC_VERCEL_ENV) === 'production') return 'production'
return 'preview'So NODE_ENV=development (local next dev) → t.ai. VERCEL_ENV=production
(Vercel production build) → today.ai. Everything else (Vercel previews,
GitHub Actions CI) → todayai.dev.
This means the preview environment is the canonical default in
ambiguous cases. Most automated runs (CI, agent E2E, smoke tests) land on
todayai.dev, which has stable infrastructure and isn't customer-visible.
Related
- Environment variables — how
NEXT_PUBLIC_*inlining works at build time - Environment detection — the runtime
getEnvironment()helper and theOnboardingRedirectGuardgating - Workflow → Local development — three dev modes and which domain each one uses
- Workflow → Deployment — how
mainvsdevpushes map to production vs preview Vercel targets