V1 Web PWA · Journey

Journey: Tenant onboarding (admin invites first member)

A source-reconciled journey record for V1 Web PWA — shipped behavior, state boundaries, failure modes, and the automation evidence available today.

automated · 4 specs verdict: partial· 2026-05-29
9sections8 minread

On this page
Journey at a glance
ActorsOperatorInvitee with an Oshun sessionTenant administratorFirst-time member
Automation4 E2E spec(s) · verdict: partial· 2026-05-29

Journey flow#

Generated from the authored steps below — click a node to jump to that section.

flowchart TD s1["1. Open the operator register"] s2["2. Create a pending invitation"] s3["3. Render the pending row"] s4["4. Open the signed-in acceptance page"] s5["5. Accept and allocate a seat"] s6["6. Continue only by explicit link"] s7["7. Observe the missing lifecycle joins"] s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 click s1 href "#1-open-the-operator-register" click s2 href "#2-create-a-pending-invitation" click s3 href "#3-render-the-pending-row" click s4 href "#4-open-the-signed-in-acceptance-page" click s5 href "#5-accept-and-allocate-a-seat" click s6 href "#6-continue-only-by-explicit-link" click s7 href "#7-observe-the-missing-lifecycle-joins" s7 --> v(["verdict: partial"]) click v href "../results/tenant-onboard.html" style v stroke:#d6a531,color:#d6a531

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/tenant and 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/tenant passes the operator page gate and can use the shared API client.
  • Invite mutation accepts admin:* or admin:workspace:tenant-console. It selects tenant from x-oshun-viewer-tenant-id, then the auth tenant claim, then house. admin:workspace:tenant-console is 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> or tenant: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.

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_login cannot distinguish “not accepted” from “accepted but not audited.”
  • Onboarding link only — navigating to /onboarding does not prove wizard completion, tenant defaults, or a first action.

E2E coverage#

  • apps/oshun/web/e2e/operator-surfaces.spec.ts covers operator entry, tenant fixture register, actions, responsive layout, and anonymous gates.
  • apps/oshun/web/e2e/tenant-onboard.spec.ts protects the rendered tenant-onboarding affordance contract.
  • apps/oshun/web/e2e/tenant-member-actions.spec.ts drives 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.ts proves 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#

Cross-references#

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-console become 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?