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

# App Coordination

> How Tuturuuu coordinates central login and API access for internal and external apps.

Tuturuuu apps should not share production Supabase service-role keys or browser
sessions. Apps coordinate through web-owned auth routes:

* 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 `ttr` CLI 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 `appId` plus 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 in
`packages/utils/src/internal-domains.ts`, such as `cms`, `calendar`, `nova`,
`rewise`, `tasks`, `drive`, `finance`, `inventory`, `track`, `learn`, `teach`,
`chat`, `mail`, `mind`, `meet`, and `hive`.

1. The app redirects unauthenticated users to `apps/web` login with a
   `returnUrl` pointing 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 use `resolveInternalAppUrl()` for their
   `BASE_URL`/app URL constants so shared environment variables such as
   `BASE_URL=https://tuturuuu.com` are rejected for satellite hosts instead of
   minting the host-only app-session cookie on the wrong domain.
   If `apps/web` receives a registered internal app `returnUrl` that points at
   `/login` or a protected product route, it must normalize the token handoff
   back through that app's `/verify-token` route before redirecting.
2. `apps/web` validates the return target and mints the normal one-time
   cross-app handoff token.
3. The app `/verify-token` route posts only that handoff token to
   `/api/auth/verify-app-token`. Registered app proxies should perform this
   step with `consumeVerifyTokenRequest()` before page rendering; the shared
   React verifier page is only a fallback.
4. The app-local verifier validates the handoff token and sets host-only
   `tuturuuu_app_session` and `tuturuuu_app_session_refresh` cookies.
   Satellites that require rewritten `apps/web` API access must call
   `createPOST('<app>', { verificationBaseUrl: WEB_APP_URL })` so the central
   verifier also returns the Web-issued `tuturuuu_web_app_session` and
   `tuturuuu_web_app_session_refresh` cookies. The cookies are `HttpOnly`,
   `SameSite=Lax`, `Path=/`, and `Secure` in production.
5. The verifier trusts the `Set-Cookie` response and redirects to `nextUrl`.
   It must not call `supabase.auth.setSession()` and must not receive Supabase
   access or refresh tokens.

The app-session JWT reuses the app-coordination claim shape: `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 native `ttr` CLI uses the same gateway JWT model without a browser cookie:

1. `ttr login` opens `apps/web` at `/api/cli/auth/start`, or prints that URL in
   `--copy` mode.
2. The authenticated web session mints the normal short-lived cross-app handoff
   token for target app `platform` and origin `cli`.
3. The CLI posts the handoff token to `/api/cli/auth/verify`.
4. 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.
5. 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.
6. The CLI refreshes shortly before access-token expiry and retries once after a
   `401`. Refresh rotates both JWTs and updates the saved config.

CLI access JWTs include the app-session scope and can authenticate gateway API
requests. CLI refresh JWTs include only the CLI refresh scope, so they cannot be
used directly as API bearer tokens. Refresh also resolves the user through an
admin-backed lookup before minting new JWTs.

Routes used by the native CLI must require both the `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

1. Register the external app ID, allowed origins, and allowed API scopes in the
   Infrastructure dashboard.
2. Issue an app secret and store it in the external app runtime environment.
3. Send users to the centralized web login with a `returnUrl` pointing back to
   the registered external app origin.
4. The external app receives a short-lived cross-app `token` on its return URL.
5. The external app server calls
   `POST /api/v1/auth/app-token/exchange` with `appId`, `appSecret`, `token`,
   optional `requestedScopes`, and `workspaceId` whenever the requested or
   issued scopes include `external-projects:*`, `external-projects:manage`,
   `external-projects:publish`, or `external-projects:read`.
   This server-to-server route must bypass browser challenges and WAF managed
   challenges. A Cloudflare response with `cf-mitigated: challenge` is an edge
   block, not an app-token validation failure, and the external app cannot
   complete it with `fetch`.
6. 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 `MEMBER`
   of 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.
7. If the user is not yet a workspace member but has a pending
   `workspace_invites` or `workspace_email_invites` row for the requested
   workspace, the exchange returns `403` with
   `code: "PENDING_WORKSPACE_INVITE"`, `workspaceId`, and `invitationUrl`.
   External apps must handle this code before showing generic no-access copy so
   the user can accept or reject the invitation in Tuturuuu first.
8. Tuturuuu validates the app secret and cross-app token, then returns a
   short-lived bearer token for Tuturuuu APIs plus the normalized authorized
   `workspaceId`.

External apps should store only their own app secret and local session material.
They should send Tuturuuu API calls with the returned bearer token and should
never require production Supabase keys. Third-party or custom Supabase JWT paths
are not used for registered internal apps; Tuturuuu JWTs stay at the gateway and
internal API layer. External-project admin apps must store the returned
authorized workspace ID in their encrypted local admin session and reject the
session if it no longer matches the app's configured linked workspace. They
should also revalidate stored local sessions against a protected
external-project API route before rendering admin UI or proxying admin API
calls; if Tuturuuu returns `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 with
`TUTURUUU_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.

The policy also supports per-internal-app overrides keyed by registered app id
for access TTL, refresh TTL, and refresh-early window. If the Infrastructure
secret is missing or invalid, runtime code falls back to compatibility
environment variables where available and then to defaults. Existing short-lived
tokens are not revoked when the policy changes; new cross-app handoffs and
refresh rotations pick up the latest cached policy after the cache window.
Refresh tokens remain HttpOnly browser cookies or CLI-local tokens. They are not
API credentials, are not exposed to browser JavaScript, and should never be sent
as bearer access tokens.

When registered internal apps are deployed separately, every deployment that
mints or verifies `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.
