6 KiB
6 KiB
Walking Skeleton — Xtablo Go+HTMX Rewrite
Phase: 1 Generated: 2026-05-14
Capability Proven End-to-End
A developer can clone the repo, run just bootstrap, podman compose up -d, just migrate up, and just dev, then load http://localhost:8080/ in a browser, click "Fetch server time", and see an HTML fragment returned by the Go server — exercising templ rendering, chi routing, HTMX swap, static asset serving, slog logging, and a /healthz endpoint that calls pgxpool.Ping against the goose-migrated local Postgres.
Architectural Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Language / runtime | Go ≥ 1.22 (developer machine has 1.26) | No JS framework / no BaaS thesis; chi v5 requires 1.22 |
| HTTP router | github.com/go-chi/chi/v5 v5.2.5 |
Idiomatic, standard middleware set (CONTEXT D-08) |
| HTML templating | github.com/a-h/templ v0.3.1020 |
Compile-time type checking; first-class HTMX fragment story (D-07) |
| Client interactivity | HTMX v2.x (vendored at static/htmx.min.js) |
"No JS framework" — HTMX swaps server-rendered fragments (D-10) |
| CSS | Tailwind v4 standalone CLI binary in ./bin/tailwindcss |
Avoids any Node toolchain in backend/ (D-12) |
| DB driver | github.com/jackc/pgx/v5 + pgxpool v5.9.2 |
sqlc-recommended, richer pg types (D-16) |
| Migrations | github.com/pressly/goose/v3 v3.27.1 (CLI in Phase 1) |
Embeddable library, one .sql per migration, sqlc-compatible (D-04, D-05) |
| Query generation | sqlc-dev/sqlc v1.31.1 (configured; no queries yet) |
Type-safe SQL without ORM (foundation for Phase 2+) |
| Logging | log/slog stdlib — JSON in prod, text in dev |
Stdlib, no external dep (D-17) |
| Request IDs | github.com/google/uuid v1.6.0 via custom middleware |
RFC 4122 UUIDs threaded through context.Context to slog (D-18) |
| Live reload | github.com/air-verse/air v1.65.1 |
Watches .go + .templ; pre_cmd runs templ generate |
| Local Postgres | podman compose with postgres:16-alpine at backend/compose.yaml |
Developer machine standard (D-11) |
| Local task runner | just |
Standard across the repo |
| Deployment target | Single VPS / single container (deferred to Phase 7) | Locked in PROJECT.md |
| Auth | Server-managed sessions, HTTP-only cookies (Phase 2) | "No JWT, no third-party auth" |
| Directory layout | Two-binary cmd/web + cmd/worker, shared internal/ (D-01, D-02, D-03) |
See below |
| Design-system | Custom templ component package at backend/internal/web/ui/ (Button, Card, Badge in Phase 1) |
UI-SPEC contract; mirrors go-backend/internal/web/ui/ enum surface |
Directory layout (locked)
backend/
cmd/
web/main.go
worker/main.go
internal/
db/ (pgxpool wiring + sqlc-generated queries)
doc.go
pool.go
web/
router.go
handlers.go
middleware.go
handlers_test.go
ui/ (custom templ design-system)
tokens.go
variants.go
helpers.go
base.css
button.templ
button.css
card.templ
card.css
badge.templ
badge.css
ui_test.go
session/doc.go (placeholder — Phase 2)
tablos/doc.go (placeholder — Phase 3)
tasks/doc.go (placeholder — Phase 4)
files/doc.go (placeholder — Phase 5)
migrations/
0001_init.sql (no-op bootstrap; real schema lands Phase 2)
templates/
layout.templ
index.templ
fragments.templ
static/
htmx.min.js (vendored, not CDN)
tailwind.css (generated by Tailwind standalone CLI)
bin/ (gitignored — tailwindcss, air, etc.)
.air.toml
.env.example
.gitignore
compose.yaml
go.mod / go.sum
justfile
sqlc.yaml
tailwind.input.css
README.md
Stack Touched in Phase 1
- Project scaffold (
go mod init backend, justfile,.air.toml,tailwind.input.css,sqlc.yaml,compose.yaml) - Routing — chi router with
/,/healthz,/demo/time,/static/* - Database —
pgxpool.New(DATABASE_URL)+pool.Ping(ctx)exercised by/healthz; one no-opgoose upmigration applied - UI —
index.templrendered viainternal/web/uidesign-system (Button + Card), HTMXhx-getround-trip to/demo/timereturning a templ fragment - Deployment — local-run only in Phase 1 (Phase 7 owns container/VPS deploy).
just devis the documented full-stack run command.
Out of Scope (Deferred to Later Slices)
- Authentication, sessions, users (Phase 2)
- Tablos CRUD (Phase 3)
- Tasks / kanban board (Phase 4)
- File uploads + S3/R2 (Phase 5)
- Real background jobs (Phase 6 —
cmd/workerin Phase 1 is boot/log/shutdown only) - Production deploy, Dockerfile,
/readyz(Phase 7) - Dark mode, icon library, web fonts (UI-SPEC defers these)
- Form components (
Input,Textarea,Select,FormField) — extendinternal/web/uiin Phase 2 - Modal / Table / EmptyState components — extend
internal/web/uiin Phase 3 - CSRF tokens, rate limiting (Phase 2)
- Embedded
gooselibrary call from app startup (Phase 7) - CI configuration (no DEPLOY-XX requirement in Phase 1)
Subsequent Slice Plan
Each later phase adds one vertical slice on top of this skeleton without altering its architectural decisions:
- Phase 2: User can sign up, log in (email + password, argon2/bcrypt), receive a session cookie, and stay logged in. Extends
internal/web/uiwithInput,FormField. Addsusers+sessionstables. - Phase 3: Authenticated user can list / create / view / edit / delete tablos. Extends
internal/web/uiwithModal,Table,EmptyState,IconButton. - Phase 4: User can run a kanban board inside a tablo (create/edit/move/reorder/delete tasks). Drag-and-drop or button-based reorder TBD.
- Phase 5: User can attach files, list, download (signed URL), delete — backed by R2/S3.
- Phase 6:
cmd/workerruns a real job queue (river/asynq/pg_notify TBD) against the same Postgres. - Phase 7: Both binaries ship in a single multi-stage Docker image, deployed to a single VPS / Cloud Run-style host with migrations applied on deploy.