Skip to content

Security

The protections below are already on. This page says where each one lives, so you know what you have and what not to remove.

Protection Where
Every route needs a session unless it is marked @Public() Auth
A password works only once the address is verified Auth
Reset links work once, for an hour, and end every other session Auth
Asking for a reset answers the same whether or not the account exists Better Auth
Sign-in is limited per visitor address in production Auth
The session cookie is httpOnly, SameSite=Lax, and Secure on HTTPS Auth
Requests from an origin other than your web app are refused Auth
People see their signed-in devices and can sign the others out Profile settings in the web app
After sign-in, people are only ever sent to a page of your own app apps/web/src/features/auth/redirect.ts
Protection Where
A workspace someone is not in answers 404, the same as one that is not there Workspaces
Every query on workspace data filters by the workspace Workspaces
Roles are checked in the API, whatever the screen shows Workspaces
Paid features are checked in the API, whatever the screen shows Charge for a feature
The web app forgets everything it loaded when someone signs out queryClient.clear() in use-session-actions.ts
Admins are only the verified addresses in ADMIN_EMAILS, checked on every request Admin panel
Signing in as a customer lasts an hour, shows a banner, and is recorded Admin panel
Protection Where
Every request body is checked against its schema before your code runs Architecture
Billing events are only accepted with Stripe’s or Polar’s signature Billing
The assistant only takes the user’s messages; instructions are built on the server AI features
The assistant limits the size of what it is sent and what it answers AI features
The contact form has a trap for bots and takes 5 messages an hour per address Marketing site

The web and site images send these on every answer, from their Caddyfile:

Header What it does
Strict-Transport-Security: max-age=31536000 Browsers use HTTPS for the next year
X-Content-Type-Options: nosniff Files are only ever what they say they are
X-Frame-Options: DENY No other site can put your app in a frame
Referrer-Policy: strict-origin-when-cross-origin Other sites learn your domain, not the page

The Server header is removed. deploy:check fails if the first three are missing.

Logs never hold a cookie, a header or a query string, where sessions and sign-in tokens live. See Logs.

  • The API refuses to start in production with the example AUTH_SECRET, or one shorter than 32 characters.
  • .env and .env.railway are ignored by git and left out of every image.
  • The API has no public address in production. Browsers reach it through the web image, under /api.
  • The API image runs as a user without root.
  • The web and site images believe X-Forwarded-For only from your host’s private network, so a visitor cannot pretend to be another address to get around a limit. See Other hosts.
  • Backups. Your host provides them. Restore one once before you launch.
  • Two-factor sign-in and passkeys are not set up. Better Auth has plugins for both.
  • A Content-Security-Policy is not set, since it depends on the scripts your product loads. Add it to the header block in apps/web/Caddyfile.
  • Limits are counted per copy of the API, in memory. With several copies, each counts on its own.
  • A usage limit for the assistant. There is none per workspace. See AI features.
  • The inboxes in ADMIN_EMAILS are keys to every account. Protect them like one.