# Sign-in for the app's users (https://lazyfrog.dev/docs/guide/sign-in-for-the-app-s-users)

{/* Generated by scripts/generate-docs.ts from services/gateway/src/guide.md. Do not edit. */}

Pick the lightest thing that does the job:
- **Only the owner or their staff** (an admin page, a stock list), or a small group for now (a
  club's members' page): protect the paths with `routing` `auth` (a username and password the
  platform hashes). No code and no user table. Leave the password out: the owner chooses it in the
  portal (Settings › Protected pages); a new one is in force on the preview and the live site at
  once. One shared password can't be taken from one person alone, and there's no "sign out": on a
  shared phone, say that anyone holding it can make changes. The router refuses changes to a
  protected page that come from another site; check `Origin` in the app's own handlers too. Put
  an owner page's API routes under its path (`/admin/api/…`), so one rule and one password cover
  both.
- **Customers of a shop or salon** rarely need accounts: a random manage link in their
  confirmation (`/booking/<token>`, 128 bits) lets them see or cancel their booking.
- **People without email** (a class of 12-year-olds, staff on a shared tablet): the owner prints a
  card per person with a random code (three words and a number, 28 bits or more); store only its
  hash, keep sessions in a table with a sign-out, and let reprinting a card end the old code.
  Limit failed codes per address generously: a whole classroom shares one school address. Schools
  with Microsoft 365 or Google Workspace accounts can use those instead (Better Auth has both).
- **Visitors with their own accounts**: use a maintained library or the framework's own
  generator, never password hashing or session logic you invent: the stack's own (Django's auth,
  Rails' authentication generator or Devise, Laravel's starter kits (Breeze before Laravel 12), Phoenix's `phx.gen.auth`, ASP.NET
  Identity, SvelteKit's `sv add better-auth` or `sv add lucia`, Auth.js, Nuxt's auth modules…)
  or, on the default Next.js stack, Better Auth (email and password, sessions in the project's PostgreSQL; put
  the tables its CLI's `generate --output migrations/0002_auth.sql` writes into a migration (the
  CLI is now the `auth` package: `npx auth@<the installed better-auth version> generate --config
  lib/auth.ts --output migrations/0002_auth.sql --yes`; `npx` fetches it outside the week-old rule,
  so pin that version); with Google only, set `emailAndPassword: { enabled: false }`; set `BETTER_AUTH_SECRET`
  for both environments, and mark it `build: true` for production: the publish rehearsal starts
  the app with build secrets only, and Better Auth refuses to start without it. Its sign-up limit
  (a few per 10 s per address) trips a school or an office: set
  `advanced: { ipAddress: { ipAddressHeaders: ["x-lazyfrog-client-ip"] } }`). Set the secret
  before anyone signs up: sessions signed without it end when it's set) or Auth.js. FastAPI: Argon2 password hashes
  (`pwdlib[argon2]`), sessions in a table, a random session id in the cookie.
- Cookies: `HttpOnly`, `Secure`, `SameSite=Lax` and host-only (no `Domain`, or the `__Host-`
  prefix): every site on `lazyfrog.app` shares that parent domain. A sign-in shared by entry
  hostnames of the user's own domain is the one exception (Custom domains).
- Generators that sign up by email link only (Phoenix 1.8's `phx.gen.auth`) can't register
  anyone while email isn't connected: add a password to registration until it is.
- Sign-in links and password resets go out through the owner's email provider; while `email
  status` says it isn't connected, offer password sign-in and tell the user there's no reset or
  verification yet, and where they connect one.
  Never link records to an account by its email address until that address is verified: anyone
  can sign up with someone else's.
- Google or GitHub sign-in: the user makes the OAuth app at the provider and gives you its client
  id and secret through `secrets request` (once for preview and once for production: tell them a
  second link comes). Register both callback addresses (preview and the live site). For Google,
  tell them in plain words: 1) create a project at console.cloud.google.com; 2) Google Auth
  Platform › Branding: app name, support email, home page and the live site's `/privacy`; Audience:
  External, then "Publish app" (in Testing only listed test users can sign in); don't upload a
  logo (that starts Google's brand review); 3) Clients: a "Web application" with both callback
  addresses (`https://<preview>/api/auth/callback/google` and the live one, the library's path);
  4) copy the client ID and secret into your link. The authorised domain is `lazyfrog.app` until
  they connect their own domain; with basic scopes and no logo Google doesn't ask to verify it,
  and a site meant to last is better on its own domain. Restricting sign-in to a list (a club's
  members): check Google's verified email against it at sign-up and at every sign-in; tell the
  owner members can make a Google account for their own address, and that people sharing an
  address need one each.
- Limit sign-in attempts per address in the app (a table of attempts is enough), and never log
  passwords, tokens or session ids.
- Accounts the app makes from a secret the owner sets (a demo or first admin from
  `DEMO_PASSWORD`; preview-only ones check `LF_ENV`, which is `preview` or `production`): your
  own test accounts don't prove those exist. Once the owner says they set
  it, check `service_status` (it names a secret set after the service started: restart it), then
  that the accounts are in the database, before you tell them to sign in (2026-11-05: an owner was
  sent to a sign-in that had no account behind it).
