- Registered internal apps consume cross-app login tokens generated by
apps/web, then store a Tuturuuu-managed app-session JWT cookie. They must not create app-local Supabase Auth sessions. - The
ttrCLI also exchanges central-login handoff tokens for Tuturuuu-managed gateway JWTs. It must not store Supabase Auth access or refresh tokens. - External apps must be registered in the Infrastructure dashboard before they can exchange a central-login token for API access.
- App coordination bearer tokens are never minted from a cross-app token alone.
The exchange route requires a registered
appIdplus app secret, and external-project APIs require explicit API scopes. - Platform admins issue or rotate external app secrets from
Infrastructure -> External Apps. The secret is shown once; Tuturuuu stores only a hash in root workspace secrets.
Registered internal app flow
Registered internal apps are the apps listed inpackages/utils/src/internal-domains.ts, such as cms, calendar, nova,
rewise, tasks, drive, finance, inventory, track, learn, teach,
chat, mail, mind, meet, and hive.
- The app redirects unauthenticated users to
apps/weblogin with areturnUrlpointing back to the app’s/verify-token?nextUrl=...route. That return URL must resolve to the satellite app origin, not the central web origin. Registered apps should useresolveInternalAppUrl()for theirBASE_URL/app URL constants so shared environment variables such asBASE_URL=https://tuturuuu.comare rejected for satellite hosts instead of minting the host-only app-session cookie on the wrong domain. Ifapps/webreceives a registered internal appreturnUrlthat points at/loginor a protected product route, it must normalize the token handoff back through that app’s/verify-tokenroute before redirecting. apps/webvalidates the return target and mints the normal one-time cross-app handoff token.- The app
/verify-tokenroute posts only that handoff token to/api/auth/verify-app-token. Registered app proxies should perform this step withconsumeVerifyTokenRequest()before page rendering; the shared React verifier page is only a fallback. - The app-local verifier validates the handoff token and sets host-only
tuturuuu_app_sessionandtuturuuu_app_session_refreshcookies. Satellites that require rewrittenapps/webAPI access must callcreatePOST('<app>', { verificationBaseUrl: WEB_APP_URL })so the central verifier also returns the Web-issuedtuturuuu_web_app_sessionandtuturuuu_web_app_session_refreshcookies. The cookies areHttpOnly,SameSite=Lax,Path=/, andSecurein production. - The verifier trusts the
Set-Cookieresponse and redirects tonextUrl. It must not callsupabase.auth.setSession()and must not receive Supabase access or refresh tokens.
sub, email,
origin_app, target_app, scopes, iat, exp, and jti. Verification
rejects expired tokens. Target app and scope checks are enforced by the caller,
so every API route that opts into app-session auth must bind the token to the
audience and scope it expects. Access tokens carry internal-app:session.
Refresh tokens carry only internal-app:refresh; they are rotation material and
must not be accepted as API credentials or bearer access tokens.
For internal API calls, apps forward the app-session cookie to apps/web.
Routes that opt into app-session auth use the verified claims as the actor and
must keep authorization explicit. Do not pass ttr_app_... HS256 JWTs to
Supabase as user access tokens.
Satellite API proxies should treat the host-only tuturuuu_app_session cookie
as an authenticated API signal before applying anonymous proxy rate limits; a
signed-in app user doing normal dashboard work must not share anonymous
read/mutation buckets with public traffic.
Every central apps/web API consumed by a registered internal app must accept
tuturuuu_app_session before falling back to Supabase Auth. Routes using
withSessionAuth should opt into app-session auth; legacy route handlers should
use resolveSessionAuthContext(request, { allowAppSessionAuth: true }) so
local satellite apps do not loop between /login and product pages or
accumulate api_auth_failed IP blocks.
apps/drive follows the centralized-API satellite model: the standalone Drive
app owns the workspace shell and Drive explorer UI, while storage list,
analytics, upload, delete, share, export, and migration APIs remain in
apps/web /api/v1/workspaces/:wsId/storage/*. Those routes authenticate
Drive’s app-session cookie through the shared storage route auth helper before
checking manage_drive.
Use route-specific app-session constraints instead of accepting any registered
internal app token. The legacy allowAppSessionAuth: true shorthand is
audience-bound in apps/web/src/lib/api-auth.ts: it maps known internal API
path prefixes to their expected satellite app audience and falls back to the
platform audience for unmapped routes. For example, CLI-only platform APIs
should configure withSessionAuth with targetApp: 'platform' and
requiredScope: 'cli:access'; satellite APIs should use that satellite’s
target app and only the scopes intended for that route. When adding a new
boolean app-session route for a satellite API, update the shared audience map or
use an explicit { targetApp, requiredScope } object in the route.
When a registered app forwards request cookies to apps/web, it must strip
stale sb-*-auth-token Supabase cookies whenever tuturuuu_app_session is
present. Registered app proxies should also call the shared
clearSupabaseAuthCookies() helper on API, redirect, and normal middleware
responses so stale Supabase session cookies are expired on the satellite host.
If both tuturuuu_app_session and tuturuuu_web_app_session are forwarded, the
Web-issued cookie is the preferred actor for central apps/web API auth; the
local app cookie remains the fallback for routes that cannot verify the Web
cookie.
Registered app proxies must consume /verify-token handoff requests before page
rendering. Use the shared consumeVerifyTokenRequest() helper from
@tuturuuu/auth/proxy so the proxy validates the handoff token through the
app-local /api/auth/verify-app-token route, copies the verifier’s
Set-Cookie headers onto the redirect, and sends the browser directly to the
safe nextUrl. The shared React verifier page remains a fallback for runtimes
where proxy middleware is unavailable; it should not be part of the normal
registered-app login path.
Registered app proxies must run app-session refresh before page redirects and
before API proxy guards. When access is close to expiry, or already expired but
a refresh cookie is still valid, the proxy posts to the app-local
/api/auth/refresh-app-session endpoint. The app-local endpoint asks
apps/web to validate and rotate the Web-issued refresh token, then sets fresh
local and Web-issued cookie pairs. The proxy must forward those refreshed cookie
values into the current request headers before the API route or page handler
runs.
A valid refresh cookie must recover an expired access token on the satellite
origin. Do not redirect the browser back to apps/web for a new cross-app
handoff unless both local refresh credentials and Web-issued refresh credentials
are missing or invalid.
Gateway API routes for registered apps should resolve the app-session actor
before any central-web Supabase Auth fallback, and admin-backed checks must use
no-cookie admin clients so satellite responses do not inject Supabase Auth
cookies back onto the app host. Shared Supabase server-client helpers must treat
requests carrying tuturuuu_app_session or Authorization: Bearer ttr_app_...
as app-session requests and avoid constructing cookie-backed Supabase clients for
those requests.
When an app-session or CLI route needs an admin-backed Supabase client for
shared helpers such as normalizeWorkspaceId(), wrap that admin client with
attachSupabaseAuthUser() from @tuturuuu/auth/app-session using the verified
actor first. This keeps personal workspace resolution, permission checks, and
shared route helpers on the same authenticated path without duplicating
per-route personal-workspace fallback queries.
CLI app-session flow
The nativettr CLI uses the same gateway JWT model without a browser cookie:
ttr loginopensapps/webat/api/cli/auth/start, or prints that URL in--copymode.- The authenticated web session mints the normal short-lived cross-app handoff
token for target app
platformand origincli. - The CLI posts the handoff token to
/api/cli/auth/verify. - The verify route returns a Tuturuuu-managed access JWT plus a longer-lived refresh JWT. It does not call Supabase magic-link, OTP, or refresh-session APIs.
- CLI API requests send the access JWT as
Authorization: Bearer ttr_app_.... App-session-aware internal API routes resolve the actor from verified claims and use explicit authorization. - The CLI refreshes shortly before access-token expiry and retries once after a
401. Refresh rotates both JWTs and updates the saved config.
platform target app and
the cli:access scope before passing the request to admin-backed workspace or
task handlers. Do not rely on the generic internal-app:session scope for
CLI-only APIs.
External app flow
- Register the external app ID, allowed origins, and allowed API scopes in the Infrastructure dashboard.
- Issue an app secret and store it in the external app runtime environment.
- Send users to the centralized web login with a
returnUrlpointing back to the registered external app origin. - The external app receives a short-lived cross-app
tokenon its return URL. - The external app server calls
POST /api/v1/auth/app-token/exchangewithappId,appSecret,token, optionalrequestedScopes, andworkspaceIdwhenever the requested or issued scopes includeexternal-projects:*,external-projects:manage,external-projects:publish, orexternal-projects:read. This server-to-server route must bypass browser challenges and WAF managed challenges. A Cloudflare response withcf-mitigated: challengeis an edge block, not an app-token validation failure, and the external app cannot complete it withfetch. - For external-project scopes, Tuturuuu validates that the workspace has an
enabled external-project binding, that the binding’s canonical adapter
matches the registered external app ID, that the user is a direct
MEMBERof the linked workspace, and that the user has the required EPM permission in that workspace. Root EPM admins can manage app registrations and bindings from the platform infrastructure UI, but they cannot enter or call a linked external app unless they are also members of that linked workspace. - If the user is not yet a workspace member but has a pending
workspace_invitesorworkspace_email_invitesrow for the requested workspace, the exchange returns403withcode: "PENDING_WORKSPACE_INVITE",workspaceId, andinvitationUrl. External apps must handle this code before showing generic no-access copy so the user can accept or reject the invitation in Tuturuuu first. - Tuturuuu validates the app secret and cross-app token, then returns a
short-lived bearer token for Tuturuuu APIs plus the normalized authorized
workspaceId.
401, 403, or 404, the app must clear its local
session and send the user through the central login flow again.
The centralized login page validates external and internal returnUrl targets
before it renders the normal sign-in form or account-confirmation handoff. If the
target origin is not registered or cannot be resolved, the page shows a warning
and asks the user to clear the broken return URL instead of silently falling back
to the generic login form.
Operational notes
App-session and external app bearer tokens are signed withTUTURUUU_APP_COORDINATION_SECRET.
Token lifetimes are configured from the root workspace Infrastructure page at
/{wsId}/infrastructure/app-coordination; the older
/{wsId}/settings/infrastructure/app-coordination path redirects there. The
policy is stored in
workspace_secrets on ROOT_WORKSPACE_ID as
APP_COORDINATION_SESSION_POLICY and cached in process for at most 60 seconds
on auth paths. Defaults and caps are:
- Internal app access TTL: 28,800 seconds default, min 300, max 86,400.
- Internal app refresh TTL: 2,592,000 seconds default, min 86,400, max 7,776,000.
- Internal app refresh-early window: 900 seconds default, min 60, max 7,200.
- Browser refresh replay grace: 30 seconds default, min 0, max 300.
- External app bearer TTL: 28,800 seconds default, min 300, max 86,400.
- CLI access TTL: 28,800 seconds default, min 300, max 86,400.
- CLI refresh TTL: 7,776,000 seconds default, min 86,400, max 7,776,000.
tuturuuu_app_session must share the same signing material.
Prefer setting TUTURUUU_APP_COORDINATION_SECRET on apps/web and every
registered satellite. For compatibility with existing Vercel satellite
deployments, the runtime also accepts the server-side Supabase secret as a
fallback verifier so satellites without the explicit coordination secret do not
fail immediately after cross-app token validation.
When rotating an app secret, deploy the new external app environment first, then
rotate from Infrastructure. Existing short-lived bearer tokens keep working
until they expire, but future exchanges require the new app secret.
Cross-app login tokens are user-bound handoff tokens. The database RPC only
mints a token for the authenticated caller’s own auth.uid(); do not use it as a
server-side impersonation primitive or as a replacement for app credentials.
Registered apps should clear tuturuuu_app_session on local logout and expire
stale sb-*-auth-token cookies on the app host. Browser form logout should
redirect back to the app’s landing/login page or to the central /logout
continuation after local cleanup instead of leaving the browser on
/api/auth/logout. Logout and browser-state recovery redirects must use the
app’s configured public URL as the fallback when the request reaches the app
through a wildcard listener such as 0.0.0.0, because that listener address is
not a browser destination.
Shared satellite UI must not assume every app publishes the full apps/web
public/media tree. Shared brand images such as TuturuuLogo should use the
canonical hosted Tuturuuu asset URL so apps without local /media/logos/*
files do not produce repeated 404s.
Satellite shell patterns
Workspace-scoped satellite apps (drive, calendar, tasks, finance, and
similar) should reuse the shared satellite provider shell from
@tuturuuu/satellite/providers. That shell mounts next-themes with
system, light, and dark, wraps NextIntlClientProvider, and wires the
shared TanStack Query client. Public or gateway-only apps such as apps/qr and
apps/apps should use the same provider re-export so theme toggles and shared
UI chrome behave consistently even when the app does not own workspace auth.
Every user-facing browser app that should appear in the Apps gateway or the
global Ctrl/Cmd+K launcher must be registered in
packages/utils/src/launchable-apps.ts. Keep the registry entry current with
the app title, slug, aliases, category, production URL, Portless/dev origin,
default path, package root, and workspace path resolver when the app has
workspace-scoped routes. apps/apps/src/lib/apps-registry.ts reads from that
shared registry so the gateway and launcher do not drift.
Workspace sidebar apps should keep an app-local structure.tsx file, but that
file should be a thin wrapper around
@tuturuuu/satellite/sidebar-structure. The shared structure owns the
collapsible sidebar behavior, SidebarProvider cookies, workspace switcher
placement, mobile header, footer actions, hover expansion, back navigation, and
user-nav slots. App wrappers should only provide app-specific navigation links,
workspace-select routing, billing URL shape, or small brand/content additions
such as Mind boards and Rewise branding. This applies to workspace product apps
such as Calendar, CMS, Drive, Finance, Inventory, Mind, Rewise, Tasks, Track,
Mail, Chat, and Meet. Learn and Teach intentionally keep their
education-specific shells.
Meet has a public /:planId route surface, so workspace-scoped Meet routes must
live under /workspace/:wsId instead of /:wsId. Use
/workspace/:wsId/plans for Meet Together plans and
/workspace/:wsId/meetings for meeting-room operations, with
/workspace/:wsId redirecting to the plans route. Keep public plan-detail URLs
on the root plan-id surface.
For server-side workspace picker data, do not call
@tuturuuu/ui/lib/workspace-actions from satellite apps. That helper expects
Supabase Auth cookies on the app host and returns empty lists when only
tuturuuu_app_session is present. Instead, use
fetchSatelliteWorkspaces() from @tuturuuu/satellite/workspace-actions, which
forwards the incoming request auth to GET /api/v1/workspaces on apps/web.
When the workspace picker resolves a selected workspace, client code may call
the legacy detail route GET /api/workspaces/:wsId. That route must also opt
into app-session auth through withSessionAuth(..., { allowAppSessionAuth: CURRENT_USER_APP_SESSION_AUTH }) so satellite sessions do
not receive 401 Unauthorized on personal or other workspace aliases.
When a workspace-scoped satellite page already has an app-session user, shared
permission checks must resolve personal from that explicit user context rather
than falling back to cookie-bound Supabase Auth on the app host. App-session
cookies are Tuturuuu-managed, so the app host may not have a local Supabase Auth
user even though the user is authenticated.