Skip to content

Authentication & sessions

FlowRunner signs you in with email and password, stores no server-side session record, and gates every API route behind a logged-in session. This page describes exactly what the code enforces today.

How sign-in works

Sign-in uses an Auth.js v5 Credentials provider. When you submit the login form, the server:

  1. Looks up the user by email in the users collection.
  2. Compares your password against the stored hash with bcrypt.compare.
  3. Issues a session only if the hash matches.

Passwords are never stored in plain text. Every hash is produced with bcrypt at cost factor 12 — both when you accept an invite and when you change your password. bcrypt is deliberately slow, so an attacker who steals the database still faces an expensive brute-force per password.

NOTE

A wrong email, a wrong password, and a rate-limited attempt all return the same "invalid credentials" result. The login form never reveals whether an email exists.

Brute-force lockout

Before any password check, each login attempt is counted against a per-email key:

ts
const rl = await rateLimitByKey(`login:${email.toLowerCase()}`, 8, 900_000);
if (!rl.ok) return null; // surfaces as "invalid credentials"

That is 8 attempts per 15-minute window, per email address. The 9th attempt in a window is rejected before bcrypt runs, so guessing is throttled and the expensive hash comparison is not used as a CPU-exhaustion lever. The limiter is a fixed-window counter backed by MongoDB with a TTL index, so it works correctly across serverless instances.

WARNING

The lockout keys on email only — not yet on client IP (there is a TODO to add IP once it is available in that callback). An attacker spreading guesses across many email addresses is not slowed by this control alone.

Stateless JWT sessions

Sessions use the jwt strategy. There is no session table — your session lives entirely in a cookie holding a JSON Web Token that Auth.js signs and verifies with the server-side secret AUTH_SECRET (a required environment variable; the server refuses to boot without it). The token carries your user id and, if set, your role, which the session callback copies onto session.user for route handlers to read.

WARNING

Because sessions are stateless, there is no server-side "log out everywhere" switch. A token stays valid until it expires (the Auth.js default lifetime, as no custom maxAge is set) or until AUTH_SECRET is rotated — which invalidates all sessions at once. Treat AUTH_SECRET as a high-value secret; see /deploy/env.

Cross-subdomain SSO falls out of this design: any FlowRunner app served on a sibling subdomain that shares the same AUTH_SECRET (and cookie domain) can verify the same session token without a shared session store, because the token is self-contained and self-verifying.

The edge route guard

Requests hit an edge-runtime middleware (matcher /api/:path*) that runs the authorized callback before any handler. Everything under /api requires a logged-in session except an explicit public allowlist:

Public namespaceWhy it is public
/api/authThe login/session endpoints themselves
/api/waitlist, /api/inviteJoining the waitlist and accepting an invite
/api/run, /api/openapiPer-flow auth is enforced by the flow itself
/api/formsPublic form submit (honeypot + rate limit)
/api/cronGated by CRON_SECRET
/api/healthLeaks nothing

This guard is defense-in-depth: every handler still runs its own auth() and ownership checks. The middleware is a backstop that keeps a route from being reachable if a handler ever forgets one — it does not replace per-route authorization.

Signup is invite-only

Self-signup is closed during early access. POST /api/auth/register always returns 403 and points you to the waitlist. Accounts are created only when you open a valid, unexpired, unused invite token: the accept endpoint sets your password (minimum 8 characters, hashed at cost 12), provisions a personal workspace, optionally adds you to an inviting workspace, and marks the token used so it cannot be replayed.

See also

FlowRunner — the no-code platform for small businesses.