Proxy
GoTrue (the auth server behind Supabase Auth) is normally exposed directly to browsers. ezz can front it instead: GoTrue stays on an internal address, your service is the only public door, and the auth endpoints become typed routes that appear in your OpenAPI document alongside everything else.
You get one object that does both jobs. It validates the tokens GoTrue issues, and it proxies the endpoints clients need.
Set it up
gt := auth.NewGoTrue(auth.GoTrueConfig{
InternalURL: "http://gotrue:9999", // never publicly routable
JWTSecret: os.Getenv("GOTRUE_JWT_SECRET"), // the same secret GoTrue signs with
AuditLogger: auth.NewSlogAuditLogger(nil),
})
app := ezz.New().Auth(gt.AuthProvider())
gt.Mount(app, "/auth")
That is the whole integration. Clients now call POST /auth/token instead of GoTrue's POST /token, and your protected routes accept the resulting bearer token.
Configuration
| Field | Required | Purpose |
|---|---|---|
InternalURL | yes | Absolute http or https URL of the GoTrue server |
JWTSecret | yes | Shared with GoTrue, used to validate incoming tokens |
AuditLogger | no | Records proxied calls; see Audit logging |
RateLimiter | no | Throttles sensitive endpoints; nil disables it |
ServerHeaders | no | Headers added to every outbound request, such as apikey |
HTTPClient | no | Custom client; defaults to a 30 second timeout |
TrustedProxies | no | CIDRs or IPs of proxies in front of you; empty means edge mode |
NewGoTrue panics on an invalid InternalURL or a malformed TrustedProxies entry, so a wiring mistake fails at startup rather than on the first login.
What the two halves do
gt.AuthProvider() returns a *auth.JWTAuthProvider backed by GoTrue's claim layout. It is the value you hand to app.Auth(...), and it takes the same options as any other JWT provider:
app := ezz.New().Auth(
gt.AuthProvider().WithResolver(auth.Cached(myResolver{db: db}, 5*time.Minute)),
)
gt.Mount(app, prefix) registers the proxied routes under the prefix. Every route is Public() at the ezz layer, because GoTrue does its own bearer-token checking on the endpoints that need it. The full list is on Endpoints.
What happens on a proxied request
For a typed JSON route such as POST /auth/token:
- Bind and validate. The request struct is bound and validated like any other ezz route, so a body missing
emailfails with400before GoTrue is contacted. - Rate limit. On sensitive routes, the configured limiter is consulted, keyed by client IP and route. Over the limit means
429. - Build the outbound request. The validated request is re-marshalled as JSON, the query string is carried over,
ServerHeadersare applied, the client'sAuthorizationheader is forwarded verbatim, andX-Forwarded-For,X-Real-IP,X-Forwarded-Proto, andX-Forwarded-Hostare set from the resolved client IP. - Forward and pass through. On success, GoTrue's status,
Content-Type, and body are returned byte for byte. - Normalize errors. A
4xxor5xxfrom GoTrue is rewritten into ezz's error shape. - Audit. Success or failure is recorded, when an audit logger is configured.
The response types exist for documentation, not for filtering. Because the body is passed through unchanged, fields ezz does not model still reach the client, so a GoTrue upgrade that adds a field does not require a change here.
Error responses
GoTrue error bodies are folded into the standard shape, keeping the original as details:
{
"error": true,
"message": "Invalid login credentials",
"status": 400,
"details": {
"error_code": "invalid_credentials",
"msg": "Invalid login credentials"
}
}
The message is taken from the first of msg, error_description, error, or message present in GoTrue's body, falling back to the standard status text.
If GoTrue cannot be reached at all, the response is:
{
"error": true,
"message": "auth service unavailable",
"status": 502
}
Reach GoTrue's protected endpoints
Endpoints like GET /auth/user need the caller's token. Because the Authorization header is forwarded verbatim, the client just sends it as usual:
TOKEN=$(curl -s localhost:8080/auth/token?grant_type=password \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"password123"}' | jq -r .access_token)
curl -s localhost:8080/auth/user -H "Authorization: Bearer $TOKEN"
Add a service key
If your GoTrue deployment expects an apikey header or another shared header, set it once:
gt := auth.NewGoTrue(auth.GoTrueConfig{
InternalURL: "http://gotrue:9999",
JWTSecret: secret,
ServerHeaders: map[string]string{
"apikey": os.Getenv("SUPABASE_ANON_KEY"),
},
})
These headers are added to typed forwards and to the raw redirect routes, and they never appear in responses.
Tune the HTTP client
The default client has a 30 second timeout and the standard transport. For a busy service, give it a connection pool sized for your traffic:
gt := auth.NewGoTrue(auth.GoTrueConfig{
InternalURL: "http://gotrue:9999",
JWTSecret: secret,
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
},
})
Mount under a different prefix
The prefix is yours to choose, and a trailing slash is trimmed:
gt.Mount(app, "/api/auth")
Whatever you choose has to match GoTrue's API_EXTERNAL_URL, because GoTrue builds email links and OAuth metadata from it. See Deployment.