xtablo-source/.planning/phases/01-foundation/SKELETON.md
2026-05-14 17:50:48 +02:00

6.2 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 (bootstrap-downloaded to static/htmx.min.js by just bootstrap; gitignored, never committed) "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          (bootstrap-downloaded by `just bootstrap`; gitignored; no runtime 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-op goose up migration applied
  • UI — index.templ rendered via internal/web/ui design-system (Button + Card), HTMX hx-get round-trip to /demo/time returning a templ fragment
  • Deployment — local-run only in Phase 1 (Phase 7 owns container/VPS deploy). just dev is 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/worker in 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) — extend internal/web/ui in Phase 2
  • Modal / Table / EmptyState components — extend internal/web/ui in Phase 3
  • CSRF tokens, rate limiting (Phase 2)
  • Embedded goose library 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/ui with Input, FormField. Adds users + sessions tables.
  • Phase 3: Authenticated user can list / create / view / edit / delete tablos. Extends internal/web/ui with Modal, 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/worker runs 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.