@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
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 declaresstart-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 attuturuuu.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;verifyAppSessionTokenrequires an exact match) plustuturuuu_app_session_refresh, host-only atPath=/on its subdomain. - With
sessionMode: 'supabase-first'and the root Supabase cookies (sb-<ref>-auth-token*, shared on.tuturuuu.com) present,refreshAppSessionForRequestalready derives the app claims from Supabase, mints the access token in memory asAuthorization: Bearerfor that request, and then callsclearAppSessionCookie. - On the apex,
clearAppSessionCookiewould expiretuturuuu_app_session, its refresh cookie and Web’s owntuturuuu_web_app_session(+ refresh) atPath=/on every mounted-app request. - Web’s
createClient(packages/supabase/src/next/server.tsandrequest/server.ts,requestHasAppSessionAuth) switches to the isolated app-session client whenever atuturuuu_app_sessioncookie is present, so any app writing that cookie atPath=/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
decideOneOriginRequestgate (#6746) in front of every mount.
HttpOnly cookie is exchanged server-side and the browser Supabase client is
not used on the apex.
Proposed model (if accepted)
- 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*ortuturuuu_web_app_session*on the apex. The audience check stays per app; the Supabase JWT is validated on every request. - Mounted mode is decided by the bridge from the request URL only. The
handler deletes any client-sent
x-tuturuuu-app-baseand 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. - 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 inpackages/auth/src/proxy/index.ts),createSupabaseBackedAppSessionResponse, and the cross-app helpers inpackages/auth/src/cross-app/server.ts. Each checks the response’s target host, not a request header. The satelliteapi/auth/refresh-app-sessionandapi/auth/verify-app-tokenroutes reject apex (mounted) calls before any write. - CSRF. Every unsafe, cookie-authenticated endpoint on the apex requires a
same-origin
Origin(or a CSRF token) and rejectsSec-Fetch-Site: cross-site; server functions keep Start’s CSRF middleware. Logout is a protected unsafe operation, not a GET. - 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). - 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.
- 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 withx-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.