refactor(server): bound HTTP and pool, extract internal/config, split cmd/server master
Closes the p1 resilience gap in the backlog (2026-07-22-no-db-pool-limits-no-http-timeouts). The HTTP server was constructed with only Addr and Handler, so all four net/http timeouts were zero — which means "no deadline". A client that opened a connection and dribbled request headers held a goroutine and a file descriptor indefinitely; enough of them exhaust the process without a single complete request ever arriving. Separately, sql.Open was called with no pool configuration at all, and database/sql defaults MaxOpenConns to unlimited, so a traffic spike could open connections until PostgreSQL's max_connections was exhausted — locking out psql and the cmd/ maintenance tools along with the AppView. Both are now bounded and env-tunable. Query time is bounded server-side via statement_timeout injected into the DSN rather than per-query context deadlines: lib/pq does send a CancelRequest on context cancellation, but that path needs a second connection, races the query finishing, and does nothing if the client dies outright. Schema migrations deliberately run on a separate connection with statement_timeout stripped, since a CREATE INDEX killed halfway is worse than a slow one. main() was 1090 lines with 30 inline os.Getenv calls and no config struct. Configuration now lives in internal/config, which applies defaults, rejects malformed values, and enforces the requirements that differ between dev and production — reporting every problem at once so a misconfigured deployment is fixed in one pass instead of one restart per mistake. main() is now ~240 lines and returns an error rather than calling log.Fatal, which is what makes the deferred cleanup reachable: os.Exit skips defers, so a fatal call partway through startup abandoned the database pool and discarded the buffered OpenTelemetry spans describing the failure. Changes: - Set ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout (cmd/server/httpserver.go) - Configure the pool: MaxOpenConns, MaxIdleConns, ConnMaxLifetime, ConnMaxIdleTime (cmd/server/database.go) - Inject statement_timeout into the app DSN; strip it for migrations (internal/config/dsn.go) - Add internal/config with Load/Validate and per-subsystem config structs - Split cmd/server into main, wiring, routes, consumers, database, httpserver, jobs, health, pds - Embed goose migrations (internal/db/migrations/embed.go), removing the working-directory dependency and the Dockerfile's migrations COPY - Drain background work concurrently with the listener rather than after it, so a slow in-flight request cannot consume the whole shutdown budget and leave Jetstream cursors unflushed; drain on the listener-failure path too, and report shutdown failures through the exit code - Bound each background job cycle and recover per cycle rather than per goroutine, so neither a panic nor a hang can silently kill the job permanently; run a cycle at startup instead of waiting out the first tick - Resolve the OAuth session store once at boot and fail loudly if absent, rather than turning the cleanup job into a silent hourly no-op - Add a context timeout to the instance PDS login, which used http.DefaultClient and could hang the boot forever - Document the new HTTP_* and DB_* variables in the env examples Production now fails closed on OAUTH_SEAL_SECRET (base64, 32 bytes), CURSOR_SECRET (rejects the documented CHANGE_ME placeholder), JETSTREAM_FEEDS, APPVIEW_PUBLIC_URL and PDS_URL (must not be loopback), a non-DID INSTANCE_DID, SKIP_DID_WEB_VERIFICATION, and a zero value for any HTTP timeout. The current docker-compose.prod.yml and .env.prod.example satisfy all of these; a live .env.prod with a short CURSOR_SECRET or a malformed OAUTH_SEAL_SECRET will refuse to boot. Verified: make test-all green across all three stages, make fmt-check clean, go vet clean, race detector clean on the changed packages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>