Skip to main content
Workspace (apps/workspace) is the native TanStack Start + Vite+ + Void home for the signed-in workspace shell. Its source is carved from apps/web: each route group is copied with its import closure, and @tuturuuu/start-bridge supplies the Next ports, request context, proxy and central API forwarding (see Start bridge and native Start migration). Qualification stays on the staging Worker name. A dispatch-only job, workspace-cutover-artifact, seals a route-free production artifact named tuturuuu-workspace (workers_dev: false, preview_urls: false, no routes). productionPublicationPolicy in tuturuuu.ci.ts does not admit the app, and apps/web on Vercel keeps serving every live path until a release owner attaches a host. The job does not deploy.

Route groups

Run bun run routes:check after every carve. If it reports stale registries, run bun ../../packages/start-bridge/src/codegen/cli.ts generate .. The page module map (src/start-bridge/page-modules.bridge.gen.ts) is regenerated from the codemod’s pageModulesModule template over the route inventory, so each new page gets its layout chain.

API guard and forward origin

Web’s src/proxy.ts runs an API cost guard and CSRF guard before an API request. Both import server-only modules that break the Start build, so Workspace’s copy of the proxy does not run them. Every /api request the app does not own is forwarded to the central Web API, whose proxy runs the guard, so the abuse and CSRF checks stay in force for every API call. Do not point /api at a Workspace-local handler and do not add an apiOrigin that bypasses Web until the guard is restored in the portable package. That protection holds only while the forward target runs Web’s proxy. start-bridge.config.ts therefore resolves the target through resolveWebApiOrigin for every forwarded request:
  • INTERNAL_WEB_API_ORIGIN unset forwards to https://tuturuuu.com.
  • A set value must be an https origin with no credentials, port, path, query or fragment, and its host must be tuturuuu.com, the built-in staging Web host (WEB_API_HOSTS in start-bridge.config.ts) or a hostname listed in TUTURUUU_WEB_API_HOSTS.
  • Anything else throws, and the bridge answers its generic 500 without contacting the target. A blank value is invalid, not unset.
TUTURUUU_WEB_API_HOSTS is a comma-separated list of exact dotted hostnames (no wildcards, no localhost), read from the Worker environment and never derived from a request. Set it only on a deployment whose Web origin is not in the built-in list, such as a staging Web Worker, and review the value like any other binding. The origin is also a binding, so a localhost or http target is refused: run local development against an https Web host and list it. The runtime suite binds https://central.invalid and never forwards an API request, so it does not exercise the check; start-bridge.config.test.ts does.

CSRF mode and origins

TTR_CSRF_MODE (off by default, then report, then enforce) is read by Web’s proxy guard, guardWebApiCsrf in apps/web/src/lib/security/csrf.ts. Workspace does not read it: Workspace forwards /api to Web, so the check runs there, and Workspace’s own server functions use TanStack Start’s CSRF middleware. The guard accepts a cookie-authenticated mutation when Sec-Fetch-Site is same-origin, or, for a client that sends no Fetch Metadata, when Origin is in allowedOrigins. That list is the request’s own origin, the configured Web origin and WEB_CSRF_TRUSTED_ORIGINS. A same-site request (a sibling subdomain calling another app’s API directly) is rejected in enforce mode even when its Origin is listed. The list does not widen that rule, and Workspace pages never make such calls. Moving TTR_CSRF_MODE to enforce for Web needs no further Workspace change; run the mode in report first and check the CSRF check failed logs for workspace.tuturuuu.com before enforcing. Regression coverage: apps/web/src/lib/security/csrf.test.ts.

Synced copies

