Skip to main content
The dashboard uses email + password sign-in with optional email-OTP two-factor authentication. There is no public sign-up — accounts are created by the owner.

Creating the owner account

On first launch, visit /setup to create the owner account with an email and password. Once an owner exists, the setup page closes and /login becomes the entry point.
There is no self-service sign-up. The public /api/auth/sign-up/email route is blocked; only the owner-bootstrap and invite flows create accounts.

Two-factor authentication (email OTP)

Two-factor is opt-in per user and strongly recommended. Enable it under Settings → Security, or during onboarding at the Security step. When 2FA is on, each sign-in is:
  1. Enter email + password.
  2. A 6-digit code is emailed to you (via Resend).
  3. Enter the code to finish signing in.
Backup codes are shown when you enable 2FA — store them somewhere safe.

Inviting team members

From Settings → Team, the owner invites teammates by email and role. Each invite:
  • Creates an account with a temporary password (emailed to them).
  • Forces a password reset on first login before they can use the dashboard.

Password reset

Users who forget their password use Forgot password? on the login page (/forgot-password) to receive an emailed reset link.

Email delivery

Sign-in codes, invites, and reset links are sent through the Resend integration (configure it under Integrations → Resend). If Resend isn’t configured, these messages are printed to the server logs instead — useful for local development:

Rate limiting

Auth endpoints are rate-limited by better-auth (e.g. sign-in is capped at 3 attempts per 10 seconds, password reset at 3 per minute), and 2FA codes are invalidated after 5 wrong attempts. Counters are stored in the database (the rate_limit table) so the limits hold across serverless instances — the in-memory default would reset on every cold start. Rate limiting is active in production automatically; no configuration is needed.

Demo mode

Set IS_DEMO_SITE=true to run a public, throwaway demo. Never enable this on a real deployment. In demo mode:
  • A demo merchant is seeded automatically — [email protected] / 123456 — along with sample products, payment links, and transactions, so the dashboard looks populated. You can also seed manually with pnpm db:seed:demo.
  • The login page pre-fills the demo credentials and accepts a fixed confirmation code (123456) instead of real 2FA. No email is sent.
  • Payout wallets are fixed and locked. They’re defined in src/lib/auth/demo.ts (DEMO_PAYOUTS); the settings API rejects changes to them and the UI renders the fields read-only.
Demo mode accepts a publicly known password and confirmation code. Only use it for disposable demo environments — never with real funds or a production wallet seed.