Guides/Set up sign-in
Go-live checklist
Work through this list before real people use your app. Each line names the value to check and where it's explained. Everything here was set up on your computer with development values; production needs its own.
Don't
Never copy your .env file to a server. Development values are weak on purpose (a public database password, a throwaway encryption key) and some are refused in production. Create every production value fresh, in your hosting provider's secret settings.
1. The basics#
-
APP_ENV=production. It turns on JSON logs and HSTS, refuses Mailpit, and makes the settings below required. The Docker image sets it for you. -
APP_ADDR=0.0.0.0:8080when the app runs in a container behind a load balancer. The Docker image sets it for you. The default127.0.0.1:8080only accepts connections from the same machine. -
DATABASE_URLpoints at your production PostgreSQL, withsslmode=require(or stricter) and a strong password. Store it as a secret, or mount it as a file and setDATABASE_URL_FILE. - Migrations run before each new version starts:
docker run --entrypoint /migrate <image>with the same environment, orgo run ./cmd/migrate. The app never migrates itself. -
APP_CORS_ORIGINSlists your web frontends, such ashttps://app.example.com. Empty means browsers on other sites can't call the API. - Decide on
APP_DOCS_ENABLED./docsis useful for your frontend team; set it tofalseif your API isn't meant to be public.
2. Encryption key#
- A new
AUTH_ENCRYPTION_KEYS, not the one from your.env. Without it the app refuses to start in production. How to generate it. - A copy of the key saved in your password manager.
3. Email#
- An email provider:
RESEND_API_KEYfor Resend, orSMTP_HOST,SMTP_PORT,SMTP_TLS,SMTP_USERNAMEandSMTP_PASSWORDfor SMTP. Email sending. - Your sending domain verified at the provider (SPF and DKIM records added).
- The sender set with
PUT /ops/settings/mail.from_emailto an address on that domain. The defaultno-reply@example.comlogs a warning in production and most providers refuse it. - A test email received:
POST /ops/mail/testwith{"to": "you@yourdomain.com"}.
4. Passkeys#
-
WEBAUTHN_RP_IDset to your domain, such asexample.com. Empty turns passkeys off in production. Passkeys. -
WEBAUTHN_ORIGINSlists your https frontends, and the same addresses are inAPP_CORS_ORIGINS. - If you have mobile apps:
WEBAUTHN_APPLE_APP_IDSandWEBAUTHN_ANDROID_APPSset, andhttps://<your domain>/.well-known/apple-app-site-associationand/.well-known/assetlinks.jsonreachable. Passkeys in mobile apps.
5. Google and Apple#
-
APP_PUBLIC_URLis your API's https address, such ashttps://api.example.com, if you use either. - Google: production redirect URI
https://api.example.com/v1/auth/google/callbackadded to the web client; consent screen published (in Testing, only listed test users can sign in);GOOGLE_CLIENT_SECRETstored as a secret. Google sign-in. - Apple: production domain and return URL
https://api.example.com/v1/auth/apple/callbackon the Services ID; notification endpointhttps://api.example.com/v1/auth/apple/notificationson the App ID; the.p8key stored as a secret file; your sending domain registered for Apple's private email relay. Apple sign-in.
6. Your first administrator#
Seed data only exists in development: cmd/seed refuses to run in production. Create the first administrator by hand:
- Sign up through your frontend or
POST /v1/auth/register, and verify the email. - In the production environment, run
/api grant-role you@example.com platform_admin(orgo run ./cmd/api grant-role …from a machine with the production environment). - Sign in and turn on an authenticator app (
POST /v1/auth/mfa/totp, thenPOST /v1/auth/mfa/totp/confirm)./opsanswers 403mfa_requireduntil you do.
7. Behind a load balancer or proxy#
- The load balancer checks
GET /readyz(ready to receive traffic) andGET /livez(the process is alive). - Sign-in requests are rate-limited per client IP address (60 a minute), read from the connection. Behind a proxy every request comes from the proxy's address, so all users share one limit: add middleware that trusts your proxy's forwarded address before the rate limiter in
internal/app/routes.go(the comment onauthLimitKeymarks the spot). A generated app doesn't include one, because which header to trust depends on your proxy. - Rate limits are per instance: with 3 instances, one address can make up to 3 × 60 requests a minute.
- Deploys allow about 30 seconds for a clean stop: the app waits 5 seconds for the load balancer to notice, then up to 25 seconds for requests and jobs to finish.
8. Final check#
In the production environment, run:
terminal
/api auth-providers
Every sign-in method you expect shows ✓. Then sign in as a normal user and as the administrator from your real frontend.
Was this page useful?
Open an issue