---
path: /tenant/invite
surface: customer
domain: onboarding-tenant
auth: signed-in
source: apps/oshun/web/src/app/tenant/invite/page.tsx
status: walked
last_walked:
  '2026-06-26 automated runtime coverage added - Playwright creates a real
  tenant invitation through the BFF, signs in as the invitee, accepts the token
  from this page, and reads the tenant-console cockpit state back through the
  BFF. Outbound email, tenant SSO/MFA, and full onboarding completion remain
  journey-level gaps.'
---

# Tenant Invite

## Purpose

Signed-in handoff page for a one-time tenant member invitation. It accepts an
`inviteId` + `token` pair through the tenant invitation BFF and then links the
member into onboarding.

## Entry Points

- Invite URL after authentication:
  `/tenant/invite?inviteId=<inviteId>&token=<token>&tenant=<tenantId>&next=/onboarding`
- Anonymous arrivals are protected by the app proxy and redirect to
  `/welcome?redirect=...` before this page renders.
- `next` is sanitized by `sanitizeRedirectPath`; default target is
  `/onboarding`.

## Layout Regions

- `LWebShell` with `LCustomerNav` active on Profile.
- `LMasthead` titled "Join your tenant." with the tenant id or membership
  fallback in the right rail.
- `TenantInviteAcceptPanel` below the masthead.

## States

- [x] **Ready** - invite id and token are present; the accept button is enabled.
- [x] **Submitting** - the accept button is disabled while the BFF request is in
      flight.
- [x] **Accepted** - renders the accepted member id, tenant id, status, roles,
      and a link to the sanitized `next` route.
- [x] **Missing** - missing invite id or token disables submit and reports the
      incomplete link state.
- [x] **Error** - maps BFF status codes to operator-readable invite messages:
      unauthenticated, token mismatch, already used/revoked, expired, or
      incomplete.

## Interactions

- [x] **Accept invitation**
  - Selector: `[data-tenant-invite-accept]`
  - BFF: `POST /v1/admin/tenant-console/members/invite/:inviteId/accept`
  - Body: `{ token }`
  - On success, the panel moves to `data-tenant-invite-status="accepted"`.
- [x] **Continue to onboarding**
  - Selector: `[data-tenant-invite-next]`
  - Target: sanitized `next` query param, default `/onboarding`.

## Data & Contracts

- **Reads**: query params `inviteId`, `token`, `tenant`, and `next`.
- **Writes**:
  - `api.post('/v1/admin/tenant-console/members/invite/:inviteId/accept', { token })`
  - The BFF creates or activates the member under the invite's tenant and marks
    the invitation accepted.
- **Auth/role check**: signed-in customer session. The BFF uses the current
  authenticated user as the accepted member.
- **Post-condition proof**: Playwright reads
  `GET /v1/tenant-console/members?tenantId=<tenantId>` with an admin bearer and
  asserts the active member, accepted invitation, hidden token hash, role, and
  seat allocation.

## E2E Coverage

Spec: `apps/oshun/web/e2e/tenant-member-actions.spec.ts`

Coverage depth: partial. The browser page, protected anonymous redirect, live
BFF accept, and tenant-console read-back are covered. The outbound email that
generates the link, tenant SSO/MFA, full `OnboardingWizard` completion, first
tenant action, and tenant audit reconciliation remain journey-level gaps.

## Cross-References

- Journey:
  [`../../journeys/tenant-onboard.md`](../../journeys/tenant-onboard.md)
- Bulk invite journey:
  [`../../journeys/tenant-bulk-member-invite-scim.md`](../../journeys/tenant-bulk-member-invite-scim.md)
- Onboarding destination: [`onboarding.md`](./onboarding.md)
- Auth handoff:
  [`../../shell/04-auth-session.md`](../../shell/04-auth-session.md)
