Skip to main content

Company Identity & Linked Accounts Rollout

This runbook covers the staged deployment of verified public company emails, revocable sessions, and linked User groups introduced by SUP-00345.

Database ownership

Do not run a local Prisma migration, prisma migrate*, or prisma db push. Backend CI/CD owns the schema update through its one-shot ECS task. Run the inventory and backfill in ECS with the environment secret bundle loaded before application imports.

Rollout order

Identity writes and legacy-wide access enforcement are deliberately separate. New writes must become typed and Pending before the legacy inventory starts. Every typed company fails closed immediately unless it is Active with a verified current claim and an eligible direct linked representative; only genuinely untyped legacy rows retain compatibility access before the zero-untyped-company gate. Enabling enforcement also implies identity writes, so a bad switch combination cannot create a new untyped operational company.

Feature controls

Environment values are deployment defaults. The live SystemSetting values provide independently controllable cohorts and take precedence when present.

ControlEnvironment fallbackLive settingPurpose
Company identity writesCOMPANY_IDENTITY_WRITES_ENABLEDsup00345.companyIdentityWrites.enabledRequire public email on new companies, reserve a Pending claim, create the direct representative, and write a typed Pending lifecycle without yet denying legacy access.
Company access enforcementCOMPANY_EMAIL_VERIFICATION_ENABLEDsup00345.companyEmailEnforcement.enabledFail closed unless lifecycle is Active, the current claim is verified, and the direct human representative is active, verified, and linked.
Linked-account readLINKED_ACCOUNTS_ENABLEDsup00345.linkedAccounts.read.enabledExpose Linked Accounts and the grouped profile model.
LinkLINKED_ACCOUNTS_ENABLEDsup00345.linkedAccounts.link.enabledStart and complete password/Google link challenges.
SwitchLINKED_ACCOUNTS_ENABLEDsup00345.linkedAccounts.switch.enabledRotate sessions into linked Users and profiles.
Ownership transferLINKED_ACCOUNTS_ENABLEDsup00345.companyOwnershipTransfer.enabledStart the staged linked-owner and public-email transfer.
Emergency stopnonesup00345.emergencyDisabledDisable linked-account mutations immediately without deleting groups, claims, or domain data. It never weakens company operational enforcement.

Keep identity writes on after step B, even if company access enforcement must later be rolled back. A code deploy must not implicitly enable a cohort.

Pre-deployment checks

  1. Confirm backend, frontend, and documentation PRs target dev and CI is green.
  2. Confirm generated Prisma client and schema.gql match the resolver contract.
  3. Confirm company creation no longer accepts client-controlled status.
  4. Confirm generic company updates cannot write Company.email and generic User updates cannot change session, verification, privilege, or selected-company security context.
  5. Confirm session validation covers GraphQL HTTP, WebSocket, SSE, and voice connections.
  6. Confirm the email sender produces one message containing both the link and code.
  7. Confirm identity-write and enforcement controls can be changed independently and that enforcement implies writes.
  8. Confirm the linked Google callback, encryption secret, provider redirect, and frontend return origin are configured as described below.
  9. Confirm the emergency linked-group kill switch is tested before pilot access.

Secure linked Google proof configuration

Configure these values in the backend task definition or secret bundle before enabling sup00345.linkedAccounts.link.enabled:

VariableRequirement
GOOGLE_CLIENT_IDOAuth client used to validate the ID-token audience. It must match the client that owns the immutable Google subjects already stored in AuthIdentity.
GOOGLE_CLIENT_SECRETProvider token-exchange credential. Store only in the backend secret bundle.
GOOGLE_LINK_CALLBACK_URLExact backend callback, API_ORIGIN/auth/google/link/callback. Register this exact HTTPS URL as an authorized Google redirect URI for each environment. Do not point it at the frontend.
GOOGLE_OAUTH_ENCRYPTION_KEYDedicated, high-entropy key material used for AES-GCM encryption of PKCE verifiers. Keep it stable across all backend tasks and deployments while requests are in flight. The JWT_SECRET fallback exists only for staged compatibility; production should set the dedicated value.
FRONTEND_URLCanonical frontend origin. The backend derives the purpose-only return page as FRONTEND_ORIGIN/auth/link-callback.

