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.
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.
| Control | Environment fallback | Live setting | Purpose |
|---|---|---|---|
| Company identity writes | COMPANY_IDENTITY_WRITES_ENABLED | sup00345.companyIdentityWrites.enabled | Require 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 enforcement | COMPANY_EMAIL_VERIFICATION_ENABLED | sup00345.companyEmailEnforcement.enabled | Fail closed unless lifecycle is Active, the current claim is verified, and the direct human representative is active, verified, and linked. |
| Linked-account read | LINKED_ACCOUNTS_ENABLED | sup00345.linkedAccounts.read.enabled | Expose Linked Accounts and the grouped profile model. |
| Link | LINKED_ACCOUNTS_ENABLED | sup00345.linkedAccounts.link.enabled | Start and complete password/Google link challenges. |
| Switch | LINKED_ACCOUNTS_ENABLED | sup00345.linkedAccounts.switch.enabled | Rotate sessions into linked Users and profiles. |
| Ownership transfer | LINKED_ACCOUNTS_ENABLED | sup00345.companyOwnershipTransfer.enabled | Start the staged linked-owner and public-email transfer. |
| Emergency stop | none | sup00345.emergencyDisabled | Disable 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
- Confirm backend, frontend, and documentation PRs target
devand CI is green. - Confirm generated Prisma client and
schema.gqlmatch the resolver contract. - Confirm company creation no longer accepts client-controlled status.
- Confirm generic company updates cannot write
Company.emailand generic User updates cannot change session, verification, privilege, or selected-company security context. - Confirm session validation covers GraphQL HTTP, WebSocket, SSE, and voice connections.
- Confirm the email sender produces one message containing both the link and code.
- Confirm identity-write and enforcement controls can be changed independently and that enforcement implies writes.
- Confirm the linked Google callback, encryption secret, provider redirect, and frontend return origin are configured as described below.
- 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:
| Variable | Requirement |
|---|---|
GOOGLE_CLIENT_ID | OAuth 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_SECRET | Provider token-exchange credential. Store only in the backend secret bundle. |
GOOGLE_LINK_CALLBACK_URL | Exact 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_KEY | Dedicated, 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_URL | Canonical 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:
- Google returns only to the configured backend
GOOGLE_LINK_CALLBACK_URL. - The backend returns to
/auth/link-callback?code=...on the configuredFRONTEND_URL. - The frontend removes the code from browser history before exchanging it against the existing authenticated session.
- Replaying the provider state or browser exchange code fails, and a different session, challenge, role, User, or Google subject cannot consume it.
- 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 record | Result |
|---|---|
| Active with one valid unique email and one eligible direct human owner | ACTIVE, verified claim, direct representative, source LEGACY_GRANDFATHERED |
| Valid unique email but no eligible direct linked User owner | PENDING through the distinct PENDING_OWNER_REQUIRED action; public email hidden and blocked until an eligible owner is assigned |
| Suspended | SUSPENDED; public compatibility email cleared and public output hidden |
| Unclaimed | UNCLAIMED; public compatibility email cleared and public output hidden |
| Active with no valid email | REVIEW_REQUIRED; public email hidden |
| Any duplicate normalized email | Every conflicting company becomes REVIEW_REQUIRED; public emails hidden |
| Unknown status | REVIEW_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:
- Create a company and confirm it is Pending and its email is absent from public company output.
- Verify once by link and once by manual code; confirm replay is idempotent.
- Confirm duplicate claims are rejected against both Pending and Active companies.
- Confirm a company address may equal its representative's User login email but still requires verification.
- 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.
- Replace an Active company's email and confirm the old address remains public until verification.
- Link Standard, Company Admin, Support Agent, Super Admin, and Root Users.
- Switch from an ordinary originating credential into each privileged User and prove the real target permissions and audit attribution.
- 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.
- Call the legacy admin direct-ownership mutation and confirm it is denied and audited rather than changing roles, billing ownership, representative, or email.
- Suspend and inspect legacy Unclaimed/Suspended companies; confirm neither a direct lookup nor directory output exposes their public email.
- 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.
- Unlink and confirm no domain foreign keys moved and stale HTTP, WebSocket, SSE, and voice contexts are rejected.
Monitoring
Track by environment and cohort:
| Signal | Investigate when |
|---|---|
| Verification failure rate | Sustained increase by provider, domain, or challenge type |
| Reservation conflicts | A spike follows onboarding or backfill enablement |
| Activation time | Median or tail grows after email delivery changes |
| Privileged switches | Unexpected volume, target, IP, or device pattern |
| Refresh-token replay | Any confirmed reuse outside expected concurrent-refresh handling |
| Stream revocation latency | A revoked session remains connected after the event fan-out window |
| Audit attribution | Active User or originating User is missing or inconsistent |
| Billing context | Charges resolve to a company other than the selected Active company |
| Ownership transfers | Representative, 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:
- revoke the User's and linked group's sessions;
- invalidate outstanding link, email, and sensitive-action challenges;
- disable the affected User if appropriate;
- inspect origin-aware audit events by session, group, IP, and device;
- restore access through normal password/Google and two-factor recovery;
- 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.