SsuperslateDocs

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

RouteAccessIncluded behavior
/auth/loginpublicmagic link, password sign-in, optional Google
/auth/signuppublicpassword signup and verification
/auth/forgot-passwordpublicpassword-reset request
/auth/reset-passwordpublic tokenpassword replacement and session revocation
/auth/verifypublic tokenemail verification callback
/dashboardauthenticatedneutral starter state; replace it with the product home
/usersauthenticatedpaginated, filterable user table with a mobile card list
/organizationauthenticatedactive organization: members, invitations, and organization settings
/invitations/:tokenauthenticatedinvitation preview and acceptance
/users/:userIdauthenticatedone user's profile, account state, and linked sign-in providers
/settings/profileauthenticatedname, phone number, and optional public profile image
/settings/accountauthenticatedlinked provider state, Google linking when configured, and deletion
/settings/billingauthenticatedtrial, plans, checkout, portal, and provider-disabled states
/settings/notificationsauthenticatedemail and in-app preferences
/notificationsauthenticatedinbox, unread state, mark-read, and delete

Organizations and tenancy

Every account owns one personal organization, created by a database trigger when the user row is inserted, so a caller always has an acting organization. organizationContext resolves it from users.active_organization_id after session resolution and falls back to the personal organization when the stored one is missing or no longer a membership.

Product data is scoped to organization_id; personal account data — profile, profile image, notification preferences, sessions, and subscription — stays scoped to users.id. The /files library therefore belongs to the active organization: every member sees it, a member may delete a file they uploaded, and an admin or owner may delete any of them.

Roles are owner, admin, and member, and the whole policy is the can(role, permission) function in packages/contracts/src/organizations.ts. The server enforces it through requirePermission; GET /organizations/current returns the caller's permission list so the UI can hide affordances it must not offer. Postgres enforces exactly one owner per organization, so ownership moves only through POST /organizations/current/transfer-ownership.

A personal organization cannot be renamed, deleted, left, or given a second member. Deleting a team organization removes its members, invitations, and files, and returns every affected member to their personal organization. An organization never outlives its last member.

Invitations are single-use tokens delivered by email. Only the SHA-256 hash is stored, they expire after seven days, and acceptance requires the signed-in account's email to match the invited address. A revoked, expired, already-accepted, and unknown token all return the same not-found answer.

The inbox has three entry points sharing one NotificationInbox implementation: the navbar bell opens a popover on desktop and a bottom sheet on mobile for triage, its "View all" opens the full inbox in a modal, and /notifications renders the same inbox as a page for deep links, the command palette, and links sent by email. The bell is the only permanent affordance; notifications are an account-level utility and deliberately hold no primary sidebar slot.

Setting the optional VITE_MARKETING_URL links the brand lockup on every authentication screen to the product's public marketing site and adds a "Website" entry to the account menu. Both render without a link when it is unset, so a deployment with no separate marketing site is unchanged.

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 is a deployment-wide directory, not an organization roster: use GET /organizations/current/members for the members of the acting organization. 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.

Both surfaces also skip any user whose users.directory_visible is false. The column defaults to true, so every account appears in the directory unless you opt it out. Set it to false — per row, or by changing the column default on one deployment — for accounts that must use the product normally while staying out of the directory other users can see, such as visitors on a public demo instance. A hidden user keeps full access to their own GET /users/me profile and their own data; only the cross-user directory stops listing them.

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;
  • organizations, members, roles, and invitations work; the invitation link is readable in the local email log, so a second account can accept an invitation with no email provider configured;
  • 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

vp run --filter @app/web test
vp run --filter @app/web typecheck
vp run --filter @app/web build

Protected-route behavior, zero-key auth, 404 handling, protected API responses, and cross-origin rejection are covered by the server and contract suites rather than by a browser. Click through the changed surfaces with:

vp run --filter @app/web dev

Provider configuration still requires the provider-specific smoke paths.

On this page