Skip to main content
@tuturuuu/start-bridge is the shared layer that lets a satellite’s existing App Router source run under TanStack Start, Vite+ and Void on a Cloudflare Worker. It replaces the per-app src/runtime shim copies in Meet, Mail and Calendar. The core (route-match, base-path, mounts, app-url, endpoints, pages, api-forward, canary, csp, request-context/core) has no React, Start or Vite imports; the next/*, start/*, intl/* and vite entries are the framework adapters at the edge. This page is the v0 API freeze. App lanes convert against these export paths and signatures. Within v0, changes are additive only: a new optional option, a new export, or a new subpath. Removing or changing a frozen signature needs a new subpath and a coordinated migration.

Export map

Worker handler

Order: base resolution from mounts (a mount host outside the mount is 404), /assets/* and declared public files from ASSETS (base-stripped), then the app’s proxy.ts, centralFirst paths to the central API (Next beforeFiles rewrites such as TUTURUUU_WEB_WORKSPACE_API_RESERVED_PATHS; Next sources like /api/v1/:wsId/x or bracket patterns), the generated endpoint registry, unowned /api/* to the central API (base-stripped), server functions to Start (base-stripped), and finally pages to Start at the browser-visible URL. The response then gets Location rewriting. Cookies pass through unchanged. A proxy.ts rewrite to another origin throws unless that origin is the configured central API origin (rewriteOrigins), which is then fetched with manual redirects and hop-by-hop headers removed. In the app: start.ts passes serverFns: { fetch: bridgeServerFnFetch } to createStart, getRouter() sets basepath: routerBasepath() and ssr: { nonce: requestNonce() }, and the root route lists appBaseHeadScript() first in head().scripts. With csp set, every response gets a fresh nonce in its Content-Security-Policy, which the router and the head script repeat. On the apex the handler sits behind the one-origin gate (decideOneOriginRequest, #6746).

Registries and the unregistered-route check

Each app declares start-bridge.routes.json at its root:
bun packages/start-bridge/src/codegen/cli.ts generate apps/<app> writes the registries; check fails when appDir is missing or not a directory (declare "appDir": null for an app without App Router source), when a generated file is stale, a route.* or page.* is neither generated, owned by an existing native file, nor ignored with a reason, an owned/ignored entry names no file, two files map to one URL shape (including [id] vs [slug], or a page and a route), or a parallel/intercepting route appears. Route groups are dropped from patterns. The page manifest exports PagePattern; type the page model as satisfies Record<PagePattern, Loader> so a new page fails type-check until it is handled. *.bridge.gen.ts files are excluded from Biome.

basePath contract

One compiled artifact serves an app at its subdomain root and mounted at tuturuuu.com/<app>. The Worker resolves the base per request from the host and path against mounts (rootHosts → ''; mountHosts + the /<app> prefix → /<app>; unlisted hosts default to the root, or 404 with unknownHost: 'reject'). The build-time TUTURUUU_BASE_PATH is only the fallback when no runtime base exists. Root mode is the identity everywhere. tuturuuu.com/meet is the Meet app root; product marketing moves to /products/<app>.

Shared session on the apex (proposal for security review)

Decision (user, 2026-10-25): keep one Tuturuuu session, usable across every app; no per-app session cookies. The bridge therefore leaves all cookies untouched. Making that safe on the apex needs the auth change below. It is a proposal awaiting security review; nothing here is implemented yet.

Current model and the apex problems

  • Each satellite holds a per-app access token in tuturuuu_app_session (target_app = that app; verifyAppSessionToken requires an exact match) plus tuturuuu_app_session_refresh, host-only at Path=/ on its subdomain.
  • With sessionMode: 'supabase-first' and the root Supabase cookies (sb-<ref>-auth-token*, shared on .tuturuuu.com) present, refreshAppSessionForRequest already derives the app claims from Supabase, mints the access token in memory as Authorization: Bearer for that request, and then calls clearAppSessionCookie.
  • On the apex, clearAppSessionCookie would expire tuturuuu_app_session, its refresh cookie and Web’s own tuturuuu_web_app_session (+ refresh) at Path=/ on every mounted-app request.
  • Web’s createClient (packages/supabase/src/next/server.ts and request/server.ts, requestHasAppSessionAuth) switches to the isolated app-session client whenever a tuturuuu_app_session cookie is present, so any app writing that cookie at Path=/ on the apex drops Web’s Supabase session. If several apps wrote it, they would overwrite each other’s per-app token.

Decision required: a JavaScript-readable shared session on one origin

The root Supabase session cookies (sb-<ref>-auth-token*) are set with Path=/, SameSite=Lax, Secure and Domain=.tuturuuu.com but not HttpOnly: getSupabaseCookieOptions (packages/supabase/src/next/common.ts) leaves @supabase/ssr’s default httpOnly: false, because the browser Supabase client reads the session. Do not toggle that flag: browser auth depends on it. Blast radius. Today an XSS in one satellite can already read the root session through the shared .tuturuuu.com domain cookie. On one origin the radius is wider: script running under any tuturuuu.com/<app> path also shares the origin’s localStorage, sessionStorage, IndexedDB, service-worker scope and same-origin fetch, so one app’s XSS can act as the user in every app and in Web without reading a cookie at all. Path separation is not a security boundary within an origin. Accepting this requires, per mounted app:
  • an enforced CSP with per-response script nonces (createBridgeHandler({ csp }), requestNonce()) and Trusted Types, which is already mandatory before an app mounts on the apex;
  • no inline event handlers or eval-style sinks, and dependency review for script injection;
  • the decideOneOriginRequest gate (#6746) in front of every mount.
Alternatives if the risk is not accepted: keep satellites on their subdomains (today’s isolation); mount only apps that pass a stricter security review; or move the browser to a backend-for-frontend session where an HttpOnly cookie is exchanged server-side and the browser Supabase client is not used on the apex.

Proposed model (if accepted)

  1. The shared apex session is the root Supabase session. Mounted apps derive their per-app claims per request (in-memory bearer, as today) and never write or clear tuturuuu_app_session* or tuturuuu_web_app_session* on the apex. The audience check stays per app; the Supabase JWT is validated on every request.
  2. Mounted mode is decided by the bridge from the request URL only. The handler deletes any client-sent x-tuturuuu-app-base and sets its own from the resolved mount. Auth code must not trust that header on any route a client can reach without the bridge in front (direct Worker hosts, Web, or tests); the invariant below is enforced by the writers themselves.
  3. The no-app-cookie invariant holds at every writer and clearer on the apex host: setAppSessionCookie, setAppSessionRefreshCookie, setWebAppSessionCookie, setWebAppSessionRefreshCookie, clearAppSessionCookie (including the MFA-redirect clear in packages/auth/src/proxy/index.ts), createSupabaseBackedAppSessionResponse, and the cross-app helpers in packages/auth/src/cross-app/server.ts. Each checks the response’s target host, not a request header. The satellite api/auth/refresh-app-session and api/auth/verify-app-token routes reject apex (mounted) calls before any write.
  4. CSRF. Every unsafe, cookie-authenticated endpoint on the apex requires a same-origin Origin (or a CSRF token) and rejects Sec-Fetch-Site: cross-site; server functions keep Start’s CSRF middleware. Logout is a protected unsafe operation, not a GET.
  5. Logout and refresh races. Logout signs the user out of the Supabase session, so it is global across Web and every mounted app by design. Concurrent tabs and Workers may refresh the same Supabase refresh token: rely on the project’s refresh-token reuse interval, never let a refresh response restore cookies after a sign-out (responses compare a session generation before writing Set-Cookie), and accept that an already issued access JWT stays valid until its expiry (bound the access-token lifetime accordingly; revocation-sensitive operations re-check the session).
  6. Verification cost. Each mounted-app request validates the Supabase JWT (local with asymmetric keys; JWKS cached with rotation-aware refetch), refreshes when near expiry, and may run the MFA lookups the satellites already run. Measure the p95 on staging before cutover.
  7. Subdomains keep today’s behaviour until each app’s cutover.

Code that must change (auth owner, after acceptance)

Page adapter

The generic App Router page adapter (React Server Components) and the 'use server' path are owned by the conversion codemod in #6760, which also owns the RSC-environment shims for next-intl/* and next/root-params. The bridge keeps its request store on a global Symbol so every bundled environment observes the same request.

Runtime tag

Cutovers are direct, with an instant one-step rollback prepared before the flip and a post-deploy watch (see the native Start migration runbook); there are no staged canaries. Tag every native response with x-tuturuuu-runtime: native (RUNTIME_HEADER; tagResponse with a native decision) so the watch can tell native traffic from the prior deployment. The cohort helpers in canary (decideCohort, TUTURUUU_NATIVE_CANARY_PERCENT) are retained but unused.

Vite preset

basePath defaults to TUTURUUU_BASE_PATH at build time. List shims explicitly when converting an app whose current config aliases only some next/* modules, so the bundle does not change.

CI template

.github/workflows/native-start-app.yaml is the reusable validation for a bridged app. Each app keeps a thin caller (<app>-cloudflare.yaml) that owns the triggers, the path filters (the app, every workspace package in its dependency closure including packages/start-bridge/**, its artifact helper, bun.lock, the caller and the template), concurrency and the ci-check.yml gate, then calls the template with: The template validates the inputs and runs, on the exact source SHA: frozen install, dependency build, build identity, the registry check, unit tests, Biome, the Void build, route-tree drift, type-check, emitted-config validation, emitted binding-type compatibility (a scoped harness: the generated bindings become a module importing only the Workers types they name, checked under the app’s own tsconfig, so global runtime types never retype app code), Wrangler deploy --dry-run, then seal → emitted runtime → verify → pack. A second job restores the archive into clean output and repeats the dry-run, runtime acceptance and byte verification. It has no publish job or provider credential; production stays held in tuturuuu.ci.ts. Apps with extra acceptance (Apps’ browser anchors, Meet’s realtime Worker) keep those steps in their own workflow until the template grows an input for them.

Verification

packages/start-bridge unit tests cover each core module and adapter at the root and under /meet, plus the ported Meet proxy-bridge regressions. App adoption still needs that app’s exact-commit CI: unit, type-check, lint, vp build, the sealed artifact and emitted-runtime acceptance where the app has them.