···114114}
115115```
116116117117-### Composition: "Standalone Mode"
117117+## Production Best Practices
118118+119119+### Enable Compression
118120119119-To act as a self-contained Authentication Server and Gate in one route, simply compose both directives.
121121+To ensure the Login and Forbidden HTML pages (which include inline CSS and SVGs) are delivered as quickly as possible, enable Gzip and Zstd compression in your Caddyfile. This reduces the transfer size significantly.
120122121123```caddyfile
122124app.example.com {
123123- route {
124124- atproto_portal {
125125- domain app.example.com
126126- # Optional: move auth paths to /auth/...
127127- path_prefix /auth
128128- }
125125+ # Enable compression for all responses (including auth pages)
126126+ encode zstd gzip
129127130130- atproto_gate {
131131- # Redirect to local portal (respecting prefix)
132132- portal_url /auth
133133-134134- # Enable refresh
135135- client_id https://app.example.com/.well-known/oauth-client-metadata.json
136136-137137- allow @alice.bsky.social
138138- }
139139-128128+ route {
129129+ atproto_portal { ... }
130130+ atproto_gate { ... }
140131 reverse_proxy localhost:8080
141132 }
142133}
143134```
135135+136136+### Localhost Development
137137+138138+The AT Protocol OAuth flow requires the Authentication Server (PDS) to fetch client metadata from your application. If you are running Caddy on `localhost`:
139139+140140+1. **Issue**: Production PDS instances (like `bsky.social`) **cannot reach** `http://localhost`.
141141+2. **Symptom**: You will see an `invalid_client` error during login.
142142+3. **Fix**: Expose your local server to the internet using a tunnel (e.g., `ngrok`, `cloudflared`, or `tailscale funnel`) and set your `domain` config to that public URL.
144143145144## Documentation
146145