Skip to content

feat: Go foundation (server, database, services, JSON API, lint layering, CI, deploy, agent docs) - #1

Merged
jayleaton merged 13 commits into
mainfrom
feat/foundation
Oct 22, 2026
Merged

jayleaton merged 13 commits into
mainfrom
feat/foundation

Conversation

@jayleaton

@jayleaton jayleaton commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Problem

The Go successor to the Next.js template has no foundation yet. There is no server, no database layer, no layering enforcement, no CI and no agent docs, so feat/auth and feat/web-ui have nothing to plug into.

How

Same layers as the original, rebuilt in Go and enforced by lint:

packages/database (goose migrations → sqlc queries → UserRepository)
        ↓
apps/web/internal/services (UserService: 403-not-self, typed service errors)
        ↓
apps/web/internal/api (JSON envelope, Public/Authenticated wrapper)   apps/web/ui (feat/web-ui)
        ↑ mounted by apps/web/internal/server: /healthz · /api/auth/ (auth.Handler) · /api/core/v1 · / (UI)
  • Binary: server serve binds 0.0.0.0:PORT with slog logging, rejects cross-origin browser writes, and shuts down gracefully. server migrate runs auth migrations, then app migrations (goose_app).
  • Config: env is validated once at boot and every problem is reported together. AppRoutes/APIRoutes and Features live in apps/web/internal/config. There are no hardcoded origins.
  • Users API: GET/PATCH/DELETE /api/core/v1/users and PATCH /api/core/v1/users/{id}. Input types live in validate; each handler is about five lines.
  • Layering: depguard rules live in .golangci.yml. scripts/check-layering.sh proves they still reject forbidden imports.
  • Tests: service and handler tests use in-memory fakes. Repository tests run against real Postgres: one throwaway tmpfs server and a migrated template database cloned per test, so tests run in parallel.
  • Tooling: Makefile, compose (Postgres 18 + pgweb on 127.0.0.1), distroless Dockerfile, railway.json (pre-deploy /server migrate, healthcheck /healthz), one CI workflow (path filters, generated-code diff, golangci-lint, race tests, ci-ok as the single required check, merge_group, drafts skipped), plus AGENTS.md, CLAUDE.md, CONTEXT.md and README.md.

Evidence

All numbers are from local runs.

Docker image (distroless static) 5.1 MB compressed; binary 11.4 MB
Idle RSS, running container 13.1 MB VmRSS (cgroup 4.1 MiB), after 200 requests
Warm make ci (build, generated-code diff, lint, layering proof, race tests) 5.3 s; 6.2 s with the test cache cleared
Repository tests about 0.1 s each
GitHub Actions, cold cache (run 39733960845, attempt 1) 1m54s wall; go job 1m44s (build 23 s, tests 33 s)
GitHub Actions, warm cache (same run, attempt 2) about 45 s wall; go job 36 s, of which service container 10 s and setup-go cache restore 12 s
  • Fresh clone: make db-up && make migrate && make dev served /healthz and the users API with the dev auth fake. make lint test gave 0 issues and all packages passed.
  • Users API, end to end: PATCH rename, image set and clear → 200. PATCH another user → 403. Bad body → 400 with field details. DELETE → 200, then GET → 401.
  • Layering proof: all 6 probes are rejected. With the api/ui rule removed, 4 probes fail and the script exits 1.

Decisions to revisit

  • No email changes through UpdateUserSettings: the app may only change name and image, so an email field gets a 400. Email changes belong in the auth flows.
  • image: null clears the image. The original couldn't clear it once set.
  • Dev auth fake and the stand-in user table: until packages/auth lands, migrate creates user from a stub (packages/database/authstub). In development every request is signed in as a seeded dev user; outside development, protected routes return 401. A dev database migrated with the stub may need docker compose down -v once real auth lands.
  • Settings beyond the original list: PORT (Railway sets it), TRUSTED_ORIGINS and FROM_EMAIL.
  • Dev tools live in tools/go.mod, not the root go.mod. actionlint has its own tools/actionlint/go.mod, because its yaml dependency clashes with golangci-lint's.
  • make dev and make build don't regenerate code. sqlc needs cgo, and the generated code is committed; CI checks it's up to date.
  • No ENTRYPOINT in the image, so Railway's /server migrate override can't be prefixed with a second /server.

Wiring left for the merges

  • feat/auth (marked TODO(feat/auth) in the code):
    • Use auth.Migrate instead of authstub.Migrate, in apps/web/cmd/server (migrations) and in packages/database/dbtest/app.go.
    • Point the sqlc schema in packages/database/sqlc.yaml at ../auth/migrations.
    • Pass *auth.Auth as server.Deps.Auth, plus a ~6-line function that turns auth.UserFrom into an api.CallerFrom.
    • Delete apps/web/internal/server/devauth and packages/database/authstub.
  • feat/web-ui: pass the UI's http.Handler as server.Deps.UI; it is mounted at /. Add templ as a tool in the root go.mod. make generate and CI already run templ generate once apps/web/ui exists.

Merge Danger

Door: two-way. Nothing is deployed, and the branch only adds files.

Blast Radius: repo-wide. Every later branch builds on these layers, the lint rules and CI.

🤖 Generated with Claude Code

jayleaton and others added 13 commits October 22, 2026 13:10
… repository

Repository tests run against a real Postgres: one throwaway server, a
migrated template database built once, and a per-test clone so tests run
in parallel. The Better Auth user table comes from a stub until
packages/auth lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pinned tools

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… UserService

Config is validated once at boot and reports every problem together.
UserService enforces the not-self 403 rule and returns typed service
errors that transports map to status codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…endpoints

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unt points

serve binds 0.0.0.0:PORT, logs with slog, rejects cross-origin browser
writes, and shuts down gracefully on SIGTERM. migrate runs the auth
migrations (stubbed until packages/auth) and then the app migrations. In
development a dev auth fake signs requests in as a seeded user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f script

scripts/check-layering.sh drops a probe with a forbidden import into each
guarded layer and fails unless depguard reports all of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d race tests

ci-ok is the single required check; merge_group is supported.
actionlint gets its own tools module because its yaml dependency
conflicts with golangci-lint's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, /healthz)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sqlc needs cgo; regenerating on every dev run would require a C
toolchain on a fresh clone. make generate stays explicit and CI checks
the diff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drafts run no jobs; marking ready for review runs CI. merge_group, push to
main and workflow_dispatch always run everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measurement done; workflow_dispatch works once ci.yml is on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton marked this pull request as ready for review October 22, 2026 06:36
@jayleaton
jayleaton merged commit a6562bd into main Oct 22, 2026
8 checks passed
@jayleaton
jayleaton deleted the feat/foundation branch October 22, 2026 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant