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’ssrc/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_ORIGINunset forwards tohttps://tuturuuu.com.- A set value must be an
httpsorigin with no credentials, port, path, query or fragment, and its host must betuturuuu.com, the built-in staging Web host (WEB_API_HOSTSinstart-bridge.config.ts) or a hostname listed inTUTURUUU_WEB_API_HOSTS. - Anything else throws, and the bridge answers its generic
500without 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 withapps/web before each deploy. They must keep behavior parity for:
logoutAllremoves the legacytuturuuu_multi_session_storelocalStorage 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 includeedit_external_projects.
/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 packagedist 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 reachestuturuuu-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.comserves the registry patterns at the host root.tuturuuu.comserves the same patterns under the base path/workspace.
/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 onapps/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 everytuturuuu.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.
- Dispatch
workspace-cloudflare.yamlonmainonly after that commit contains#7981(b84abea571) and its required workflows are green. The jobworkspace-cutover-artifactuploadsworkspace-cutover-<sha>. It does not deploy. - Confirm the sealed config name is
tuturuuu-workspace,workers_devis false, androutesis absent. - On the production Worker, leave
INTERNAL_WEB_API_ORIGINandTUTURUUU_WEB_API_HOSTSunset. SetTUTURUUU_APP_COORDINATION_SECRETto exactly Web’s signer value and set the sameSUPABASE_SECRET_KEY. Leave Web’sTTR_CSRF_MODEoffenforceuntil Web is deployed withapps/web/src/lib/security/csrf.ts. - Deploy the artifact with no route:
bunx wrangler deploy --config /tmp/workspace-cutover/dist/server/wrangler.json --name tuturuuu-workspace. - Attach only
workspace.tuturuuu.com.
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:
- The exact accepted
mainSHA (it contains#7981), the green required workflows for that SHA, and theworkspace-cutover-<sha>artifact from the dispatch ofworkspace-cutover-artifact. - 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.
- The secret and variable names in Required Worker environment, with
INTERNAL_WEB_API_ORIGINandTUTURUUU_WEB_API_HOSTSunset, andTTR_CSRF_MODEleft offenforceuntil Web shipscsrf.ts. - The gateway allowlist above, so deleting
workspace.tuturuuu.comis ready before the flip. - A post-deploy watch with immediate rollback on any
5xxor login regression.