From c2ad27c8c7d3c4eb0b39bc7351d9431256df5e36 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 18 Apr 2026 09:03:53 +0200 Subject: [PATCH] docs: add client password invite flow spec --- ...4-18-client-password-invite-flow-design.md | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-18-client-password-invite-flow-design.md diff --git a/docs/superpowers/specs/2026-04-18-client-password-invite-flow-design.md b/docs/superpowers/specs/2026-04-18-client-password-invite-flow-design.md new file mode 100644 index 0000000..5ab20f9 --- /dev/null +++ b/docs/superpowers/specs/2026-04-18-client-password-invite-flow-design.md @@ -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