End-user sign-in for your app
Your app's own users — customers, members, agents — signing into the app you built, not into Waymaker. They never need a Waymaker account.
This is separate from the Waymaker login you use to build. If your app's users are your colleagues, you want Waymaker sign-in instead.
Turn it on
waymaker host db auth provision <app-id> # installs the sign-in tables
waymaker host db auth deploy <app-id> # turns it on, returns the sign-in address
Order matters, and not in a way you can guess. deploy is what creates the authenticated
database role that your row-level security policies grant to. Any migration referencing that role
fails if you run it first. So the sequence for an app with RLS is:
db create → db connect → auth provision → auth deploy → db migrate → apps deploy
deploy prints the sign-in address. It is not guessable — it carries an organisation-scoped
suffix — so read it from the output rather than constructing it. Health check is
<sign-in-address>/api/auth/ok. (/.well-known/jwks.json returns 404 by design; that is not a
broken deploy.)
Both commands are safe to re-run. If deploy reports that the key endpoint has not propagated,
wait a minute and run it again — it is idempotent.
What your app talks to
Everything mounts under /api/auth on your sign-in address:
| Route | Purpose |
|---|---|
POST /api/auth/sign-up/email | Create an account |
POST /api/auth/sign-in/email | Sign in |
POST /api/auth/sign-out | Sign out |
GET /api/auth/token | Exchange the session for a JWT your database trusts |
GET /api/auth/ok | Health |
The JWT carries sub (the user id) and role. Your RLS policies read sub; the Data API reads
role.
Two-factor sign-in
The second factor is available on every app — your users can enrol whenever you give them the UI for it.
POST /api/auth/two-factor/enable # returns the QR/secret; user confirms with a code
POST /api/auth/two-factor/verify-totp # the second step of sign-in
POST /api/auth/two-factor/generate-backup-codes
POST /api/auth/two-factor/disable
Ten single-use backup codes are issued at enrolment and stored encrypted. Show them once, tell the user to keep them, and mean it — they are the difference between a lost phone being self-service and being a support call.
When a user with two-factor enabled signs in, the response asks for the second factor instead of returning a session. There is no half-signed-in state to handle: no session exists until the second factor is verified.
Requiring it for some roles
waymaker host db auth policy <app-id> --require finance,ceo
waymaker host db auth deploy <app-id> # the policy applies on deploy, not on save
Matched case-insensitively against the role column on your user table. Your app owns those
values.
This is enforced in the token, not in your UI. A user whose role requires a second factor and
has not enrolled is issued a token with role: "anonymous" and mfa_required: true — so
row-level security and the Data API refuse them even if your interface would have let them
through. Send them to enrolment when you see mfa_required.
Roles you do not list are unaffected. Setting no roles requires it of nobody, which is the default.
A saved policy does nothing until you redeploy. The sign-in service reads it at deploy time. The CLI warns you; it is worth believing.
When a user loses their phone
waymaker host db auth reset-user <app-id> --email them@example.com
Clears their second factor, signs out every session they have, and records who did it. They enrol again on next sign-in — and if their role requires it, they have no data access until they do.
Sessions are ended deliberately: if the account was compromised, leaving them alive would let the attacker outlive the reset.
Keeping data away from your app's users
After auth deploy, every table and view you create in the public schema is automatically
readable and writable by signed-in users of your app. That is a standing rule, not a one-off
grant, and it applies to tables created long afterwards.
A REVOKE on a single table does not survive. It is undone by replacing a view, by enabling the
Data API, or by a restore — silently, with no error.
For anything your app's users must never read, use a separate schema:
CREATE SCHEMA private_finance;
-- no GRANT USAGE to authenticated, so nothing inside is reachable
CREATE VIEW private_finance.true_wage_cost AS SELECT …;
Per-table revokes are not durable. Schema separation is.
Checking it works
waymaker host doctor <app-id>
Reports whether sign-in is live, its address, and whether the sign-in schema is current. If it
says the schema is out of date, re-run auth provision — it is additive and safe.
Turning it off
waymaker host db auth disable <app-id> --confirm
Removes the sign-in service. Accounts are preserved — deleting user data is a separate, deliberate act. Everyone signed in is signed out.