Skip to main content
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.