11 KiB
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
- The invite email behaves as an authentication mechanism instead of a one-time onboarding step.
apps/clientsdoes not provide a standard login form for later access.- The current callback-style flow is a poor fit for a client portal meant to feel like a stable authenticated product.
- Reinviting the same email is awkward because the current model is centered around link acceptance rather than an account reused across multiple tablos.
- The current flow does not express a strong boundary between
apps/mainusers andapps/clientsusers.
Goals
- Replace callback-style magic-link onboarding with one-time password setup
- Make
apps/clientsa 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/clientsonly - 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
- Admin invites a client from
app.xtablo.com. - Backend creates or reuses the client auth user.
- Backend grants access to the target tablo.
- Backend creates a one-time setup token.
- Email sends a setup URL into
clients.xtablo.com. - Client opens the link and validates the token.
- Client sets a password.
- Backend invalidates the token immediately.
- Client is signed in and redirected into the client portal.
Additional tablo access for an already-onboarded client
- Admin invites the same email to another tablo.
- Backend reuses the same client account.
- Backend grants access to the target tablo.
- Email sends a notification link to
clients.xtablo.com/tablo/:tabloId. - If the client already has a session, the tablo opens directly.
- If not authenticated,
apps/clientsredirects to login and returns to that tablo after successful login.
Frontend Design
apps/clients should expose three auth surfaces:
LoginPageSetPasswordPage- 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/:tabloIdredirects 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/mainandapps/clientsown 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/clientsresources they were granted - client-portal users cannot use
apps/mainflows - 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/:tabloIdroute 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/:tabloIdworks both with and without an existing session- client user cannot enter
app.xtablo.com
Migration Notes
- the current
apps/clients/src/pages/AuthCallback.tsxroute 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