Workspace holds copies of Web files. Re-compare the auth and account files with apps/web before each deploy. They must keep behavior parity for:
  • logoutAll removes the legacy tuturuuu_multi_session_store localStorage key before it revokes server sessions, and tolerates a storage error.
  • The account list refetches on window focus and is never stale, and a save that hits the account limit returns errorCode: 'account_limit' so the add-account button, switcher modal and add-account page show the limit instead of navigating away.
  • Recovery sessions (lib/auth/recovery-session.ts, recovery.ts) are created with the recovering browser’s user agent.
  • The sessions card describes devices with lib/sessions/session-user-agent.ts.
  • External-project scope checks live in lib/external-projects/access-mode.ts, and the route permissions include edit_external_projects.
Intentional differences from Web: no roadmap proxy or /roadmap public path (Web owns the public roadmap), no API cost and CSRF guard in the proxy (see above), lib/datasets/page-access.ts redirects a 401 to the login, and the workspace settings page omits the newsletter sending-domain form, which depends on Web-only services. Coverage lives in src/context, src/lib/auth, src/lib/sessions and src/lib/external-projects; run it with bunx vitest run from apps/workspace.

Hosts and Vercel

start-bridge.config.ts sets mounts.unknownHost to reject, so only workspace.tuturuuu.com and the tuturuuu.com/workspace mount are served and any other host gets the bridge’s 404. Both deployment profiles emit workers_dev: false and preview_urls: false, and neither declares routes. VITE_WORKSPACE_NATIVE_PROFILE=production selects the name tuturuuu-workspace and TUTURUUU_NOINDEX=0; an unset profile keeps the staging Worker name and TUTURUUU_NOINDEX=1. No Vercel project is linked: apps/workspace has no .vercel directory, and its vercel.json disables Git deployments and the GitHub integration.

Local verification

Build the workspace package dist folders first (they are gitignored):
bun run preview:cloudflare writes .void/preview-deploy-config.json, which makes test:runtime fail with “Exactly one native/saved deploy pointer required”. After a preview run, rm -rf .void .void-wrangler.jsonc dist, rebuild, then test. Smoke against 127.0.0.1:<port>; the preview host check rejects workspace.tuturuuu.com. Stop only the preview process you started. The runtime suite uses an empty synthetic environment, so Supabase calls fail by design. It asserts the status of the public pages in start-bridge.routes.json, the apex mount, the unknown-path policy and the response policy. The 307 results for dashboard paths are expected: an unauthenticated request is redirected to the central login.

Required Worker environment

Names only. vite.config.mts calls startBridgePreset, which injects these at build time: void.config.ts emits the Worker var TUTURUUU_NOINDEX (0 on the production profile, 1 on staging). The ASSETS binding is part of the emitted config. start-bridge.config.ts reads these from the Worker environment at request time: Runtime secrets, set on the Worker and never inlined by Vite: TTR_CSRF_MODE is Web’s (apps/web/src/lib/security/csrf.ts). Workspace does not read it. Leave it off enforce until Web itself is deployed with that csrf.ts change.

Gateway plan

This page does not change the gateway. The sealed artifact has no routes. Traffic reaches tuturuuu-workspace only when a release owner attaches a host after the deploy conditions in the D74 cutover runbooks. The Worker has two surfaces (cloudflare/worker.ts):
  • workspace.tuturuuu.com serves the registry patterns at the host root.
  • tuturuuu.com serves the same patterns under the base path /workspace.
Live Web URLs are not under /workspace. They are https://tuturuuu.com plus the locale prefix plus the pattern. src/i18n/routing.ts uses localePrefix: as-needed with locales en and vi: English has no prefix, Vietnamese is /vi. start-bridge.routes.json strips [locale] (default en), so page-registry.bridge.gen.ts lists the pattern without it. A real URL is the locale prefix plus the pattern.

Patterns that belong on Workspace

Auth, with no [wsId] segment. English and Vietnamese:
  • /login, /vi/login
  • /signup, /vi/signup
  • /add-account, /vi/add-account
  • /auth/recovery, /vi/auth/recovery
  • /auth/recovery/confirm, /vi/auth/recovery/confirm (the only owned API route, api-patterns.bridge.gen.ts)
  • /sso/consent, /vi/sso/consent
  • /verify-token, /vi/verify-token
