# Client Password Invite Flow

**Date**: 2026-04-18
**Status**: Draft
**Supersedes**: `docs/superpowers/specs/2026-04-15-client-magic-links-design.md`

## Overview

The current client invite flow is built around a magic-link callback path. That model is no longer the target.

`apps/clients` should become a normal password-based portal for invited client users. Invitations should bootstrap account access, not serve as the long-term authentication mechanism.

The revised flow is:

- a client is invited by email from `app.xtablo.com`
- if this email has not completed onboarding yet, the email contains a one-time password setup link
- the client sets a password once
- that setup link becomes invalid immediately after successful use
- all later access goes through a normal login form in `apps/clients`
- clients can reset their password themselves from the client login page

Client accounts are reused across multiple tablos by email. If a client who already has a password-based account is invited to another tablo, they should receive an access notification email instead of another password-setup link.

## Problem Statement

The current magic-link callback flow creates the wrong steady-state model for the client portal.

### Current issues

1. The invite email behaves as an authentication mechanism instead of a one-time onboarding step.
2. `apps/clients` does not provide a standard login form for later access.
3. The current callback-style flow is a poor fit for a client portal meant to feel like a stable authenticated product.
4. Reinviting the same email is awkward because the current model is centered around link acceptance rather than an account reused across multiple tablos.
5. The current flow does not express a strong boundary between `apps/main` users and `apps/clients` users.

## Goals

- Replace callback-style magic-link onboarding with one-time password setup
- Make `apps/clients` a normal email/password application after onboarding
- Reuse one client account per email across multiple tablos
- Allow self-service password reset from the client login page
- Support direct notification links to `clients.xtablo.com/tablo/:tabloId`
- Keep clients restricted to `apps/clients` only
- Reuse the main login page visual design through a shared auth UI surface

## Non-Goals

- Permanent bearer links that grant direct tablo access without authentication
- Self-service client signup without invitation
- Creating a separate custom auth system outside Supabase
- Granting client-portal users access to `apps/main`
- Preserving the current callback-based onboarding as the primary flow

## Hard Requirements

- Client users must not have access to `apps/main`
- The password-setup link must be one-time use
- The setup link must become invalid immediately after successful password creation
- The same email must map to one reusable client account across multiple tablos
- Existing onboarded clients invited to another tablo must receive an access notification email, not a new setup link
- The notification email must link directly to `clients.xtablo.com/tablo/:tabloId`

## Chosen Approach

Keep Supabase as the underlying authentication provider, but move invitation control into an invite lifecycle owned by the backend.

The backend creates or reuses a client auth user by email, grants access to the target tablo, and then chooses between two email modes:

- onboarding email with a one-time setup token
- access notification email for an already-onboarded client

`apps/clients` becomes a normal authenticated app with:

- a login page
- a one-time set-password page
- a forgot-password flow
- protected routes that redirect unauthenticated users to login and then resume their intended destination

## User Classes And App Boundary

The system should treat main-app users and client-portal users as distinct user classes.

### Main-app users

- collaborators
- internal users
- users who are allowed to access `app.xtablo.com`

### Client-portal users

- external client users invited to tablos
- users who are allowed to access `clients.xtablo.com`
- users who must not be able to use `apps/main`

### Boundary rule

Sharing auth UI does not mean sharing authorization.

Client accounts must be rejected by `apps/main` even if they hold a valid authenticated session. This boundary must be enforced in backend authorization as well as frontend routing.

## Auth Model

`apps/clients` becomes a normal password-based portal.

### Steady state

- one client account per email
- reused across multiple tablos
- normal email/password login after onboarding
- standard self-service password reset via "mot de passe oublié"

### Invite role

The invite email is no longer the long-term access credential. It is only the bootstrap mechanism for first-time password setup.

## Invite Lifecycle

Invite creation should branch based on whether the email already belongs to an onboarded client account.

### First invite for an email without a password-based client account

- create or reuse the client auth user for that email
- create or confirm the tablo access grant
- create a one-time setup token
- send a setup email to `clients.xtablo.com`

### Later invite for an existing onboarded client account

- create or confirm the tablo access grant
- do not create a setup token
- send a "you now have access" notification email

This keeps onboarding single-use while allowing account reuse across many tablos.

## End-To-End Flows

### First-time onboarding flow

