docs: add client password invite flow spec
This commit is contained in:
parent
740bedaf50
commit
c2ad27c8c7
1 changed files with 318 additions and 0 deletions
|
|
@ -0,0 +1,318 @@
|
|||
# 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
|
||||
Loading…
Reference in a new issue