onboarding.mdtenant-invite.mdoperator-tenant.md04-auth-session.md05-notifications.mdJourney flow#
Generated from the authored steps below — click a node to jump to that section.
The shipped browser path covers operator invite creation, a one-time raw token, signed-in acceptance, durable seat/member state, and a link to onboarding. It does not send an email, perform invite-specific SSO/MFA, run the ten-step wizard, record first login/action, or reconcile live KPIs. “Tenant onboarding” is therefore a partial operator→member hand-off, not a completed first-use funnel.
Personas#
- Operator — opens
/operator/tenantand creates one pending invite. - Invitee with an Oshun session — receives the token out of band and accepts it while already authenticated.
- Tenant administrator — needs tenant-scoped authorization and live seat/ member truth rather than the fixed operator fixture.
- First-time member — should continue through onboarding and a first domain action, but that continuation is not joined here.
Pre-conditions#
/operator/tenantpasses the operator page gate and can use the shared API client.- Invite mutation accepts
admin:*oradmin:workspace:tenant-console. It selects tenant fromx-oshun-viewer-tenant-id, then the auth tenant claim, thenhouse.admin:workspace:tenant-consoleis not tenant-qualified, and this route does not verify the chosen tenant against a tenant-specific scope. - Cockpit read-back is a separate authorization contract requiring
tenant:admin:<tenantId>ortenant:admin:*. - Deployable BFF startup requires the Postgres-backed tenant invite snapshot, validates and hydrates it before traffic, and fails closed if that durable sink is unavailable. Invite, member, seat-pool, and erasure-fence state share the snapshot.
Steps#
1. Open the operator register#
The page renders a fixed Pacific Theological register and KPI band, then appends
live invite-store rows after successful mutations/read-back. Export CSV captures
the rendered rows. The trailing per-row ··· cell is presentational; there is
no member action menu.
2. Create a pending invitation#
Invite opens an accessible dialog for email and one optional free-text role.
POST /v1/admin/tenant-console/members/invite validates email syntax, creates a
seven-day pending record, stores only the SHA-256 token hash, returns the raw
token once, and emits tenant.member_invite. The response is acknowledged only
after the candidate invite/member/pool snapshot is saved. A failed save returns
503 tenant_invite_persistence_failed, leaves the prior state live, and emits
no success audit. New success audit detail identifies the invitation record and
categorical state without copying the target email.
It does not check a tenant email-domain allowlist, approved role catalogue, reviewer sign-off, or seat capacity. It also does not construct a full URL or call an email adapter; the operator is told to share the token manually.
3. Render the pending row#
TenantMemberRegister calls the tenant-console cockpit and adds the invitation
as a live invited row. If that read fails, it inserts a client-side fallback
row from the mutation response. The fixed “247 members” heading and KPI values
do not change, so the row is stronger evidence than the masthead count.
4. Open the signed-in acceptance page#
The working hand-off is
/tenant/invite?inviteId=<id>&token=<token>&tenant=<label>&next=/onboarding.
The page sanitizes next, displays the query tenant as context, and waits for
an explicit Accept click. The email-style root link, authentication redirect,
SSO claim mapping, and MFA challenge are not part of this implementation.
5. Accept and allocate a seat#
The acceptance route requires any authenticated user, then verifies invite id, pending state, expiry, token hash in constant time, and seat capacity. It does not compare the authenticated account's email to the invited email; possession of the token authorizes binding the current user id. The tenant query parameter is not checked against the stored invite; the BFF uses the invite's tenant.
On success it creates an active member with the invite roles, allocates one
seat, marks the invite accepted, links that invite internally to the exact
authenticated subject, and commits invite/member/pool together before the
response. The subject link is omitted from every public invite view. It lets
profile export and erasure find this acceptance even if the account email later
differs from the invited address. A failed save publishes no acceptance or seat
allocation. Second use returns tenant_invite_not_pending; bad token returns
tenant_invite_token_mismatch.
6. Continue only by explicit link#
The accepted panel shows account, tenant, status, and roles and renders
“Continue to onboarding.” The link is not an automatic navigation and carries no
verified tenant defaults. From this point /onboarding uses the ordinary
customer onboarding contract documented in its own journey.
7. Observe the missing lifecycle joins#
Acceptance does not append an audit event. No code in this path records
tenant.member_first_login, tenant.role_assigned, or
tenant.member_first_action; no first domain action updates the member's
lastSeenUnixSeconds; and the operator register has no drill-down or refreshed
KPI proof.
The direct “+ Add member” and SCIM routes are adjacent provisioning options, not steps in this invitation sequence. Direct add bypasses token acceptance; SCIM writes a separate projection.
Post-conditions#
- A pending invitation has a durable token hash, tenant, email, role ids, and expiry; raw token is visible once.
- Successful acceptance atomically creates an active member, consumes one seat, closes the invitation, and survives BFF restart as one validated snapshot.
- Signed profile export finds memberships and accepted invitations by exact subject even after an email change. Signed account erasure removes those rows, releases their seats, and leaves an exact-user resurrection fence.
- New invite/member audit rows preserve event type, tenant, record identity, categorical state, roles, and status without raw target email or user id.
- The customer can choose to open ordinary onboarding through a sanitized link.
- Invite creation is audited; acceptance/onboarding/first action are not joined into the tenant audit chain.
- No email delivery, invite-email/account equality, tenant-qualified workspace authorization, SSO/MFA, completed preferences, session mint, or first-use proof is provided by this journey.
Failure modes#
- Cross-tenant workspace authority — the unqualified tenant-console admin scope can pair with an arbitrary valid viewer-tenant header on mutation.
- Email/account mismatch — any signed-in account holding the valid token can accept; invited email is not matched to the auth identity.
- No delivery — the raw token can remain in the operator dialog with no branded link, provider receipt, bounce, resend, or suppression state.
- Capacity races at acceptance — invite creation succeeds without a seat;
later acceptance can return
409 tenant_seat_capacity_exceeded. - Persistence failure — any invite/member/seat mutation whose snapshot save
fails returns controlled
503; the prior published snapshot remains live. - Replica race — mutation ordering is per BFF process and uses one whole snapshot; there is no distributed compare-and-swap across replicas.
- Retention/audit history — terminal invites have no general time-based purge, historical audit rows containing older target PII are not rewritten, and successful acceptance still has no dedicated audit event.
- Expired/reused/revoked token — explicit 410/409 states are returned, but no operator resend/revoke control is exposed in this page.
- Fallback row ambiguity — a cockpit-read failure can still show the mutation response as a live-looking row, marked only by refresh-state data.
- Static metrics — member/active/KPI figures do not incorporate accepted or added members.
- Audit chain stops — acceptance has no event, so absence of
member_first_logincannot distinguish “not accepted” from “accepted but not audited.” - Onboarding link only — navigating to
/onboardingdoes not prove wizard completion, tenant defaults, or a first action.
E2E coverage#
apps/oshun/web/e2e/operator-surfaces.spec.tscovers operator entry, tenant fixture register, actions, responsive layout, and anonymous gates.apps/oshun/web/e2e/tenant-onboard.spec.tsprotects the rendered tenant-onboarding affordance contract.apps/oshun/web/e2e/tenant-member-actions.spec.tsdrives browser Invite/Add, real BFF/cockpit rows and audits, plus signed-in accept, second-use, wrong-token, validation, and capacity branches.apps/oshun/web/e2e/tenant-invite-role-bff.spec.tsproves role ids and the broad admin scope gate at the API layer.- The final isolated exact-source run of the two affected BFF-backed specs above passed 12/12 in Chromium, including public omission of the internal accepted subject link.
- Coverage depth: partial. Creation and acceptance are deep. Email, auth/SSO/MFA entry, wizard completion, first action, acceptance audit, and operator reconciliation are missing implementation joins.
Per-view files touched#
operator/operator-tenant.md— fixed register plus live invite/member rows.customer/01-onboarding/tenant-invite.md— protected acceptance page.customer/01-onboarding/onboarding.md— explicit next link only.shell/04-auth-session.md— ordinary auth boundary; no invite-specific SSO hand-off.shell/05-notifications.md— planned email channel, not called here.
Cross-references#
tenant-bulk-member-invite-scim.md— separates CSV, Invite/Add, and SCIM projections.onboarding-ten-steps.md— ordinary wizard behavior after the link.tenant-audit-log-investigation.md— current audit evidence and missing acceptance row.- Sources:
TenantMemberRegister.tsx,TenantMemberActions.tsx,TenantInviteAcceptPanel.tsx,admin-tenant-invitations.ts,tenant-invite-store.ts, andtenant-console-reads.tsunderapps/oshun.
Open questions#
- Should acceptance require verified invited-email equality, an explicit delegated invitee claim, or another binding beyond token possession?
- How should
admin:workspace:tenant-consolebecome tenant-qualified on every mutation, matching the cockpit read contract? - Which service generates and sends the full invite URL, stores delivery receipts, and supports resend/revoke without exposing the token again?
- What event distinguishes invitation acceptance from first authenticated login, completed onboarding, and first tenant action?
- How should live seat/member/active metrics replace the fixture masthead and expose failure instead of a fallback row that looks authoritative?