Federated sign-in (Google): rollout and operations
This is the operator runbook for phase 1a of PLT-1520: people who already have a Constellation login can also sign in with Google. Sign-in stays invite-only: Supabase sign-ups are off, so a Google sign-in can only link to an account that already exists. The rollout is tracked as INF-659, and its evidence is recorded there as comments.
The design is in SPEC-plt-1520-federated-sign-in.md: D3a for the settings, D8 for the custom domain, D10 for the accepted residuals, D11 for operations. Where this page and the spec disagree, the spec wins; fix this page.
Follow the rollout sections in order: 1, then 2 to 6 on staging, then 7 to 10 on production. Sections 11 to 16 are standing procedures.
One step is tied to a release, not to this rollout. Before PT-1147 part 2 reaches an environment, link every PENDING partner-admin invitation there (section 16). PT-1147 part 2 is the registration rule that reuses an identity row only when the invitation names it. For staging, that means before it merges to develop; for production, before the release that carries it. Section 2 step 1 and section 7 step 1 check that it was done.
1. Owners, environments and secrets
Gregor Siwinski performs every console step on this page. The coordinating agent session records the evidence on INF-659. Store every secret in the operator vault, never in a repository, a PT comment or a chat.
| Item | Where it lives | Owner | Second owner |
|---|---|---|---|
| Google Cloud project for sign-in (consent screen + clients) | Google Cloud console | Gregor Siwinski | TODO: second owner not decided |
| Google OAuth client secret, staging | Supabase staging, Google provider; operator vault | Gregor Siwinski | TODO: second owner not decided |
| Google OAuth client secret, production | Supabase production, Google provider; operator vault | Gregor Siwinski | TODO: second owner not decided |
| Supabase Auth settings, both projects | Supabase dashboard | Gregor Siwinski | TODO: second owner not decided |
| Supabase custom-domain add-on (billing) | Supabase production, Project Settings → Add-ons | Gregor Siwinski | TODO: second owner not decided |
planetb2b.com DNS records for login | AWS Route 53 | Gregor Siwinski | TODO: second owner not decided |
| Vercel environment variables (none of them secret) | Vercel team planet-b2-b | Gregor Siwinski | TODO: second owner not decided |
| Apple sign-in key and client secret (phase 3) | Not created yet. The client secret expires after at most 6 months. | TODO | TODO |
| Microsoft Entra client secret (phase 3) | Not created yet. Entra caps a client secret at 24 months. | TODO | TODO |
The Apple and Entra rows are placeholders for phase 3 (spec D10). Whoever creates either secret adds its expiry date to this table, and a reminder well before it, in the same change.
| Environment | Supabase project ref | Shell origin (Directory) | Auth origin |
|---|---|---|---|
| Staging | ewsinatfnuzmnfazivgm | https://constellation-dev.planetb2b.com | https://ewsinatfnuzmnfazivgm.supabase.co (no custom domain) |
| Production | dulaxksjkqtxidehqoaz | https://constellation.planetb2b.com | https://login.planetb2b.com from the go-live release (sections 8 and 10) |
Project Tracker (/projects) and Catalog (/catalog) are served on the shell origin through Directory's rewrites (Deployment). Every sign-in this page enables starts and ends on the shell origin. A zone's own host, such as projects-dev.planetb2b.com or a *.vercel.app alias, is not allow-listed, so a Google sign-in started there lands on the Site URL instead. That is expected.
Reading the settings: scripts/check-federated-auth-settings.ts
Run this read-only check before and after every change in sections 3 to 10, and paste its output into the INF-659 comment for that step. It reads <auth origin>/auth/v1/settings with the project's publishable key, which it takes only from an environment variable. It refuses a key on the command line, and it refuses a secret or service-role key.
cd "$(git rev-parse --show-toplevel)"
printf 'Publishable key: ' && read -rs SUPABASE_PUBLISHABLE_KEY && echo
export SUPABASE_PUBLISHABLE_KEY
npx tsx scripts/check-federated-auth-settings.ts --project-ref ewsinatfnuzmnfazivgm
npx tsx scripts/check-federated-auth-settings.ts --project-ref ewsinatfnuzmnfazivgm --expect-provider google
npx tsx scripts/check-federated-auth-settings.ts --auth-url https://login.planetb2b.com --expect-provider google
unset SUPABASE_PUBLISHABLE_KEY
The publishable key is under Project Settings → API Keys. It is not a secret, because every browser receives it, but the habit costs nothing.
| Line | Meaning |
|---|---|
| PASS / FAIL | disable_signup must be true, mailer_autoconfirm must be false ("Confirm email" on), external.email must be true (passwords and recovery use it), external.anonymous_users must be false, and saml_enabled must be false (SAML single sign-on, which the endpoint reports outside external). |
| PASS / FAIL | Each --expect-provider must be on, and every other social provider must be off. Without the flag, Google must be off, which is how sections 3 and 13 are confirmed. |
| NOT CHECKED | "Secure email change" and "Allow manual linking" are not in the public settings, so read them in the dashboard. The script prints these lines on every run, so a green result is never taken to cover them. |
Exit codes: 0 means every checked setting passed, 1 means a setting failed, and 2 means the check itself could not run (bad arguments, no usable key, or the endpoint did not answer 200).
Baseline, 2026-10-21: both projects reported disable_signup: false, meaning sign-ups were on, with mailer_autoconfirm: false and email as the only provider. The first run therefore fails on disable_signup, and section 3 fixes it.
Making a Vercel variable change live
Sections 6, 8, 10 and 13 change a NEXT_PUBLIC_* variable, which is inlined at build time. A change is live only once a Git-integration build of the branch has finished: develop for staging (Preview, scoped to develop), main for production. Two other routes look as if they work and do not:
- A redeploy from the dashboard or the API re-runs the app's
ignoreCommand(turbo-ignore) against the same commit, finds nothing changed, and is cancelled.constellation-directoryandconstellation-catalogboth have one.constellation-platform(Project Tracker) has none, so it is the only one of the three that rebuilds this way. - A CLI deploy builds, but receives only the generic Preview values, not the ones scoped to
develop.
So a change reaches the branch through its next merge, which turbo-ignore builds only if the merge touches the app. Put [vercel deploy] in the PR title so the merge commit carries it, and every zone builds. On develop that can be any PR that merges after the change. On main it is a release or hotfix PR: section 10's go-live release carries both production variables. Until the build finishes, the old deployment keeps serving.
Proof is two readings together (.ai/lessons/platform-build-and-deploy.md, 2026-10-22): the project's Git-integration deployment of the branch head shows READY (not CANCELED), and one request on the live origin shows the new behaviour, as each section names. Neither reading alone counts: a cancelled deployment's URL can still answer 200 from the previous build.
Staging builds on merge again. The INF-847 hold that kept Directory, Catalog, Wiki and Platform off automatic develop deployments was lifted on 2026-11-04 (Staging refresh). A merge to develop rebuilds them, [vercel deploy] included; a CLI deploy would still miss the values scoped to develop.
2. Staging pre-flight
- Partner-admin invitations. Confirm that every PENDING partner-admin invitation on staging was linked before PT-1147 part 2 reached staging (section 16). If it has not merged yet, link them now. If it has, and one is unlinked, link it now and record on INF-659 that it was late.
- Staging builds again. Confirm that the four zones' newest
developdeployments areREADYat thedevelophead that carries the phase 1a code (Staging refresh), and record their SHAs on INF-659. - Confirm that the phase 1a code is on
developand deployed to staging: PLT-1521, PLT-1522, PLT-1523, PLT-1524, DIR-146, PT-1146, CAT-92 and PT-1147 (both parts). The staging checks in section 6 name what each one needs. - Confirm that the staging shell serves both sub-zones.
https://constellation-dev.planetb2b.com/projects/must render Project Tracker, andhttps://constellation-dev.planetb2b.com/catalog/loginmust render Catalog's sign-in page. Behind the shell, each sub-zone must be built with its base path. If either page does not render, check two sets of Preview values scoped todevelop: Directory'sPROJECTS_ZONE_URLandCATALOG_ZONE_URLmust point at the sub-zones'developdeployments, and each sub-zone'sNEXT_PUBLIC_BASE_PATHmust be/projectsor/catalog. Other previews stay standalone and leave the base path unset, as Deployment describes. The sub-zone rows in section 6 cannot run until this passes. - Run the settings check against staging, and record the output on INF-659 as the before state.
3. Supabase Auth settings (staging now, production in section 7)
In the Supabase dashboard for the project, under Authentication → Sign In / Providers:
| Setting | Required value | Why |
|---|---|---|
| Allow new users to sign up | Off | Invite-only by construction (I1). With it off, Supabase creates no account for a provider sign-in; it only links to one that exists. |
| Confirm email | On | With it off, Supabase treats any provider email as verified when it links an identity. |
| Secure email change (Email) | On | An email change needs both mailboxes. |
| Allow manual linking | Off | Users cannot link or unlink identities themselves. Section 14 is the operator path. |
| Allow anonymous sign-ins | Off | An anonymous session is an account no invitation created. |
Turn sign-ups off first, after one check. Until PT-1147 part 2 is deployed in an environment, Project Tracker's registration (apps/project-tracker/src/app/api/auth/register/route.ts) uses the admin API, which ignores the setting, only when its admin client exists. That means NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are both set on constellation-platform for that environment. Without them it falls back to the public signUp(), which sign-ups-off refuses, and invitation registration breaks with signup_disabled. So confirm both variables exist there, by name only, before turning sign-ups off. PT-1147 part 2 removes the fallback. No other public sign-up, one-time-code or anonymous call exists in apps/ or packages/, as INF-659's comment of 2026-10-21 records. With that check done, production's sign-up switch may also go off early, on its own, before section 7, and section 7 then confirms it with the settings check.
Then run the settings check again. Without --expect-provider every checked line must pass, and you read the two NOT CHECKED settings in the dashboard. Record both on INF-659.
4. Google Cloud: consent screen and one client per environment
Work in the Google Cloud project named in section 1, under Google Auth Platform.
- Branding. The app name is
Constellation. Set the user support email and the developer contact, the home pagehttps://constellation.planetb2b.com, and the privacy-policy and terms links. Addplanetb2b.comas an authorized domain. - Submit brand verification now. It takes days, and an unverified consent screen shows no logo.
- TODO (open): Google may ask to verify every domain the clients' redirect URIs use, which includes
supabase.co. If it does, the fallback is to remove the production client'sdulaxksjkqtxidehqoaz.supabase.coURI once section 8 has activated the custom domain, after which nothing uses it, and to move the staging client into a Google Cloud project of its own. Record what Google asked for on INF-659.
- TODO (open): Google may ask to verify every domain the clients' redirect URIs use, which includes
- Audience. Choose External, because partners sign in with their own Google accounts, and publish the app ("In production"). In Testing mode, only listed test users can sign in.
- Data access. Scopes
openid,.../auth/userinfo.emailand.../auth/userinfo.profileonly. Supabase needs nothing more, and any sensitive scope triggers a longer verification. - Clients. Create two clients of type Web application, one per environment. Never share one between environments.
| Client | Authorized JavaScript origins | Authorized redirect URIs |
|---|---|---|
Constellation staging | https://constellation-dev.planetb2b.com | https://ewsinatfnuzmnfazivgm.supabase.co/auth/v1/callback |
Constellation production | https://constellation.planetb2b.com | https://dulaxksjkqtxidehqoaz.supabase.co/auth/v1/callback and https://login.planetb2b.com/auth/v1/callback |
Two kinds of callback must not be confused. Google's redirect URIs are Supabase's callbacks (…/auth/v1/callback), and only Supabase's. The applications' own /auth/callback routes go in Supabase's redirect allow-list (section 5), never in Google.
Register the production client's login.planetb2b.com URI now, before the custom domain exists. Once section 8 activates the domain, Supabase sends Google that callback and no other, so a URI added later breaks production sign-in in between.
Copy each client's ID and secret straight into the operator vault.
5. Supabase: Google provider and redirect allow-list (staging)
-
Under Authentication → Sign In / Providers → Google, turn on "Enable Sign in with Google". Paste the staging client ID and secret. Leave "Skip nonce checks" off, and leave "Allow users without an email" off if it is shown. Save.
-
Under Authentication → URL Configuration, record the current Site URL and Redirect URLs on INF-659 before changing anything. Existing entries serve Project Tracker's password-reset emails; keep every one of them.
-
Site URL. It should be the shell origin. If it is not, do not change it as part of this rollout, because Supabase's email templates are built from it. The entries below admit the callbacks without it.
-
Redirect URLs. Add exactly these three:
https://constellation-dev.planetb2b.com/auth/callback**https://constellation-dev.planetb2b.com/projects/auth/callback**https://constellation-dev.planetb2b.com/catalog/auth/callback** -
Run the settings check with
--expect-provider google. Every checked line must pass.
Why each entry ends in a double asterisk. Every zone sends Supabase a callback carrying a query string, such as /catalog/auth/callback?next=%2Ftaxonomy&provider=google. Supabase matches each entry against the whole redirect URL, query string included, and drops only the fragment. This is IsRedirectURLValid in Supabase Auth's source; see also Supabase's redirect URL guide. In its globs a single asterisk stops at . and /, while a double asterisk matches any characters. An entry for the exact path therefore never matches, and Supabase silently sends the person to the Site URL instead. A redirect on the Site URL's own scheme, host and port is admitted without any entry.
Previews. Add no *.vercel.app pattern. A pattern such as https://*-planet-b2-b.vercel.app/** would also match a project another Vercel account names something-planet-b2-b, and staging's checks run on constellation-dev.planetb2b.com, so they do not need one. Add a preview pattern only after verifying that no other Vercel account can claim a host it matches, and record how that was verified on INF-659. Production never gets one: it lists exact production origins only.
6. Enable the buttons on staging, then run the staging checks
- In each of the Vercel projects
constellation-directory,constellation-platformandconstellation-catalog, addNEXT_PUBLIC_AUTH_FEDERATED_PROVIDERS=googleto Preview, scoped to thedevelopbranch. Scoped to every Preview branch, each PR preview would show a Google button that cannot work, because its host is not allow-listed. - Make it live through the next merge to
developafter the staging restoration (section 2 step 2), with[vercel deploy]in that PR's title (Making a Vercel variable change live). A dashboard redeploy is not enough. Then take both readings: each project's Git-integration deployment of thedevelophead isREADY, and the Google button appears onhttps://constellation-dev.planetb2b.com/login, on Project Tracker's sign-in page under/projects/, and on/catalog/login. Record both on INF-659. - Run every check below on staging, fill in the Result column, and post the completed table to INF-659. The test accounts need Google accounts that Google is authoritative for: Gmail addresses, or Google Workspace accounts (spec D10).
| # | Check | Expected | Needs on staging | Result |
|---|---|---|---|---|
| b | An existing password user signs in with Google for the same address from https://constellation-dev.planetb2b.com/login. | Signed in as the same Supabase user id, with the same identity row, memberships and roles in Directory. Supabase shows a google identity on that user. The audit log has AUTH_LOGIN (provider google) and one AUTH_IDENTITY_LINKED. | DIR-146 | |
| c | A Google account whose address has no Supabase account signs in. | Refused, and the sign-in page says the person is not invited (not_invited, from Supabase's signup_disabled). Supabase → Authentication → Users has no account for that address. | DIR-146 | |
| e | For an address with no account, request a magic link or one-time code from Supabase directly (POST <auth origin>/auth/v1/otp with {"email": "<address>", "create_user": true} and the publishable key), and try POST <auth origin>/auth/v1/signup, with and without an email. Try every other method the settings check lists as enabled. | Each is refused, and Supabase → Authentication → Users has no account for that address. Record the error code each returned. | section 3 only | |
| f | Ban the check (b) user (section 12, step 1), then sign in with Google. | Refused, with no session. Lift the ban afterwards, and confirm the next Google sign-in works. | DIR-146 | |
| g | Create an unconfirmed user with a password (Authentication → Users → Add user, with auto-confirm off), then sign in with Google for that address. | Record what Supabase did to the unconfirmed email identity and its password (expected: removed). Then try the old password, which is expected to be refused. Delete the test user afterwards. | DIR-146 | |
| k | An invitee registers through a Project Tracker invitation, opens the emailed password link on another device, chooses a password there, and later signs in with Google. | The link works on the other device, the password works, and Google links to the same account (the check (b) evidence). | PT-1147 part 2, DIR-146 | |
| l | An invitee registers, and the emailed password link is never used. | No password signs in to that account: it has none. | PT-1147 part 2 | |
| m | Signed out, open https://constellation-dev.planetb2b.com/catalog/login?redirect=/taxonomy and sign in with Google (CAT-92). | The final URL is https://constellation-dev.planetb2b.com/catalog/taxonomy, signed in. It is not catalog-dev.planetb2b.com or a *.vercel.app host. | CAT-92, section 2 step 4 | |
| n | Signed out, open a Project Tracker deep link on the shell, such as https://constellation-dev.planetb2b.com/projects/tasks/INF-659, and sign in with Google from the sign-in page it leads to (PT-1146). | The final URL is on https://constellation-dev.planetb2b.com under /projects/, signed in. It is not projects-dev.planetb2b.com or a *.vercel.app host. | PT-1146, section 2 step 4 | |
| r | Signed out, request a password reset for the check (b) user at https://constellation-dev.planetb2b.com/projects/forgot-password, and follow the emailed link. | The link leads to a page where a new password can be chosen, and the new password signs in. Record the URL the link returned through, which section 11 relies on. | PT-1483 |
Check r cannot pass until PT-1483 is deployed to staging. Until then, Project Tracker's reset link leaves out /projects and lands on Directory's /auth/callback, as Deployment records. Do not work around the gap by adding /projects to NEXT_PUBLIC_SITE_URL. Gregor accepted a go-live exception for it on 2026-10-29 (section 11), so record check r as "exception: PT-1483" and run it once PT-1483 is on develop.
Checks m and n exist because the shared callback redirects to the origin of the request it receives, and the sub-zones are reached through Directory's rewrite. INF-659's comment of 2026-10-21 from CAT-92 explains this. A final URL on a zone's own host is a failure even when the person is signed in.
Production goes ahead only when every row passes, or Gregor has accepted a recorded exception on INF-659.
7. Production: settings and the Google client
Production follows the order Gregor set on 2026-10-29. Sections 7 to 9 run before the go-live release, the release that brings the last item of section 2 step 3 to production, and none of them turns Google sign-in on: the custom domain is activated and the auth URL is set, but no production build carries it yet, and the Google provider stays off. Section 10 sets the buttons' variable just before its release, which then builds both variables at once, and the provider goes on right after it deploys, so no Google sign-in reaches production code that lacks the phase 1a checks.
- Partner-admin invitations. Link every PENDING partner-admin invitation in production before the release that carries PT-1147 part 2 (section 16). If an earlier release already carried it, confirm each was linked before that release, link any that was not, and record on INF-659 that it was late.
- Run the settings check against
dulaxksjkqtxidehqoaz, apply section 3 to production (or confirm it was already applied early), then run the check again, without--expect-provider. Record both runs. - Confirm that the production client from section 4 lists both redirect URIs.
8. Production: login.planetb2b.com (required before go-live)
Gregor decided on 2026-10-21 that production goes live on the custom domain. Without it, Google's consent screen shows dulaxksjkqtxidehqoaz.supabase.co. Staging has no custom domain.
-
Add-on. The Supabase organisation is on the Pro plan through the Vercel Marketplace. Enable Project Settings → Add-ons → Custom domain on the production project. It is a paid add-on.
-
Create the domain for
login.planetb2b.com, either from that page or withsupabase domains create --project-ref dulaxksjkqtxidehqoaz --custom-hostname login.planetb2b.com. Note the TXT record name and value it prints. -
DNS in AWS Route 53, zone
planetb2b.com. Use a low TTL while you set this up.Name Type Value login.planetb2b.comCNAME dulaxksjkqtxidehqoaz.supabase.co.the name step 2 printed (normally _acme-challenge.login.planetb2b.com)TXT the value step 2 printed, without surrounding whitespace -
Verify. Run
supabase domains reverify --project-ref dulaxksjkqtxidehqoazuntil it reports the domain verified. It can take several attempts while DNS propagates. -
Before activating, confirm once more that Google's production client lists
https://login.planetb2b.com/auth/v1/callback. From activation on, Supabase sends Google that callback and no other. -
Activate with
supabase domains activate --project-ref dulaxksjkqtxidehqoaz. The certificate can take up to 30 minutes. -
Check both origins.
--auth-url https://login.planetb2b.comand--project-ref dulaxksjkqtxidehqoazmust both answer and pass, because the default domain keeps serving. -
Point the zones at it, for the release. In
constellation-directory,constellation-platformandconstellation-catalog, setNEXT_PUBLIC_SUPABASE_AUTH_URL=https://login.planetb2b.comfor Production. It goes live with the go-live release's build (section 10 step 3), not with a redeploy (Making a Vercel variable change live). A build ofmainbefore then, a hotfix for instance, carries it too, which is safe once step 7 has passed. LeaveNEXT_PUBLIC_SUPABASE_URLonhttps://dulaxksjkqtxidehqoaz.supabase.co. The session cookie's name is derived from it, so that is what keeps everyone signed in (spec D8). Section 10 step 4 confirms both.
9. Production: redirect allow-list
Do section 5 steps 2 to 4 on production, with these three entries only. Production lists exact production origins and no pattern:
https://constellation.planetb2b.com/auth/callback**
https://constellation.planetb2b.com/projects/auth/callback**
https://constellation.planetb2b.com/catalog/auth/callback**
Record the Site URL and Redirect URLs before changing anything, and keep the existing entries. Leave the Google provider off: section 10 step 5 turns it on, after the release. Then run the settings check with --auth-url https://login.planetb2b.com and no --expect-provider. It must pass, which shows Google is still off.
10. Production go-live: the release, then the provider
- Set the buttons' variable, last. Shortly before the go-live release merges, add
NEXT_PUBLIC_AUTH_FEDERATED_PROVIDERS=googleto Production in the three Vercel projects. Like the auth URL, it goes live with the release's build (Making a Vercel variable change live). A build ofmainbefore then, a hotfix for instance, would show the buttons early, failing as step 3 describes, which is why this comes last. - Sign in first. Sign in to production with a password, and keep that browser open for step 4.
- Merge the go-live release with
[vercel deploy]in its title, so that every zone builds both variables. Once it is deployed, every item in section 2 step 3 is in production. PT-1483 is not one of them: under Gregor's go-live exception (section 11) Google ships first, and PT-1483 follows separately. Staging passing proves nothing about production, becausedevelopreachesmainonly through a release. Its migrations are applied to the production database before it merges, and itsVerify Release Migrationscheck is green. Then take both readings: each project's Git-integration deployment of themainhead isREADY, and the Google button appears onhttps://constellation.planetb2b.com/login, under/projects/and on/catalog/login. Until step 5 the buttons fail closed: the provider is still off, so Supabase refuses the sign-in and issues no session. Do steps 4 and 5 straight after, to keep that window short. - Confirm the auth URL, then that nobody was signed out. A page load does not reliably call Supabase Auth from the browser: the app reads a live session from its cookie and checks it on the server. So make one call happen. In a second browser or a private window, open the developer tools and turn on the Network panel's keep-on-navigation option: Preserve log in Chrome, Persist Logs in Firefox. A successful sign-in navigates away, and browsers clear the Network panel on navigation by default. Then sign in to production with a password. The sign-in's request to
/auth/v1/tokenmust go tohttps://login.planetb2b.com, not todulaxksjkqtxidehqoaz.supabase.co. Then reload the browser from step 2: it is still signed in, and its cookie is still namedsb-dulaxksjkqtxidehqoaz-auth-token(or its.0,.1chunks). An old build passes the cookie check just as well, so the cookie check alone proves nothing. Record both, with step 3's readings, on INF-659: they are one half of INF-659's acceptance criterion 2. - Turn the provider on, right away. Do section 5 step 1 on production, with the production client. Then run the settings check with
--auth-url https://login.planetb2b.com --expect-provider google: every checked line must pass. - Sign in with Google at
https://constellation.planetb2b.com/loginwith an account that already has a login. Google's consent screen must namelogin.planetb2b.com, notsupabase.co. Record that on INF-659; it is the other half of acceptance criterion 2. - Repeat checks b, c, m and n of section 6 on the production origin, with test accounts only.
- INF-659 closes only after this section. Its rollout PRs reference it without closing it.
11. Recovery: password reset
A person who loses access to their Google account, or never set a password, recovers through their mailbox.
Prerequisite: PT-1483. Until it ships, Project Tracker's reset link leaves out
/projectsand lands on Directory's/auth/callback, so these steps do not complete on the shell, on staging or production (Deployment). Staging check r confirms the path once PT-1483 is there.Go-live exception (Gregor, 2026-10-29): "Exception; ship Google first". Under section 6's rule, Google sign-in goes live without PT-1483, so until PT-1483 is deployed there is no self-service password recovery on the shell. Record the exception on INF-659 before section 10, and record check r's result as "exception: PT-1483".
- They open Project Tracker's forgot-password page,
https://constellation.planetb2b.com/projects/forgot-password, and enter their address. - Supabase emails a recovery link, which brings them back to Project Tracker's reset-password page, where they choose a password. Staging check r records the exact path the link takes.
- They sign in with it. Their Google identity, if any, stays linked.
This works whether or not the account already had a password. A person who has lost the mailbox as well cannot recover this way, by design: that is an administrator's decision, made through Directory.
12. Offboarding
Banning the Supabase user ends sign-in by any method, Google included, and stops the account refreshing a token (spec D11). Revoking the person's memberships ends their API keys and bearer tokens, because their liveness reads the identity row and its memberships. Ban first, because it takes effect at once.
Waiting for DIR-188 (Gregor, 2026-10-31). The audited listing of a person's memberships across the tenants you name, and ending their sessions, wait for DIR-188's operator tool. This runbook gives no hand-run SQL for either. Until DIR-188 ships, escalate to Gregor for both, and never run SQL by hand for them.
TODO (INF-659): rehearse this procedure on staging before its first production use, and record the rehearsal on INF-659. It has not been run yet.
-
Ban the Supabase user. Under Authentication → Users, search by address, open the account, check that the user id is the person's, ban them, and choose the longest duration offered. The ban sets the user's
banned_until, which the user panel shows. Record the user id and the time on the offboarding ticket. -
Escalate the memberships to Gregor. Write on the offboarding ticket the user id from step 1 and every tenant to check: the person's home tenant, and each tenant the offboarding request names or the person is known to work in. Gregor decided on 2026-10-29 to "scope to named tenants". Gregor returns the memberships to revoke.
-
Revoke each one, through Directory. For each membership, an admin of that membership's tenant sets it to
REVOKED. Directory has no control for this on the user page, so it is an API call, which Directory audits. With an API key that acts in more than one organisation,x-act-as-orgnames the tenant:printf 'Directory API key: ' && read -rs DIRECTORY_API_KEY && echoprintf 'header = "Authorization: Bearer %s"\n' "$DIRECTORY_API_KEY" | \curl -sS -K - -X PUT -H 'Content-Type: application/json' \-H 'x-act-as-org: <tenant id>' \-d '{"status":"REVOKED"}' \"https://constellation.planetb2b.com/api/v1/members/<membership id>"unset DIRECTORY_API_KEYA tenant with no admin able to do this is escalated to Gregor. Do not update
identity.user_tenant_membershipsin SQL, because that write would bypass Directory's audit and its identity write boundary.The key goes through
curl -K -on standard input, andprintfis a shell builtin, so it never appears in a process's arguments. Tenant authority is checked on every request, so a revoked membership is refused on the next one, whatever an issued token still claims. To stop the user in their home tenant as well, use "Suspend User" on that page.
Residuals, until DIR-188:
- An access token the person already holds stays valid until it expires: up to the project's access-token lifetime, which is one hour unless someone changed it (the JWT expiry in its Auth settings). The ban stops refreshing it, but nothing here revokes it.
- An API key or bearer token for a membership that is not yet revoked stays live until that membership is revoked. This includes a membership in a tenant nobody named, which is the gap Directory already has, since no tenant admin sees other tenants' memberships. Name every tenant you know of.
Re-inviting someone who was banned: lift the ban first (the same panel, or the admin API's ban_duration: "none"), then invite or reactivate them. A banned account cannot complete an invitation's sign-in, which looks like a broken invitation.
13. Rollback
Google sign-in. The order matters: the provider switch takes effect immediately, while the environment variable needs a build.
- Turn Google off in Supabase (Authentication → Sign In / Providers → Google) in the affected project. New Google sign-ins stop at once.
- Run the settings check without
--expect-provider. It must pass, which shows Google is off. - Remove
NEXT_PUBLIC_AUTH_FEDERATED_PROVIDERSfrom the affected environment in all three Vercel projects. The buttons disappear with the next Git-integration build of the branch: a merge with[vercel deploy]in its PR title on staging, a release or hotfix PR for production (Making a Vercel variable change live). Until then a button may still show, and step 1 already makes it fail. Confirm with both readings: the deployment of the branch head isREADY, and the button is gone.
Sessions issued earlier stay valid until they expire. To end one person's access now, use section 12. Sign-ups stay off: rolling back Google does not reverse section 3.
Custom domain. Remove NEXT_PUBLIC_SUPABASE_AUTH_URL from Production in the three projects. It takes effect with the next build of main, as in section 8 step 8, and the proof is the reverse of section 10 step 4: Auth requests go to dulaxksjkqtxidehqoaz.supabase.co again. If that cannot wait for a build, Vercel's Instant Rollback to each project's production deployment from before the go-live release restores the old value, together with that deployment's code, so it also takes back everything that release brought. Nobody is signed out, because the cookie name never depended on it (spec D8). The domain itself keeps serving Supabase, and Supabase keeps sending Google the login.planetb2b.com callback while the domain is active, so keep that redirect URI registered. If the production client's dulaxksjkqtxidehqoaz.supabase.co redirect URI was removed (the section 4 fallback), register it with Google again before deleting the domain, because once the domain is gone Supabase sends Google that callback again. Delete the domain (supabase domains delete --project-ref dulaxksjkqtxidehqoaz) only to abandon it. Then remove the Route 53 records, and last of all the Google redirect URI.
14. Removing a stale provider identity (IDENTITY_EMAIL_MISMATCH)
The middleware refuses every session of an account that has a provider identity whose email is missing or differs from the account's email, compared case-insensitively. APIs answer 401 IDENTITY_EMAIL_MISMATCH, and pages send the person to sign-in with that code (spec D4). It happens after an account's email changes while a Google identity linked for the old address remains, and that identity may belong to whoever controls the old address now. Users cannot unlink it, because manual linking is off, and Supabase's admin API has no call to remove an identity.
Waiting for DIR-188 (Gregor, 2026-10-31). Removing the identity, and ending the account's sessions before it, wait for DIR-188's audited operator tool. This runbook gives no hand-run SQL for either. Until DIR-188 ships, escalate to Gregor, and never remove an identity or end sessions in SQL by hand. While the identity remains, the mismatch check keeps refusing every session of the account, so the holder of the old address gets no access through it; the account's owner is locked out until the identity is removed.
-
Inspect (read-only):
select u.id as user_id, u.email as account_email, i.id as identity_id, i.provider,i.identity_data ->> 'email' as identity_email, i.created_at, i.last_sign_in_atfrom auth.users ujoin auth.identities i on i.user_id = u.idwhere u.id = '<user id>'; -
Confirm with the account's owner, through the current address, that the mismatched identity is not theirs or is no longer wanted.
-
Escalate to Gregor with the user id, the identity id and the owner's confirmation, recorded on the ticket.
15. Accepted residuals (spec D10)
These are known and accepted for phase 1a. Do not report them as new defects.
- Google is not authoritative for every address. Google vouches for an address only when it is a Gmail address, or when
email_verifiedis true and the account has a Workspace domain (hd). For any other address, Google verified it when the Google account was created, and the address may have changed hands since. A former holder of a reassigned address could therefore link a personal Google account to the Constellation account for that address. Offboarding (section 12) still ends that access. Restricting providers per domain needs a custom access-token hook, which is out of scope. - Passwords planted before PT-1147. Before PT-1147, whoever held an invitation token could register the invited address with a password of their choosing. If the real owner later links Google and never resets that password, the planted password keeps working, because Supabase removes only unconfirmed identities when it links. It needs a malicious inviter and a victim who never resets. Invalidating every password on its first Google link would close it, at the cost of every Google adopter's password; that remains Gregor's choice.
- A rewritten email, until the backfill marker (phase 1b). A home-tenant admin can rewrite a user's email to an address the admin controls. A login for that address then reaches the user's row, memberships in other tenants included, through the email fallback. This is true today, independent of Google. PT-1147 closes the registration path to it, and DIR-149's backfill marker ends the fallback; until then DIR-152 decides an interim mitigation.
Phase 1b (INF-663) adds its procedures to this page: resolving IDENTITY_CONFLICT, what DIR-149 reports, and a replaced Supabase account.
16. Linking a partner-admin invitation to its pre-created row
Project Tracker reuses an existing identity row at registration only when the invitation names it (projects.invitations.precreated_user_id, PT-1147). Partner provisioning does not set that link yet (DIR-154), so an operator links each partner-admin invitation that lacks it before its URL is sent. That covers every new partner invitation, one issued before this change, one reissued after an earlier run created the row, and one an interrupted run left.
The procedure, with its commands, is in .ai/runbooks/partner-tenant-provisioning.md, "Link the invitation to the admin row, then send the URL". Follow it there rather than from a copy. Two of its checks are yours alone, because the tool cannot make them:
- Provisioning's audit trail shows that provisioning created the row for that address (
admin_user.provisionedwithuserCreated: true). - Immediately before applying, no login has used the row: no Supabase account has the row's id, and no account exists for the address, or the one that does has never signed in.
Before PT-1147 part 2 reaches an environment (its merge to develop for staging, the release that carries it for production), link every PENDING partner-admin invitation there this way. Section 2 step 1 and section 7 step 1 check that it was done.