···11-# Turnstile-Protected Railway PDS
22-33-This repo now includes two deployment helpers for running a Bluesky PDS behind a Turnstile-backed signup gate while keeping Railway as the host:
44-55-- `apps/pds-gatekeeper`: a small ATProto-compatible signup gatekeeper
66-- `apps/pds-edge-proxy`: a Caddy reverse proxy that exposes the PDS publicly and routes protected signup paths to the gatekeeper
77-88-## Service layout
99-1010-Create three Railway services from this repo or from your existing PDS repo setup:
1111-1212-1. `pds-origin`
1313- - Your existing Bluesky PDS template service
1414- - Internal only after rollout
1515- - Private network target: `http://pds-origin.railway.internal:3000`
1616-2. `pds-gatekeeper`
1717- - Dockerfile path: `apps/pds-gatekeeper/Dockerfile`
1818- - Public networking disabled
1919- - Attach a Railway volume mounted at `/data`
2020-3. `pds-edge-proxy`
2121- - Dockerfile path: `apps/pds-edge-proxy/Dockerfile`
2222- - Public networking enabled
2323- - Attach both your apex domain and wildcard domain
2424-2525-## Gatekeeper environment
2626-2727-Set these on `pds-gatekeeper`:
2828-2929-```env
3030-HOST=0.0.0.0
3131-PORT=8080
3232-PDS_BASE_URL=http://pds-origin.railway.internal:3000
3333-PDS_HOSTNAME=example.com
3434-TURNSTILE_SITE_KEY=your-turnstile-site-key
3535-TURNSTILE_SECRET_KEY=your-turnstile-secret-key
3636-TURNSTILE_EXPECTED_HOSTNAME=example.com
3737-TURNSTILE_EXPECTED_ACTION=signup
3838-GATEKEEPER_DB_PATH=/data/gatekeeper.sqlite
3939-GATEKEEPER_SIGNUP_CODE_TTL_SECONDS=300
4040-GATEKEEPER_ENABLE_SIGNUP_PROTECTION=true
4141-GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT=https://bsky.app
4242-GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS=https://bsky.app,https://your-app.example.com
4343-```
4444-4545-## Edge proxy environment
4646-4747-Set these on `pds-edge-proxy`:
4848-4949-```env
5050-PORT=8080
5151-PDS_BASE_URL=http://pds-origin.railway.internal:3000
5252-GATEKEEPER_BASE_URL=http://pds-gatekeeper.railway.internal:8080
5353-```
5454-5555-## Public routing behavior
5656-5757-The proxy routes these paths to the gatekeeper:
5858-5959-- `/xrpc/com.atproto.server.describeServer`
6060-- `/xrpc/com.atproto.server.createAccount`
6161-- `/gate/*`
6262-6363-Everything else is forwarded directly to the origin PDS, including normal XRPC traffic and websocket federation.
6464-6565-## Cloudflare and Railway DNS
6666-6767-Use your apex domain for the PDS host when possible, for example:
6868-6969-- PDS hostname: `example.com`
7070-- User handles: `alice.example.com`
7171-7272-On Railway:
7373-7474-1. Attach `example.com` to `pds-edge-proxy`
7575-2. Attach `*.example.com` to `pds-edge-proxy`
7676-3. Keep `_acme-challenge` verification records DNS-only
7777-7878-On Cloudflare:
7979-8080-1. Proxy the apex and wildcard CNAMEs through the orange cloud
8181-2. Enable Universal SSL
8282-3. Set SSL/TLS mode to `Full`
8383-8484-## Suggested Cloudflare rate limits
8585-8686-Use rate limiting rules as defense in depth, not as the primary signup gate.
8787-8888-Recommended starting points:
8989-9090-- `/gate/*`
9191- - Count by IP
9292- - 10 requests per minute
9393- - Action: Managed Challenge or block after repeated abuse
9494-- `/xrpc/com.atproto.server.createAccount`
9595- - Count by IP
9696- - 5 requests per 10 minutes
9797- - Action: block
9898-9999-Do not put a Cloudflare challenge directly in front of the XRPC signup endpoint as the main control. The client needs the ATProto-compatible `verificationCode` flow.
100100-101101-## Rollout checklist
102102-103103-1. Deploy `pds-gatekeeper`
104104-2. Deploy `pds-edge-proxy`
105105-3. Move the public custom domains from `pds-origin` to `pds-edge-proxy`
106106-4. Confirm `https://example.com/xrpc/_health` still works
107107-5. Confirm `GET /xrpc/com.atproto.server.describeServer` returns `phoneVerificationRequired: true`
108108-6. Confirm direct `createAccount` calls without `verificationCode` fail
109109-7. Confirm `/gate/signup` returns a code and a fresh signup succeeds
110110-8. Confirm reusing the same code fails
111111-9. Confirm websocket federation still works through the proxy
112112-113113-## Notes
114114-115115-- This phase intentionally protects signup only.
116116-- Login and 2FA overrides are out of scope here.
117117-- The gatekeeper stores one-time signup codes in SQLite on the attached Railway volume.