1. Admin invites a client from `app.xtablo.com`.
2. Backend creates or reuses the client auth user.
3. Backend grants access to the target tablo.
4. Backend creates a one-time setup token.
5. Email sends a setup URL into `clients.xtablo.com`.
6. Client opens the link and validates the token.
7. Client sets a password.
8. Backend invalidates the token immediately.
9. Client is signed in and redirected into the client portal.

### Additional tablo access for an already-onboarded client

1. Admin invites the same email to another tablo.
2. Backend reuses the same client account.
3. Backend grants access to the target tablo.
4. Email sends a notification link to `clients.xtablo.com/tablo/:tabloId`.
5. If the client already has a session, the tablo opens directly.
6. If not authenticated, `apps/clients` redirects to login and returns to that tablo after successful login.

## Frontend Design

`apps/clients` should expose three auth surfaces:

- `LoginPage`
- `SetPasswordPage`
- existing authenticated portal routes

### LoginPage

Requirements:

- minimal standalone auth screen
- visually matches the main login page
- built from a shared auth UI package instead of importing directly from `apps/main`
- email and password fields
- forgot-password entry point
- no self-service signup

### SetPasswordPage

Requirements:

- dedicated route for one-time invite setup
- validates token before allowing password creation
- handles invalid, expired, and already-used tokens clearly
- on success, invalidates token and transitions into an authenticated client session

### Protected route behavior

- unauthenticated access to `clients.xtablo.com/tablo/:tabloId` redirects to login
- login preserves and resumes the intended destination
- fallback destination remains the client tablo list if no target route was captured

## Shared Auth UI

The login page in `apps/clients` should look like the main login page, but this should be done through extraction, not duplication.

Recommended ownership split:

- shared package owns auth shell, layout, form framing, banners, and visual treatment
- `apps/main` and `apps/clients` own submit handlers, route targets, and app-specific copy

This keeps visual parity durable without coupling `apps/clients` directly to `apps/main` internals.

## Backend Design

`client_invites` should remain the lifecycle/control record, but its meaning changes.

### Previous role

- pending invite accepted through callback-style magic-link flow

### New role

- one-time password-setup authorization record for first-time onboarding

### Backend responsibilities

`POST /client-invites/:tabloId`

- create or reuse client auth user by email
- create or confirm tablo access
- decide whether this email needs onboarding or only an access notification
- send the correct email type

Token validation endpoint:

- used by `SetPasswordPage`
- verifies token exists, is pending, and is still valid

Password setup completion endpoint:

- verifies token again
- sets password for the underlying auth user
- invalidates token immediately
- completes the onboarding transition cleanly

Admin visibility and cancellation endpoints:

- remain available for operational control
- cancelling a pending setup invite invalidates that setup path immediately

## Authorization Model

Authorization must reflect the split between apps.

### Required behavior

- client-portal users can access `apps/clients` resources they were granted
- client-portal users cannot use `apps/main` flows
- main-app authorization cannot assume that every authenticated user is a main-app user

This must be enforced on the backend, not only in the frontend shell.

## Error Handling

### Frontend

`SetPasswordPage` must handle:

- invalid token
- expired token
- already-used token
- password policy failure

`LoginPage` must handle:

- wrong credentials
- reset email sent state
- reset failure

Protected routes must:

- redirect unauthenticated users to login
- preserve intended destination
- resume navigation after login

### Backend

Invite creation must distinguish:

- first-time onboarding invite
- additional-access notification

Token completion must fail cleanly on:

- expired token
- reused token
- cancelled token

## Testing Strategy

### API tests

- first invite for a new client creates a setup token and sends setup email
- second invite for an already-onboarded client skips setup token creation and sends access notification
- setup token can be used exactly once
- expired or reused setup token is rejected
- client-only accounts are rejected by main-app authorization paths

### Frontend tests

- login page renders through shared auth UI and submits email/password flow
- forgot-password flow is reachable from the client login page
- set-password page handles success, invalid token, expired token, and reused token states
- protected `tablo/:tabloId` route redirects to login and resumes correctly after authentication
- access notification deep-link opens the intended tablo after login

### Manual verification

- first invite email for a new client leads to one-time setup, then normal login
- second invite for the same client leads to access notification only
- `clients.xtablo.com/tablo/:tabloId` works both with and without an existing session
- client user cannot enter `app.xtablo.com`

## Migration Notes

- the current `apps/clients/src/pages/AuthCallback.tsx` route should be removed or reduced to legacy compatibility once this flow is live
- existing frontend code that assumes invitation equals magic-link acceptance should be replaced with setup-token and login flows
- because the feature is not yet live, no legacy client-user migration path is required
