GoTrue

Deployment

Configure GoTrue to sit behind ezz, run it locally with Docker, and refresh the golden fixtures.

Once ezz is the public entry point, GoTrue has to be told about it. GoTrue builds email links, OAuth metadata, and WebAuthn challenges from its own configuration, and those have to point at your service rather than at GoTrue.

Settings that have to match

SettingValueWhy
GOTRUE_JWT_SECRETSame string as GoTrueConfig.JWTSecretezz validates the tokens GoTrue signs
API_EXTERNAL_URLPublic base URL plus mount prefix, for example https://api.example.com/authEmail links, OAuth metadata, and the issuer resolve back through ezz
GOTRUE_SECURITY_SB_FORWARDED_FOR_ENABLEDtrueGoTrue reads the forwarded client IP instead of your service's address
GOTRUE_WEBAUTHN_RP_ORIGINSPublic ezz origin, for example https://api.example.comThe browser performs WebAuthn against your domain
GOTRUE_WEBAUTHN_RP_IDPublic hostnameMust match the origin the browser sees
GOTRUE_SITE_URLYour frontend URLWhere users land after clicking an email link

Feature switches worth being deliberate about:

SettingEffect
GOTRUE_OAUTH_SERVER_ENABLEDTurns the OAuth2 server endpoints on
GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATIONLets anyone register an OAuth client; leave off unless you need it
GOTRUE_PASSKEY_ENABLEDTurns passkey endpoints on
GOTRUE_MAILER_AUTOCONFIRMSkips email confirmation; development only

The proxy mounts OAuth and passkey routes whether or not the features are enabled. GoTrue is the source of truth and answers with its own error when something is off, so there is no second switch to keep in sync.

Keep GoTrue off the internet

The point of the proxy is that InternalURL is unreachable from outside. In practice that means one of:

  • A private network or VPC with no public ingress to the GoTrue service.
  • A Docker or Kubernetes network where only your service can resolve the hostname.
  • A firewall rule limiting GoTrue's port to your service's address.

If GoTrue is still publicly reachable, clients can bypass your rate limits, your audit trail, and any policy you add in front of the proxied routes.

Run it locally

The repository has a working Compose setup in examples/gotrue with Postgres and supabase/gotrue, and Make targets to drive it:

Terminal
make gotrue-up      # start Postgres and GoTrue, wait for both to report healthy
make gotrue-down    # stop them and remove volumes

gotrue-up runs docker compose against examples/gotrue/docker-compose.yml with examples/gotrue/.env, so put your local values there. The example environment enables the OAuth2 server, dynamic registration, passkeys, forwarded-for support, and mailer autoconfirm, which is a reasonable development posture and not one to copy to production.

With the services up, run the example app:

Terminal
cd examples/gotrue
go run main.go
Terminal
# sign up through the proxy, not through GoTrue
curl -s localhost:8080/auth/signup \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com","password":"password123"}'

# call a protected route with the resulting token
curl -s localhost:8080/dashboard -H "Authorization: Bearer $TOKEN"

Refresh the golden fixtures

The typed request and response structs are written against captured GoTrue responses, stored as JSON envelopes in auth/testdata/gotrue:

{
  "request": { "email": "capture@example.com", "password": "..." },
  "status": 200,
  "response": { "access_token": "...", "token_type": "bearer" }
}

Tests read these instead of talking to a live server, so the suite runs offline and a GoTrue upgrade shows up as a diff in the fixtures.

To re-capture against a newer GoTrue:

Terminal
make gotrue-up
make gotrue-capture
make gotrue-down
git diff auth/testdata/gotrue

gotrue-capture runs a build-tagged harness (go run -tags capture ./auth/internal/gotruecapture/) that waits for GoTrue's health endpoint, exercises sign-up, token, user, OTP, recovery, logout, OAuth metadata, JWKS, client registration, and passkey options, and writes one file per endpoint. A failed call is written as an error envelope rather than silently skipped, so a broken capture is visible in the diff.

If a fixture diff shows a new field, add it to the matching struct in auth/gotrue_endpoints.go, auth/gotrue_oauth.go, or auth/gotrue_passkey.go. Existing clients keep working either way, because response bodies are passed through unchanged and the structs only shape the documentation.

Production checklist

Share the JWT secret

GOTRUE_JWT_SECRET and GoTrueConfig.JWTSecret come from the same secret store. Rotating it invalidates every issued token.

Point API_EXTERNAL_URL at your prefix

Public base URL plus the prefix you passed to Mount. Getting this wrong produces email links and OAuth metadata that bypass or break your proxy.

List your load balancer ranges

Set TrustedProxies so the client IP is real, and enable GOTRUE_SECURITY_SB_FORWARDED_FOR_ENABLED in GoTrue. See Client IP and rate limits.

Add a rate limiter

Sign-in and sign-up are the endpoints attackers care about. A shared Redis limiter covers all replicas.

Turn on audit logging

Set AuditLogger on both the proxy config and the auth provider so proxied calls and token failures land in the same stream.

Close what you do not use

Disable dynamic client registration, disable passkeys or the OAuth server if unused, and turn off GOTRUE_MAILER_AUTOCONFIRM.

Size the HTTP client

The default has a 30 second timeout and no tuned connection pool. Give it a transport that matches your traffic.

Next steps

Copyright © 2026