The provider authorization request must use server-owned state, nonce, and PKCE S256. The request is bound to the authenticated session, link challenge, proof role (CURRENT or TARGET), expected User, and immutable provider subject. The provider callback may expose only the short-lived one-time proof code; it must never put a JWT, refresh token, provider token, state value, challenge ID, or User identity in the redirect URL.

Before enabling the link cohort, verify both callback directions:

  1. Google returns only to the configured backend GOOGLE_LINK_CALLBACK_URL.
  2. The backend returns to /auth/link-callback?code=... on the configured FRONTEND_URL.
  3. The frontend removes the code from browser history before exchanging it against the existing authenticated session.
  4. Replaying the provider state or browser exchange code fails, and a different session, challenge, role, User, or Google subject cannot consume it.
  5. A User with enabled two-factor authentication still completes that factor after Google provider proof.

Legacy email inventory

Run the GitHub Actions workflow SUP-00345 Company Identity Backfill (.github/workflows/sup-00345-company-identity-backfill.yml) in inventory mode before enabling enforcement. The workflow uses the deployed backend ECS task definition, network, and configured awslogs group and stream prefix, waits for the one-shot task to finish, captures its CloudWatch JSON report, and uploads that report as deployment evidence.

The workflow invokes this packaged command inside the CI-owned ECS task after the environment secret bundle has supplied DATABASE_URL:

npm run sup-00345:company-identity-inventory

The report must include:

  • total companies by legacy status;
  • active companies with one syntactically valid unique normalized email;
  • companies with an identifiable active, verified direct human owner and current linked-account membership;
  • companies whose ownership relation is missing, ambiguous, or points to an ineligible User;
  • active companies with a missing or invalid email;
  • every normalized email shared by two or more companies, including all Company IDs;
  • suspended and unclaimed companies;
  • unknown status values;
  • proposed lifecycle and activation source counts.

Do not partially grandfather a duplicate set. Every company sharing a normalized legacy address enters REVIEW_REQUIRED, and the conflicting addresses are hidden until unique claims verify.

Backfill rules

Legacy recordResult
Active with one valid unique email and one eligible direct human ownerACTIVE, verified claim, direct representative, source LEGACY_GRANDFATHERED
Valid unique email but no eligible direct linked User ownerPENDING through the distinct PENDING_OWNER_REQUIRED action; public email hidden and blocked until an eligible owner is assigned
SuspendedSUSPENDED; public compatibility email cleared and public output hidden
UnclaimedUNCLAIMED; public compatibility email cleared and public output hidden
Active with no valid emailREVIEW_REQUIRED; public email hidden
Any duplicate normalized emailEvery conflicting company becomes REVIEW_REQUIRED; public emails hidden
Unknown statusREVIEW_REQUIRED

The backfill must be idempotent and emit inserted, updated, unchanged, PENDING_OWNER_REQUIRED, conflict, and failure counts. Re-run SUP-00345 Company Identity Backfill in apply mode. Production apply requires the workflow's exact confirmation value; never invoke the database command from a workstation. The workflow runs this packaged command:

npm run sup-00345:company-identity-apply

Zero-untyped-company gate

After the apply pass, run npm run sup-00345:company-identity-inventory again against the same environment. Company access enforcement is blocked until all of these are true:

  • the apply report has zero failures;
  • the post-apply report has counts.legacyCandidates: 0 (every Company has a non-null typed lifecycle);
  • a second apply is idempotent and performs no unexplained writes;
  • the original redacted conflict report is retained for companies now typed REVIEW_REQUIRED;
  • suspended, unclaimed, duplicate-email, missing-email, and unknown-status companies do not publish a compatibility email.

