From a7cc2cc0bd3482b92885c596257d6bec9a924027 Mon Sep 17 00:00:00 2001 From: premsgr77 Date: Mon, 11 May 2026 09:28:43 +0545 Subject: [PATCH 1/2] docs: add user creation flow documentation --- user_creation_flow.md | 167 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 user_creation_flow.md diff --git a/user_creation_flow.md b/user_creation_flow.md new file mode 100644 index 0000000..8212a46 --- /dev/null +++ b/user_creation_flow.md @@ -0,0 +1,167 @@ +# User Creation Flow + +This document explains how new users are created in this monorepo, with focus on `saas` and its dependency on `fastify/packages/user`. + +Assumption used here: the app user table is named `users`. + +## Components Involved + +- `saas/packages/fastify`: SaaS-specific signup/account logic +- `fastify/packages/user`: base user persistence and SuperTokens integration +- SuperTokens: authentication/session/role internals + +## Main Tables + +### App-managed tables + +- `users` (from `fastify/packages/user`) +- `__accounts` (from `saas/packages/fastify`) +- `__account_users` (from `saas/packages/fastify`) +- `__account_invitations` (from `saas/packages/fastify`) + +### SuperTokens-managed tables + +- Auth/session tables (internal to SuperTokens) +- Role mapping tables (for example `st__user_roles`) + +## `users` Schema Location (Public vs Tenant) + +- `users` is in `public` schema when account-level separate schema is not used. +- `users` is in tenant schema when `account.database` is set (for example `s_xxxxxxxx`). +- Runtime routing is done via `dbSchema` in request/user context, and tenant schema is prepared by `runAccountMigrations`. + +### Tenant Schema Diagram (`account.database` is set) + +```mermaid +flowchart TD + A[Request resolved to account] --> B{account.database present?} + B -->|Yes| C[Set request.dbSchema = account.database] + C --> D[Use tenant search_path/schema] + D --> E[Create auth user in SuperTokens] + E --> F[Insert into tenant.users] + F --> G[Insert into tenant.__account_users] + G --> H{Invitation flow?} + H -->|Yes| I[Update tenant.__account_invitations.accepted_at] + H -->|No| J[Complete] +``` + +## High-Level Flow Diagram + +```mermaid +flowchart TD + A[Signup request received] --> B{Signup type} + + B -->|Email/password main app domain| C[Create account in __accounts] + C --> D[Create auth user in SuperTokens] + D --> E[Insert profile in users] + E --> F[Assign roles in SuperTokens role tables] + F --> G[Insert membership in __account_users as SAAS_ACCOUNT_OWNER] + + B -->|Email/password existing account context| D2[Create auth user in SuperTokens] + D2 --> E2[Insert profile in users] + E2 --> F2[Assign roles in SuperTokens role tables] + F2 --> G2[Insert membership in __account_users as MEMBER or provided role] + + B -->|Invitation signup token| H[Validate invitation in __account_invitations] + H --> D3[Create auth user in SuperTokens] + D3 --> E3[Insert profile in users] + E3 --> F3[Assign roles in SuperTokens role tables] + F3 --> G3[Insert membership in __account_users using invitation role] + G3 --> I[Update __account_invitations.accepted_at] + + B -->|Third-party sign-in/up new user| J[Create auth user in SuperTokens] + J --> K[Insert profile in users] + K --> L[Assign roles in SuperTokens role tables] + L --> M{Account context exists?} + M -->|Yes| N[Insert membership in __account_users] + M -->|No| O[Finish] +``` + +## Detailed Flows + +### 1) Email/password signup from main app domain + +Source path: +- `saas/packages/fastify/src/supertokens/recipes/third-party-email-password/emailPasswordSignUpPost.ts` +- `saas/packages/fastify/src/supertokens/recipes/third-party-email-password/emailPasswordSignUp.ts` + +Writes: +1. `__accounts` insert (new tenant/account) +2. SuperTokens auth user creation +3. `users` insert +4. SuperTokens role assignment insert(s) +5. `__account_users` insert as `SAAS_ACCOUNT_OWNER` + +Failure handling: +- If signup API result is not OK, created account is deleted. +- If `users` insert fails, SuperTokens user is deleted. + +### 2) Email/password signup on existing account domain/subdomain + +Source path: +- `saas/packages/fastify/src/plugins/accountDiscoveryPlugin.ts` +- `saas/packages/fastify/src/supertokens/recipes/third-party-email-password/emailPasswordSignUp.ts` + +Writes: +1. SuperTokens auth user creation +2. `users` insert +3. SuperTokens role assignment insert(s) +4. `__account_users` insert (default member role unless overridden) + +### 3) Invitation signup (new user) + +Source path: +- `saas/packages/fastify/src/model/accountInvitations/handlers/signup.ts` + +Writes: +1. SuperTokens auth user creation +2. `users` insert +3. SuperTokens role assignment insert(s) +4. `__account_users` insert using invitation role +5. `__account_invitations.accepted_at` update + +### 4) Invitation join (existing logged-in user) + +Source path: +- `saas/packages/fastify/src/model/accountInvitations/handlers/join.ts` + +Writes: +1. `__account_users` insert +2. `__account_invitations.accepted_at` update + +No new row is created in `users` in this path. + +### 5) Third-party signup (new social user) + +Source path: +- `saas/packages/fastify/src/supertokens/recipes/third-party-email-password/thirdPartySignInUp.ts` + +For new users (`createdNewUser = true`), writes: +1. SuperTokens auth user creation +2. `users` insert +3. SuperTokens role assignment insert(s) +4. optional `__account_users` insert (if account context exists) + +For existing users (`createdNewUser = false`): +- no new user rows; `users.last_login_at` is updated. + +## Multi-Database Note + +If an account has its own schema (`account.database`), the same logical writes happen in that schema context for: +- `users` +- `__account_users` +- `__account_invitations` + +If `account.database` is not set, these writes go to `public` schema. + +This is wired via request context (`dbSchema`) and account migrations in: +- `saas/packages/fastify/src/migrations/runAccountMigrations.ts` + +## Quick Verification Checklist + +When a brand-new user signs up for a new account, verify inserts in: +- SuperTokens auth table(s) +- `users` +- SuperTokens role mapping table(s) +- `__account_users` +- `__accounts` (main app self-signup path only) From 5522806c9887126d65b0c13470083181b63c60e5 Mon Sep 17 00:00:00 2001 From: premsgr77 Date: Mon, 18 May 2026 15:35:56 +0545 Subject: [PATCH 2/2] docs(fastify): colocate flow docs and add reset password guide Move user creation documentation under the fastify package and document admin vs tenant password reset behavior alongside it. --- packages/fastify/docs/reset_password_flow.md | 139 ++++++++++++++++++ .../fastify/docs/user_creation_flow.md | 0 2 files changed, 139 insertions(+) create mode 100644 packages/fastify/docs/reset_password_flow.md rename user_creation_flow.md => packages/fastify/docs/user_creation_flow.md (100%) diff --git a/packages/fastify/docs/reset_password_flow.md b/packages/fastify/docs/reset_password_flow.md new file mode 100644 index 0000000..5cf81d6 --- /dev/null +++ b/packages/fastify/docs/reset_password_flow.md @@ -0,0 +1,139 @@ +# Reset Password Flow + +This document explains how password reset works for **admin** users versus **tenant** (SaaS account) users in this monorepo, with the same assumptions as `user_creation_flow.md`: app profiles live in `users`, and SuperTokens owns credentials and reset tokens. + +Typical `saas` config shape (for context—see your app’s `ApiConfig`): + +- `saas.mainApp.domain` / `saas.mainApp.subdomain`: main self-service app host. +- `saas.apps`: optional extra apps (for example **admin**), each with `domain` / `subdomain`. +- `saas.subdomains`: how hostnames are interpreted (`required` implies real account hosts are expected). +- `saas.multiDatabase.mode` and `saas.multiDatabase.migrations.path`: whether accounts can use a dedicated DB schema and where account migrations live. + +## Components Involved + +- `saas/packages/fastify`: account discovery hook, SuperTokens recipe overrides for email/password. +- `@prefabs.tech/fastify-user`: email sending helpers, reset link path helpers. +- SuperTokens `thirdpartyemailpassword`: token generation, password update, internal user id. + +## Request Context Before `/auth/*` + +`accountDiscoveryPlugin` runs on every request unless the host matches a configured **`saas.apps`** entry (for example the admin UI), in which case it **returns immediately** and does **not** set `request.account`, `request.dbSchema`, or `request.authEmailPrefix`. + +For other hosts, discovery runs. Defaults in `saas/packages/fastify/src/config.ts` include **`/^\/auth\//`** in `excludeRoutePatterns`: + +- **Main app host** (`hostname === saas.mainApp.domain`): if the URL matches an excluded pattern (including `/auth/...`), `discoverAccount` **returns no account** (header-based lookup is skipped in that branch). So `request.account` / `request.authEmailPrefix` are usually **unset** on SuperTokens routes under `/auth/` for the main app host. +- **Tenant account host** (custom domain or account subdomain): account is resolved **by hostname** even for `/auth/...` routes, so `request.account` and (when applicable) `request.authEmailPrefix` and `request.dbSchema` are set as below. + +When an account is resolved and `account.database` is set (tenant schema name, for example `s_xxxxxxxx`), the plugin sets: + +- `request.dbSchema = account.database` + +When the account has a `slug`, it sets: + +- `request.authEmailPrefix = "${account.id}_"` (SuperTokens “logical” email uses this prefix; see `user_creation_flow.md`). + +## High-Level Flow (Admin vs Tenant) + +```mermaid +flowchart TD + subgraph Admin app host + A1[Request to admin app domain] --> A2[Plugin skips discovery] + A2 --> A3[No request.account / dbSchema / authEmailPrefix] + A3 --> A4[SuperTokens reset APIs use plain email in ST] + end + + subgraph Tenant account host + T1[Request to account hostname] --> T2[discoverAccount by hostname] + T2 --> T3[request.account set] + T3 --> T4{account.database?} + T4 -->|Yes| T5[request.dbSchema = database name] + T4 -->|No| T6[request.dbSchema unset] + T5 --> T7{account.slug?} + T6 --> T7 + T7 -->|Yes| T8[request.authEmailPrefix = id + _] + T7 -->|No| T9[No email prefix] + T8 --> T10[SuperTokens reset APIs use prefixed email] + T9 --> T10 + end +``` + +## End-to-End Reset Sequence + +```mermaid +sequenceDiagram + participant U as User / Browser + participant API as Fastify + ST middleware + participant ST as SuperTokens + participant Mail as Mailer + + U->>API: POST generate reset token (email) + Note over API: generatePasswordResetTokenPOST copies
request.account, request.authEmailPrefix + API->>API: Prefix email field for ST if authEmailPrefix set + API->>ST: Create password reset token for prefixed email + ST-->>API: OK + API->>Mail: sendPasswordResetEmail (custom link + display email) + Note over Mail: Link host from appId / Referer / Origin / Hostname
Recipient: email with prefix stripped + Mail-->>U: Reset email + + U->>API: POST reset password (token + new password) + Note over API: resetPasswordUsingToken uses request.account
and request.authEmailPrefix for getUserById context + API->>ST: Update password / consume token + ST-->>API: OK + userId + API->>Mail: Optional reset-password-notification email +``` + +## Detailed Steps + +### 1) Request reset token (`generatePasswordResetTokenPOST`) + +Source path: `saas/packages/fastify/src/supertokens/recipes/third-party-email-password/generatePasswordResetTokenPost.ts` + +- Copies `input.userContext.account` and `input.userContext.authEmailPrefix` from the Fastify request (`input.options.req.original`). +- Applies `updateFields` so the **`email` form field** is prefixed with `authEmailPrefix` when the tenant uses slug-based isolation (matches signup/signin behavior). +- Delegates to SuperTokens to issue the token (stored in SuperTokens tables, not in `users`). + +### 2) Deliver email (`sendPasswordResetEmail`) + +Source path: `saas/packages/fastify/src/supertokens/recipes/third-party-email-password/sendPasswordResetEmail.ts` + +- Rewrites SuperTokens’ default reset URL to a host derived from: + - `?appId=` (resolved to `config.apps`), or else + - `Referer` / `Origin` / `hostname`. +- Sends the message to the **human-readable** address using `Email.removePrefix` with `userContext.authEmailPrefix`. + +### 3) Complete reset (`resetPasswordUsingToken`) + +Source path: `saas/packages/fastify/src/supertokens/recipes/third-party-email-password/resetPasswordUsingToken.ts` + +- After SuperTokens reports `OK`, loads the user again with `getUserById` and passes through `request.account` and `request.authEmailPrefix` so **display email** matches other recipe overrides. +- Sends a **“password changed”** notification email when configured (`resetPasswordNotification` templates / subjects in `fastify.config.user.emailOverrides`). + +### 4) `getUserById` display email + +Source path: `saas/packages/fastify/src/supertokens/recipes/third-party-email-password/getUserById.ts` + +- Strips `authEmailPrefix` from emails returned to callers when a prefix is in context. + +## Multi-Database (`account.database`) + +- Tenant schema selection for **app tables** (`users`, `__account_users`, etc.) follows `request.dbSchema` / `account.database`, as in `user_creation_flow.md` and `runAccountMigrations`. +- The **password reset token and password hash** remain in SuperTokens’ store; resetting a password does not migrate rows in `users` by itself. +- Consistency matters: signup/signin and reset must agree on whether the SuperTokens email is **prefixed** (`authEmailPrefix`) for that account. Tenants resolved **by hostname** get that context on `/auth/*` routes; admin-app traffic does not. + +## Source File Index + +| Concern | Location | +|--------|----------| +| Account discovery, `dbSchema`, `authEmailPrefix` | `saas/packages/fastify/src/plugins/accountDiscoveryPlugin.ts` | +| Main vs header vs hostname discovery | `saas/packages/fastify/src/lib/discoverAccount.ts` | +| Default excluded routes (`/auth/`, etc.) | `saas/packages/fastify/src/config.ts` | +| Reset token API override | `.../third-party-email-password/generatePasswordResetTokenPost.ts` | +| Email delivery override | `.../third-party-email-password/sendPasswordResetEmail.ts` | +| Consume token + notification | `.../third-party-email-password/resetPasswordUsingToken.ts` | +| Recipe wiring | `saas/packages/fastify/src/supertokens/recipes/index.ts` | + +## Quick Verification Checklist + +- **Tenant user** (account host, slug): submit reset with unprefixed email in the form; SuperTokens receives prefixed email; email arrives unprefixed; completing reset succeeds when the browser/API uses the **same host** context as token generation. +- **Admin user** (dedicated `saas.apps` domain): no tenant `account` on the request; SuperTokens email has **no** tenant prefix—matches how that user was provisioned. +- **Multi-database**: after reset, sign-in still loads profile via `users` using the tenant schema implied by `account.database` during session creation (see `createNewSession` and sign-in recipes)—password change alone should not blank profile data. diff --git a/user_creation_flow.md b/packages/fastify/docs/user_creation_flow.md similarity index 100% rename from user_creation_flow.md rename to packages/fastify/docs/user_creation_flow.md