[wsId] pages from page-registry.bridge.gen.ts. Each line is the English pattern; the Vietnamese URL inserts /vi after the origin. On the apex mount, prefix /workspace as well. / (and /vi) is the locale root and answers 404. It stays off the Worker on tuturuuu.com. Every /api/* path other than /auth/recovery/confirm is forwarded to Web.

Patterns that stay on Web

Anything not in the lists above stays on apps/web. That includes the uncarved groups: cron, diet, epm, external-projects, feedback, hive, inventory, link-shortener, metrics, mind, notifications, progress, qr-generator, readings, whiteboards, workouts, workspace-setup, and personal-journal. The proxy redirects mail, finance, drive, chat, time-tracker, meet, users, workforce, posts, and forms to their apps; leave those paths on Web for the first flip so satellite routing stays where it is. [wsId] is one dynamic segment. A path rule written as a prefix of that segment (/:wsId/*, or /:locale/:wsId/*) also matches every uncarved group. Workspace answers 404 for those groups, so the prefix takes them off Web. The same one-segment shape matches a locale (/vi) and any other single-segment Web path. A path-only split is safe when the gateway allowlists the carved suffixes in the table and keeps every uncarved group on Web.

Smallest safe first flip

Leave every tuturuuu.com path on Web. Attach one host, workspace.tuturuuu.com, to the route-free artifact. That host serves the patterns at its root and does not split apex paths. Uncarved suffixes on that host answer 404; the same paths on tuturuuu.com stay on Web.
  1. Dispatch workspace-cloudflare.yaml on main only after that commit contains #7981 (b84abea571) and its required workflows are green. The job workspace-cutover-artifact uploads workspace-cutover-<sha>. It does not deploy.
  2. Confirm the sealed config name is tuturuuu-workspace, workers_dev is false, and routes is absent.
  3. On the production Worker, leave INTERNAL_WEB_API_ORIGIN and TUTURUUU_WEB_API_HOSTS unset. Set TUTURUUU_APP_COORDINATION_SECRET to exactly Web’s signer value and set the same SUPABASE_SECRET_KEY. Leave Web’s TTR_CSRF_MODE off enforce until Web is deployed with apps/web/src/lib/security/csrf.ts.
  4. Deploy the artifact with no route: bunx wrangler deploy --config /tmp/workspace-cutover/dist/server/wrangler.json --name tuturuuu-workspace.
  5. Attach only workspace.tuturuuu.com.
Smoke that host before calling the flip done:
Logout-all is a signed-in browser check. Set localStorage['tuturuuu_multi_session_store'], use logout-all, and confirm the key is gone. The regression is apps/workspace/src/context/account-switcher-context.test.tsx. One-step rollback: delete the workspace.tuturuuu.com route or custom domain. Do not rebuild, and do not change apex DNS. tuturuuu.com is still the Web deployment recorded in the runbook.

Rollback and cutover

Rollback is a routing change, never a data change. apps/web is untouched and serves every path on Vercel. Removing the workspace.tuturuuu.com route returns that host to its previous origin. No database migration or write is part of this app. Before any cutover the release owner records:
  1. The exact accepted main SHA (it contains #7981), the green required workflows for that SHA, and the workspace-cutover-<sha> artifact from the dispatch of workspace-cutover-artifact.
  2. The immutable prior Vercel deployment ID and URL for the Web project. Capturing it is a provider read and belongs to the release owner, not to this change.
  3. The secret and variable names in Required Worker environment, with INTERNAL_WEB_API_ORIGIN and TUTURUUU_WEB_API_HOSTS unset, and TTR_CSRF_MODE left off enforce until Web ships csrf.ts.
  4. The gateway allowlist above, so deleting workspace.tuturuuu.com is ready before the flip.
  5. A post-deploy watch with immediate rollback on any 5xx or login regression.
DNS, zone and routing changes need release-authority approval.