Skip to main content

Channel authentication cutover runbook

This runbook moves an environment from API-issued Telegram sessions to Supabase sessions issued through the API's channel authentication. Accounts from the API-issued Telegram sessions are not carried over. Every person signs in again and gets a new Supabase user, professional user and client account.

Run it first in a test environment, then in production. Production rollout is a separate, scheduled operational action.

:::warning Not yet rehearsed No test-environment rehearsal of this runbook has been recorded, and hosted Supabase acceptance has not been run. Treat the steps as the planned procedure and record the rehearsal's results before using it in production. :::

What changes at once​

The API release that ships with this change replaces the old Telegram exchange, bot webhook and session secrets with channel authentication, and rejects every API-issued Mini App token. So these switch together, in one window:

  • the API release;
  • both Mini App releases, which sign in through the new /auth/* routes;
  • both bots' webhooks, which move to /webhooks/telegram/:app.

Supabase Auth fetches the issuer's discovery document when the provider is registered, so the provider can be registered only once the API release serves it, at the start of the window.

Create or update the provider locally​

After applying the database migrations, start the API and its public HTTPS tunnel. The tunnel must forward to the API (development port 3000). The API serves the issuer itself; no separate issuer process is needed.

Set these values in apps/api/.env or the shell environment:

VariableValue
SUPABASE_URLSupabase gateway origin, usually http://127.0.0.1:54321 locally
CHANNEL_AUTH_ISSUER_URLCurrent public HTTPS origin of the API tunnel
CHANNEL_AUTH_SUPABASE_PROVIDERStable provider identifier, for example custom:bookygram-channels
CHANNEL_AUTH_ASSERTION_AUDIENCEProvider client ID, for example bookygram-channels
SUPABASE_SECRET_KEYSupabase secret key or legacy service_role JWT for this instance; needed only by this administrative command

Keep an existing provider identifier and audience if already configured. The normal API also needs its publishable key, bot bindings and assertion signing key as described in Environment variables. The setup command does not need or generate the assertion signing key.

Run from the repository root:

npm run auth:provider --workspace api

This command reads apps/api/.env when present; exported shell variables take precedence. Keep the administrative key out of frontend variables. It is not needed by the running API.

Expected output includes "outcome":"created", "updated", or "unchanged". The command creates the provider if missing, or updates its issuer, discovery URL, audience, scopes, enabled state, email-optional setting and nonce checking in place. It verifies the saved settings before reporting success. It never deletes a provider or rotates an existing provider secret, and does not print credentials or remote response bodies.

Supabase Auth fetches discovery when creating or updating the provider. The API must already advertise the same issuer URL, and Supabase must be able to reach that HTTPS URL. Discovery and JWKS must return JSON, not a frontend page or tunnel interstitial. A 400 during registration can mean that discovery is unreachable or invalid; 401/403 indicates an administrative access problem. A generic 404 is not treated as a missing provider.

After a database reset, run the command again. When the API tunnel changes, update CHANNEL_AUTH_ISSUER_URL, restart the API, update both Mini Apps' VITE_API_URL, and rerun the command. Update bot webhook URLs too. Keep the provider identifier stable. Finally, fully close and reopen the Mini App for fresh Telegram launch data; a previously consumed launch cannot be reused in a new exchange context.

Provider setup uses the Supabase Auth administrative API rather than SQL in auth tables: Auth owns discovery validation, client-secret encryption and issuer-cache invalidation. Database migrations alone do not register this environment-specific provider.

Prerequisites​

  • Hosted compatibility accepted for the target Supabase project; see Channel auth compatibility. Do not continue without it.
  • A database with no client accounts from the API-issued Telegram sessions. The client_accounts.supabase_user_id migration requires a Supabase subject on every row and fails while any such account remains. Professional users rows keyed telegram:<id> do not block it, but nobody can sign in to them.
  • An assertion key, generated as in Rotate the channel assertion key step 1.
  • Disposable Telegram test accounts for verification.

Phase 1: prepare (no user impact)​

  1. Prepare the API release configuration with every variable in Environment variables, including each bot's new webhookSecret. CHANNEL_AUTH_ISSUER_URL is the API's public origin. Remove TELEGRAM_BOT_TOKEN, PROFESSIONAL_SESSION_SECRET, TELEGRAM_INIT_DATA_MAX_AGE_SECONDS, CLIENT_BOT_TOKEN, CLIENT_SESSION_SECRET and CLIENT_BOT_WEBHOOK_SECRET; the new release ignores them, and keeping them only widens exposure.
  2. Prepare the provider registration: the provider identifier (CHANNEL_AUTH_SUPABASE_PROVIDER), the API's issuer, the assertion audience as client ID, email-optional registration and nonce checking enabled, as proven by the compatibility harness. Do not register it yet; Supabase Auth rejects a registration whose issuer does not serve discovery.

Phase 2: cutover window​

Announce a short window. Open Mini Apps lose their session at step 1 and must be reopened.

  1. Apply the database migrations, including the channel_auth schema, as in Database migrations, then deploy the API release. The previous release cannot create client accounts once the migrations are applied, so do both in one step. API-issued Mini App sessions stop working immediately: the API verifies only Supabase access tokens. Nothing else is needed to invalidate them.
  2. Register the provider in Supabase Auth with the values prepared in phase 1, using npm run auth:provider --workspace api against that environment. Expected: GET <api-origin>/.well-known/openid-configuration answers 200, and a lookup of the provider returns it. Sign-ins fail until this step is done.
  3. Move both webhooks, as in Move Telegram webhooks to channel authentication. Do this right after step 2. Updates that failed against the old route are retried by Telegram and delivered to the new route after the switch (Telegram keeps retrying for up to a day).
  4. Deploy both Mini App releases.
  5. Drain webhook retries. Check both bots with getWebhookInfo until pending_update_count stays near 0, then check for pending facts as in Troubleshoot contact onboarding. Expected: no pending facts for signed-in people older than a few minutes.

Phase 3: verify​

With disposable test accounts:

  • A new user signs in through each bot without sharing a phone.
  • The same Telegram user in both apps gets one Supabase user and keeps both roles; a client-only user is denied professional routes.
  • A client shares contact and the account becomes ready.
  • Leaving an app open past an hour keeps it working (refresh).
  • Declining to share contact leaves the account contact_required with a usable session.
  • Opening either Mini App in a browser shows sample data and creates no session.
  • Website sign-in works as before.

Rollback​

  • Before phase 2: nothing has changed for users.
  • From phase 2 step 1: fix forward. The database migrations are forward only, and the previous API release cannot create client accounts on the migrated schema. The API release and both Mini Apps can each be redeployed on their own.