Do not treat a healthy ECS task or a nonzero alreadyTyped count as proof of this gate. Preserve the complete post-apply JSON report and explicitly record the zero untyped count in deployment evidence.

Dev pilot

Exercise real workflows, not only service health:

  1. Create a company and confirm it is Pending and its email is absent from public company output.
  2. Verify once by link and once by manual code; confirm replay is idempotent.
  3. Confirm duplicate claims are rejected against both Pending and Active companies.
  4. Confirm a company address may equal its representative's User login email but still requires verification.
  5. Remove or invalidate the direct owner relation and confirm the company remains Pending and every operational company boundary rejects it even when its mailbox is verified.
  6. Replace an Active company's email and confirm the old address remains public until verification.
  7. Link Standard, Company Admin, Support Agent, Super Admin, and Root Users.
  8. Switch from an ordinary originating credential into each privileged User and prove the real target permissions and audit attribution.
  9. Assign a linked User as owner/representative of another owned company. Confirm the old owner/email remain effective until the linked User's login email completes the independent company challenge, then confirm the ownership and email swap is atomic.
  10. Call the legacy admin direct-ownership mutation and confirm it is denied and audited rather than changing roles, billing ownership, representative, or email.
  11. Suspend and inspect legacy Unclaimed/Suspended companies; confirm neither a direct lookup nor directory output exposes their public email.
  12. Complete current-User and target-User Google proofs. Confirm replay, wrong-session, wrong-challenge, wrong-role, and wrong-subject exchanges fail, while enabled two-factor authentication remains required.
  13. Unlink and confirm no domain foreign keys moved and stale HTTP, WebSocket, SSE, and voice contexts are rejected.

Monitoring

Track by environment and cohort:

SignalInvestigate when
Verification failure rateSustained increase by provider, domain, or challenge type
Reservation conflictsA spike follows onboarding or backfill enablement
Activation timeMedian or tail grows after email delivery changes
Privileged switchesUnexpected volume, target, IP, or device pattern
Refresh-token replayAny confirmed reuse outside expected concurrent-refresh handling
Stream revocation latencyA revoked session remains connected after the event fan-out window
Audit attributionActive User or originating User is missing or inconsistent
Billing contextCharges resolve to a company other than the selected Active company
Ownership transfersRepresentative, owner role, billing owner, and verified claim do not change together

Keep deployed/healthy separate from proven by a real workflow in rollout notes.

Containment and rollback

If account linking or switching is unsafe, enable the emergency group kill switch first. Block new link/switch/ownership-transfer operations, revoke affected sessions, and leave memberships and domain data intact for investigation.

If email enforcement causes unexpected activation failures, disable only access enforcement while keeping company identity writes enabled. Do not delete claims, create new untyped companies, republish conflicting or non-operational legacy emails, or use the legacy admin ownership mutation as a repair shortcut. Do not roll back a schema after claims or sessions have been written. Fix forward with additive code and an idempotent repair task.

For suspected credential compromise:

  1. revoke the User's and linked group's sessions;
  2. invalidate outstanding link, email, and sensitive-action challenges;
  3. disable the affected User if appropriate;
  4. inspect origin-aware audit events by session, group, IP, and device;
  5. restore access through normal password/Google and two-factor recovery;
  6. require fresh company-email verification for any cancelled ownership transfer.

Completion evidence

Attach the following to SUP-00345 through the support-system MCP:

  • merged PRs and successful dev deployment run IDs;
  • backend and frontend test/build summaries;
  • inventory and post-backfill count reports with conflicts redacted appropriately;
  • explicit evidence that the post-backfill inventory reported zero untyped companies;
  • real dev workflow evidence for activation, privileged switching, linked ownership transfer, and stale-session rejection;
  • linked Google configuration and replay/isolation test evidence with all secrets and one-time codes redacted;
  • health endpoints and ECS service stability;
  • feature-setting/cohort state and emergency-switch verification.

Only mark the ticket Ready for Test after the deployed dev workflow has been exercised end to end.