Included product surfaces
Understand the shipped browser routes, settings flows, and zero-key product behavior.
Included product surfaces
The browser application is a neutral authenticated SaaS shell. It includes complete auth and settings paths plus a starter dashboard that must be replaced with the buyer's product outcome. React Router owns navigation, TanStack Query owns server state, and Better Auth owns the session.
Browser routes
| Route | Access | Included behavior |
|---|---|---|
/auth/login | public | magic link, password sign-in, optional Google |
/auth/signup | public | password signup and verification |
/auth/forgot-password | public | password-reset request |
/auth/reset-password | public token | password replacement and session revocation |
/auth/verify | public token | email verification callback |
/dashboard | authenticated | neutral starter state; replace it with the product home |
/users | authenticated | paginated, filterable user table with a mobile card list |
/users/:userId | authenticated | one user's profile, account state, and linked sign-in providers |
/settings/profile | authenticated | name, phone number, and optional public profile image |
/settings/account | authenticated | linked provider state, Google linking when configured, and deletion |
/settings/billing | authenticated | trial, plans, checkout, portal, and provider-disabled states |
/settings/notifications | authenticated | email and in-app preferences |
/notifications | authenticated | inbox, unread state, mark-read, and delete |
Every protected route passes through PrivateRoute. A missing session redirects to login; a session
resolution failure remains visible instead of rendering protected content from stale client state.
Profile and account ownership
GET /users/me returns the current user's public application profile. PUT /users/profile accepts
the shared UpdateProfileRequestSchema; the caller cannot choose another user ID. The profile-image
flow is separately entitlement-gated and provider-backed as described in
Object storage.
GET /users lists non-deleted users for the table, and GET /users/:user_id returns one user's
detail record for /users/:userId. Both require a session, return no credential material, and never
expose a deleted account. Add an authorization rule before exposing either surface to a role that
must not see the whole directory.
Email changes are not included. Add them only through a Better Auth-compatible flow with new-address verification, existing-session policy, notification, and recovery behavior. Do not mutate the email column through the generic profile update.
Account deletion uses DELETE /users/profile/account. It is blocked while a provider subscription
can renew or paid access remains. See Authentication for the terminal
deletion state and billing boundary.
Zero-key states
With only Postgres configured:
- password, verification, reset, and magic-link flows work through the local email log;
- Google controls are hidden;
- billing renders an actionable unavailable state without a checkout button;
- profile text fields work, while profile-image upload returns the documented storage-unavailable response;
- notifications work in-app when application events create them;
- the dashboard clearly identifies itself as starter content.
An unavailable optional provider is not an empty success. Keep the current disabled state visible or remove the complete subsystem using Removing subsystems.
Replacing the dashboard
Replace apps/web/src/modules/dashboard/pages/DashboardPage.tsx with the first product-owned route.
Keep the route lazy, use shared contracts and a service-owned TanStack Query hook, and cover
loading, empty, error, unauthorized, narrow, keyboard, and reduced-motion states in proportion to
the feature. The maintained implementation path is Build your first feature.
Verification
pnpm test:e2e
vp run --filter @app/web test
vp run --filter @app/web typecheck
vp run --filter @app/web buildThe browser suite verifies every protected route, zero-key auth, the main settings surfaces, 404 handling, protected API behavior, and cross-origin rejection. Provider configuration still requires the provider-specific smoke paths.