Local development
Three dev modes — localhost-direct, local full-stack, and remote dev — plus proxy setup, env files, and troubleshooting.
Prerequisites
- Node.js 22+
- pnpm 9+
- wproxy / whistle (recommended)
- Proxyman (alternative)
Development Modes
The web app supports two modes depending on which backend you connect to:
| Mode | Backend | Domain | Proxy Rules | Use Case |
|---|---|---|---|---|
| Local | Full-stack local services | https://t.ai | Proxyman today-local-dev.js | Backend + frontend work |
| Remote Dev | Remote todayai.dev services | https://todayai.dev | Whistle today-remote-dev.rules (recommended), Proxyman today-remote-dev.js | Frontend-only work |
Quick Start (Remote Dev)
The fastest way to get started — no backend services needed:
pnpm install
# Copy the example and fill in credentials
cp apps/web/.env.development.local.example apps/web/.env.development.local
# Edit .env.development.local: uncomment the URL overrides and set credentials
pnpm devSet up wproxy with today-remote-dev.rules (see Proxy Setup), then browse https://todayai.dev or https://admin.todayai.dev.
Quick Start (Local Full-Stack)
Requires all backend services running locally (see today-cloud local development guide):
pnpm install
# Copy and fill credentials (leave URL overrides commented out)
cp apps/web/.env.development.local.example apps/web/.env.development.local
pnpm devSet up your local domain proxy (for example Proxyman with today-local-dev.js), then browse https://t.ai.
Localhost-direct dev mode (LOCALHOST_BFF)
pnpm dev serves the app directly at http://localhost:<port> — no whistle
or Proxyman proxy required. It picks the first free port in 4060-4069, so
multiple worktrees each get their own port and run side by side.
This works because of the LOCALHOST_BFF flag, set to true in the committed
apps/web/.env.development. With it on, the auth BFF strips the Domain
attribute from the better-auth session cookie it relays, so the browser stores
the cookie host-only for localhost instead of rejecting it for the
.todayai.dev domain mismatch. Without that strip a direct-localhost login
succeeds on the server but the browser drops the session cookie — which is the
original reason a domain proxy was needed at all.
- Just run
pnpm devand open thehttp://localhost:<port>URL it prints. Email-OTP login, the authenticated app, and chat/SSE all work directly. LOCALHOST_BFFis server-only (noNEXT_PUBLIC_prefix) and lives in.env.development, which Next loads only underNODE_ENV=development— a production build never sees it.- The whistle / Proxyman setup below is still available, and still needed for
flows that must round-trip a real
todayai.devOAuth callback (e.g. Google social login) — but it is no longer required for everyday email-OTP dev. - Caveat: browser cookies are scoped by host, not port, so two worktrees on
localhost:<different ports>share one cookie jar within a single browser profile — logging into one replaces the other's session there. Use a separate browser profile per worktree if you need both logged in at once.
See docs/plans/2026-05-22-localhost-bff-dev-mode.md
for the design rationale.
Environment Variables
File Structure
Next.js loads env files in this order during next dev (NODE_ENV=development):
| Priority | File | Committed | Purpose |
|---|---|---|---|
| 1 (highest) | .env.development.local | No | Credentials + optional URL overrides |
| 2 | .env.development | Yes | Default URLs for local backend |
| 3 (lowest) | .env | Yes | Shared defaults (if any) |
.env.development (committed)
Default URLs pointing to the local backend:
NEXT_PUBLIC_OIDC_AUTHORITY=https://auth.t.ai
OIDC_INTERNAL_AUTHORITY=http://localhost:4010
NEXT_PUBLIC_API_URL=https://api.t.ai.env.development.local (gitignored)
Credentials and optional URL overrides. Copy from .env.development.local.example:
# --- Credentials (always required) ---
NEXT_PUBLIC_OIDC_CLIENT_ID=replace-with-client-id
OIDC_CLIENT_SECRET=replace-with-client-secret
# --- URL overrides (uncomment to connect to dev backend) ---
# NEXT_PUBLIC_OIDC_AUTHORITY=https://auth.todayai.dev
# OIDC_INTERNAL_AUTHORITY=https://auth.todayai.dev
# NEXT_PUBLIC_API_URL=https://api.todayai.dev- Local mode: Leave URL overrides commented — defaults from
.env.developmentapply. - Remote dev mode: Uncomment the URL overrides to point at
todayai.dev.
Getting Credentials
From Vercel (remote dev):
cd apps/web
vercel link # Link to todayai/today-web
vercel env pull .env.development.local --environment previewThen remove extraneous variables and keep only the ones listed above.
From seed script (local):
cd ../today-cloud/services/auth-service && pnpm db:seed-clientsConvenience Scripts
pnpm dev # Use existing .env.development.local as-is
pnpm dev:local # Delete .env.development.local → use local backend defaults
pnpm dev:remote # Ensure .env.development.local exists (from example) → use remoteProxy Setup (Recommended: wproxy / whistle)
OAuth login requires domain-aligned cookies. Direct localhost access breaks social login (Google/GitHub) because cookie domains don't match. A domain proxy maps production-like domains to local dev servers.
wproxy (whistle) Remote Dev Mode (recommended)
todayai.dev → localhost:4060 (local web)
admin.todayai.dev → localhost:4061 (local admin)
auth.todayai.dev → remote (direct)
api.todayai.dev → remote (direct)Use:
dev-tools/whistle/today-remote-dev.rulesdev-tools/whistle/README.md
Quick steps:
- Install whistle (
npm i -g whistle) and start it (w2 start) - Import
dev-tools/whistle/today-remote-dev.rules - Set system/browser proxy to
127.0.0.1:8899 - Install and trust whistle CA (
w2 ca) - Browse
https://todayai.devorhttps://admin.todayai.dev
Proxyman Remote Dev Mode (alternative)
todayai.dev → localhost:4060 (Proxyman)
admin.todayai.dev → localhost:4061 (Proxyman)
auth.todayai.dev → remote (direct)
api.todayai.dev → remote (direct)Setup:
- In Proxyman, create a new Script
- Name: Today AI Remote Dev
- Matching Rule: Regex
- URL Pattern:
^https?://(admin\.)?todayai\.dev($|/.*)
- Paste the contents of
dev-tools/proxyman/today-remote-dev.js - Enable Run Script on Request
- Add
todayai.devandadmin.todayai.devto SSL Proxying List - Browse
https://todayai.devorhttps://admin.todayai.dev
Local Full-Stack Mode
All external-facing domains are proxied to local services.
t.ai → localhost:4060 (web)
auth.t.ai → localhost:4010 (auth)
api.t.ai → localhost:4020 (api gateway)Setup:
- In Proxyman, create a new Script
- Name: Today AI Local Dev
- Matching Rule: Regex
- URL Pattern:
^https?://([a-z]+\.)*t\.ai($|/.*)
- Paste the contents of
today-cloud/dev-tools/proxyman/today-local-dev.js - Enable Run Script on Request
- Add
t.aiand*.t.aito SSL Proxying List - Browse
https://t.ai
Only enable one Proxyman script at a time.
Architecture
Request Flow (Remote Dev)
Browser (https://todayai.dev)
|
+-- todayai.dev/* ---- domain proxy (wproxy/Proxyman) ----> localhost:4060 (Next.js)
| |
| +-- /api/auth/* (BFF proxy)
| | |
| | +--> auth.todayai.dev (remote)
| |
| +-- SSR pages (local render)
|
+-- auth.todayai.dev/* --- direct -----> remote auth service
| (Google OAuth callback lands here)
|
+-- api.todayai.dev/* --- direct -----> remote API gatewayWhy a domain proxy is required for OAuth
Social login (Google/GitHub) uses this flow:
- Frontend on
todayai.devinitiates login via BFF - BFF proxies to
auth.todayai.dev, which redirects to Google - Google redirects back to
auth.todayai.dev/api/auth/callback/google - Auth server sets session cookie on domain
.todayai.dev - Auth server redirects back to
todayai.dev - Browser sends
.todayai.devcookie on subsequent requests
If you access via localhost:4060 instead, step 6 breaks — the browser won't send .todayai.dev cookies to localhost. A domain proxy ensures the browser sees todayai.dev throughout, keeping cookies aligned.
BFF Proxy
The web app uses a Backend-for-Frontend pattern. All auth requests go through /api/auth/* route handlers that proxy to the auth server:
Browser --> /api/auth/get-session --> auth.todayai.dev/api/auth/get-session
(Next.js API route) (via OIDC_INTERNAL_AUTHORITY)OIDC_INTERNAL_AUTHORITY controls where the BFF proxy sends server-side requests:
- Local mode:
http://localhost:4010(direct, bypasses the proxy) - Remote dev mode:
https://auth.todayai.dev(direct to remote)
Troubleshooting
Login Redirects to Localhost and Fails
You're accessing localhost:4060 directly instead of through the configured proxy. Use https://todayai.dev (remote dev) or https://t.ai (local).
"Invalid origin" on Login
The auth server's BETTER_AUTH_TRUSTED_ORIGINS doesn't include your origin. For remote dev, https://todayai.dev should already be listed. For local, ensure https://t.ai is in the auth service's trusted origins.
"OIDC client_id not configured"
.env.development.local is missing or doesn't have NEXT_PUBLIC_OIDC_CLIENT_ID. Copy from the example and fill in credentials.
502 on Auth API Calls
The BFF proxy can't reach the auth server. Check:
- Remote dev:
OIDC_INTERNAL_AUTHORITY=https://auth.todayai.devin.env.development.local - Local: auth service running on port 4010,
OIDC_INTERNAL_AUTHORITY=http://localhost:4010
"invalid_client" Error
The OIDC client ID/secret are wrong or the client doesn't exist in the auth server's database.
- Remote dev: Pull credentials from Vercel (
vercel env pull) - Local: Run
pnpm db:seed-clientsin the auth service
Stuck on Login Redirect
Clear OIDC state from the browser:
- DevTools → Application → Local Storage
- Delete all keys starting with
oidc. - Refresh the page
CORS Errors
Ensure the auth server has your origin in BETTER_AUTH_TRUSTED_ORIGINS.
Dependent Services (Local Mode)
| Service | Port | Purpose |
|---|---|---|
| auth-service | 4010 | Authentication |
| api-gateway | 4020 | API Gateway |
| agent-service | 4030 | AI Agent |
Start them before the web app:
# In today-cloud repo
cd services/auth-service && pnpm dev # Terminal 1
cd services/api && pnpm dev # Terminal 2
cd services/agent-service && pnpm dev # Terminal 3
# In today-platform-web repo
pnpm dev # Terminal 4MSW handlers and fixtures
MSW (Mock Service Worker) is the mock-mode runtime — handlers + fixtures live under `apps/web/src/lib/msw/` and are also the source of truth for Storybook stories that need network responses.
Parallel worktrees
Run multiple branches in parallel via `git worktree add`. What env files to mirror, the cookie-jar caveat, and the future per-worktree hostname plan.