Today Platform Web — Dev Docs
Architecture

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:

EnvironmentRoot domainAuth originAPI origin
developmentt.aiauth.t.aiapi.t.ai
previewtodayai.devauth.todayai.devapi.todayai.dev
productiontoday.aiauth.today.aiapi.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 path

That 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:

VariableEffect
OIDC_INTERNAL_AUTHORITYServer-side auth origin (BFF proxy target)
NEXT_PUBLIC_OIDC_AUTHORITYClient-side auth origin (what the browser sees)
NEXT_PUBLIC_API_URLAPI origin (both server and client)
NEXT_PUBLIC_APP_URLWeb 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.ai

apps/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.

On this page