> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workspace on Start + Void

> Route groups carved from Web into the native Workspace app, the missing API guard, and the rollback to Vercel.

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](https://docs.tuturuuu.com/build/devops/start-bridge.md) and
[native Start migration](https://docs.tuturuuu.com/build/devops/native-start-migration.md)).

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

| Group | Source in Web | Status |
| - | - | - |
| Shell and proxy | `[locale]/layout`, `(dashboard)/layout`, `(dashboard)/[wsId]/layout`, `src/proxy.ts` | Carved. Cookie names, domain and `SameSite` are the Web implementation. |
| Auth | `(auth)/login`, `signup`, `add-account`, `auth/recovery`, `sso/consent`, `verify-token` | Carved. |
| Dashboard home | `(dashboard)/[wsId]/(dashboard)/page.tsx` and its Mira, assistant, user-group and generative-UI closure | Carved. `/[wsId]` renders the Mira dashboard. |
| Tasks | `(dashboard)/[wsId]/tasks/[[...slug]]` | Carved. Redirects to the Tasks app origin with the path and query preserved. |
| Calendar | `(dashboard)/[wsId]/calendar` | Carved. Redirects to the Calendar app origin. |
| Workspace settings | `(dashboard)/[wsId]/(workspace-settings)`: `settings` (+ `notifications`, `reports`), `members`, `roles` (redirects to `members?tab=roles`), `api-keys` (+ usage logs), `secrets`, `usage`, `sso`, `inquiries`, `billing` (+ `success`, which redirects to the Pay app) | Carved. `lib/datasets/page-access.ts` redirects a `401` to `/login` because the bridge has no `unauthorized()` boundary. |
| Documents | `(dashboard)/[wsId]/documents`, `documents/[documentId]` | Carved. |
| Habits | `(dashboard)/[wsId]/habits/[[...slug]]` | Carved. Redirects to the Tasks app origin with the path and query preserved. |
| Notes | `(dashboard)/[wsId]/notes` | Carved. |
| Memories | `(dashboard)/[wsId]/memories` | Carved. |
| Mira | `(dashboard)/[wsId]/mira` | Carved. |
| Integrations | `(dashboard)/[wsId]/integrations`, `integrations/discord` | Carved. |
| AI | `(dashboard)/[wsId]/ai`: `executions`, `spark` | Carved. |
| AI models, datasets, pipelines | `(dashboard)/[wsId]/(ai)`: `models`, `datasets`, `pipelines` | Carved. |
| AI chat | `(dashboard)/[wsId]/ai-chat`, `[chatId]`, `chatbots`, `my-chatbots`, `new` | Carved. |
| Changelog | `(dashboard)/[wsId]/changelog` | Carved. |
| Crawlers | `(dashboard)/[wsId]/crawlers` | Carved. |
| Polls | `(dashboard)/[wsId]/polls` | Carved. |
| Queues | `(dashboard)/[wsId]/queues` | Carved. |
| Locale root | `[locale]/page.tsx` (Workspace-owned) | Answers `404`. The locale-only path would otherwise match `/[wsId]` as a workspace named after the locale. Signed-in users never reach it because the proxy redirects them to their workspace. |
| Redirected by the proxy | `workforce`, plus `finance`, `drive`, `mail`, `chat`, `time-tracker`, `meet`, `users`, `posts` and `forms` (satellite redirects in `src/proxy.ts`) | Nothing to carve. The proxy answers with a redirect to the owning app before any page renders. |
| Not carved | `cron`, `diet`, `epm`, `external-projects`, `feedback`, `hive`, `inventory`, `link-shortener`, `metrics`, `mind`, `notifications`, `progress`, `qr-generator`, `readings`, `whiteboards`, `workouts`, `workspace-setup`, `(dashboard)/personal-journal` | Workspace answers `404` for them, so a router in front of it must send only the carved patterns here. |

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`.

| Surface | Browser `Origin` | Decision |
| - | - | - |
| `tuturuuu.com/workspace` mount | `https://tuturuuu.com` | Already allowed: it is the configured Web origin. No entry needed. |
| `workspace.tuturuuu.com` | `https://workspace.tuturuuu.com` | Listed in `WEB_CSRF_TRUSTED_ORIGINS`. Browsers call the subdomain's own `/api`, which is `same-origin` and passes through the forward; the entry covers clients without Fetch Metadata. |
| Staging hosts | Any staging Worker or staging subdomain | Not listed. Staging acceptance runs with `TTR_CSRF_MODE` at `off` or `report`. |

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

```bash theme={null}
for p in types supabase internal-api editor masonry devbox sdk; do
  (cd packages/$p && bun run build)
done
cd apps/workspace
bun run routes:check
bun run type-check:cloudflare
bun run build:cloudflare
CI=true CI_SOURCE_SHA=$(git rev-parse HEAD) bun run test:runtime
```

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

| Name | Where it is read | Production value |
| - | - | - |
| `NODE_ENV` | shared `define` (`process.env.NODE_ENV`) | `production` |
| `TUTURUUU_BASE_PATH` | optional; inlined as `import.meta.env.VITE_TUTURUUU_BASE_PATH` | unset, so one artifact serves the subdomain root and the `/workspace` mount |
| `NEXT_PUBLIC_SUPABASE_URL` | client `define` (`process.env`, every `NEXT_PUBLIC_*`) | `PRODUCTION_SUPABASE_URL` |
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | client `define` | `PRODUCTION_SUPABASE_PUBLISHABLE_KEY` |
| `NEXT_PUBLIC_APP_URL` | client `define` | `https://workspace.tuturuuu.com` |
| `NEXT_PUBLIC_WEB_APP_URL` | client `define` | `https://tuturuuu.com` |

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

| Name | Production |
| - | - |
| `INTERNAL_WEB_API_ORIGIN` | unset. The forward target stays `https://tuturuuu.com`. |
| `TUTURUUU_WEB_API_HOSTS` | unset. Extra Web hosts are refused. |

Runtime secrets, set on the Worker and never inlined by Vite:

| Name | Rule |
| - | - |
| `SUPABASE_SECRET_KEY` | same value Web uses |
| `TUTURUUU_APP_COORDINATION_SECRET` | exactly Web's signer value, which is Web's `SUPABASE_SECRET_KEY` |

`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](https://docs.tuturuuu.com/build/devops/d74-cutover-runbooks.md).

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.

| Group | Patterns |
| - | - |
| Dashboard home | `/[wsId]` |
| Workspace settings | `/[wsId]/settings`, `/[wsId]/settings/notifications`, `/[wsId]/settings/reports`, `/[wsId]/members`, `/[wsId]/roles`, `/[wsId]/api-keys`, `/[wsId]/api-keys/[keyId]/usage-logs`, `/[wsId]/secrets`, `/[wsId]/usage`, `/[wsId]/sso`, `/[wsId]/inquiries`, `/[wsId]/billing`, `/[wsId]/billing/success` |
| Documents | `/[wsId]/documents`, `/[wsId]/documents/[documentId]` |
| Habits | `/[wsId]/habits`, `/[wsId]/habits/[[...slug]]` |
| Notes | `/[wsId]/notes` |
| Memories | `/[wsId]/memories` |
| Mira | `/[wsId]/mira` |
| Integrations | `/[wsId]/integrations`, `/[wsId]/integrations/discord` |
| AI | `/[wsId]/ai/executions`, `/[wsId]/ai/spark` |
| AI models, datasets, pipelines | `/[wsId]/models`, `/[wsId]/datasets`, `/[wsId]/datasets/[datasetId]`, `/[wsId]/datasets/[datasetId]/api-references`, `/[wsId]/datasets/[datasetId]/explore`, `/[wsId]/datasets/[datasetId]/settings`, `/[wsId]/pipelines` |
| AI chat | `/[wsId]/ai-chat`, `/[wsId]/ai-chat/new`, `/[wsId]/ai-chat/[chatId]`, `/[wsId]/ai-chat/chatbots`, `/[wsId]/ai-chat/my-chatbots`, `/[wsId]/ai-chat/my-chatbots/new` |
| Changelog | `/[wsId]/changelog` |
| Crawlers | `/[wsId]/crawlers`, `/[wsId]/crawlers/uncrawled`, `/[wsId]/crawlers/[crawlerId]` |
| Polls | `/[wsId]/polls` |
| Queues | `/[wsId]/queues` |
| Tasks (redirect to the Tasks app) | `/[wsId]/tasks`, `/[wsId]/tasks/[[...slug]]` |
| Calendar (redirect to the Calendar app) | `/[wsId]/calendar` |

`/` (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:

```bash theme={null}
# Unknown host answers 404.
curl -sS -o /dev/null -w '%{http_code}\n' \
  -H 'Host: unknown.example' \
  https://workspace.tuturuuu.com/login

# Carved auth page.
curl -sS -o /dev/null -w '%{http_code}\n' \
  https://workspace.tuturuuu.com/login

# /api/v1 GET is forwarded and answered by Web (JSON, including Web's 400
# when the query is absent — not the Workspace HTML 404).
curl -sS -D - -o /dev/null \
  https://workspace.tuturuuu.com/api/v1/mobile/version

# Apex was not moved.
curl -sS -o /dev/null -w '%{http_code}\n' \
  https://tuturuuu.com/login

node scripts/cutover-smoke.mjs workspace https://workspace.tuturuuu.com
```

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.