Deployment
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
| Setting | Value | Why |
|---|---|---|
GOTRUE_JWT_SECRET | Same string as GoTrueConfig.JWTSecret | ezz validates the tokens GoTrue signs |
API_EXTERNAL_URL | Public base URL plus mount prefix, for example https://api.example.com/auth | Email links, OAuth metadata, and the issuer resolve back through ezz |
GOTRUE_SECURITY_SB_FORWARDED_FOR_ENABLED | true | GoTrue reads the forwarded client IP instead of your service's address |
GOTRUE_WEBAUTHN_RP_ORIGINS | Public ezz origin, for example https://api.example.com | The browser performs WebAuthn against your domain |
GOTRUE_WEBAUTHN_RP_ID | Public hostname | Must match the origin the browser sees |
GOTRUE_SITE_URL | Your frontend URL | Where users land after clicking an email link |
Feature switches worth being deliberate about:
| Setting | Effect |
|---|---|
GOTRUE_OAUTH_SERVER_ENABLED | Turns the OAuth2 server endpoints on |
GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION | Lets anyone register an OAuth client; leave off unless you need it |
GOTRUE_PASSKEY_ENABLED | Turns passkey endpoints on |
GOTRUE_MAILER_AUTOCONFIRM | Skips 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:
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:
cd examples/gotrue
go run main.go
# 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:
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
- Roles and permissions for authorization once GoTrue has identified the user.
- OpenAPI for how proxied routes appear in your documentation.