Backend scaffold: Go module, config, HTTP server, SQLite store + migrations #1

Closed
opened 2026-07-18 18:14:34 +00:00 by steve · 0 comments
Owner

Part of epic #20 · Design: DESIGN.md

Context

Pansy is a self-hostable garden planner: a Go backend serving a JSON API plus an embedded React frontend, shipped as one static binary. This issue creates the backend skeleton everything else builds on: Go module, env config, gin HTTP server, SQLite store with an embedded migration runner, and the full initial schema.

Scope

  • go.mod: module gitea.stevedudenhoeffer.com/steve/pansy, Go 1.26.x. Pure-Go deps only — the build must work with CGO_ENABLED=0.
  • internal/config: load from env with defaults — PANSY_PORT (8080), PANSY_DB (./pansy.db), PANSY_BASE_URL, PANSY_REGISTRATION (open|closed, default open), PANSY_LOCAL_AUTH (default true), PANSY_OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/BUTTON_LABEL, trusted proxies. Auth values are plumbed now, consumed by #4/#5.
  • internal/store/sqlite.go: open modernc.org/sqlite with WAL + busy_timeout=5000 pragmas.
  • internal/store/migrate.go: tiny migration runner — numbered .sql files via embed.FS, applied in order, tracked in schema_migrations(version), run at startup, idempotent.
  • internal/store/migrations/0001_init.sql: full initial schema per DESIGN.md § Domain model — users, sessions, gardens, garden_shares, garden_objects, plants, plantings. All measurements in cm; version INTEGER NOT NULL DEFAULT 1 on user-mutable tables; unique (oidc_issuer, oidc_subject) on users; unique (garden_id, user_id) on garden_shares; indexes on foreign keys. Reserve shape='polygon' + points JSON column on garden_objects even though v1 only uses rect/circle.
  • internal/domain: plain structs mirroring the schema + sentinel errors ErrNotFound, ErrForbidden, ErrVersionConflict.
  • internal/api: gin server — gin.New() + slog-gin + gin.Recovery(); route GET /api/v1/healthz returning {"ok":true}.
  • cmd/pansy/main.go: config → store.Open → migrate → api → ListenAndServe with graceful shutdown on SIGINT/SIGTERM.

Out of scope

  • Any endpoints beyond healthz (auth is #4/#5, gardens #7).
  • Frontend (#2) and embedding/Makefile (#3).
  • Entity query methods — each feature issue adds its own store queries.

Key files & patterns

  • gin + slog setup pattern: /Users/steve/Projects/mort/pkg/ginserver/ginserver.go (sibling repo on this machine).
  • sqlite open/pragma pattern: /Users/steve/Projects/executus/contrib/store/sqlite.go.
  • Schema source of truth: DESIGN.md § Domain model.

Dependencies

None — first issue.

Acceptance criteria

  • CGO_ENABLED=0 go build ./... succeeds; go vet ./... is clean.
  • Running the binary creates and migrates the DB; curl localhost:8080/api/v1/healthz returns ok.
  • A second run is a no-op for migrations (idempotent).
  • Inspecting the DB shows all seven tables with the DESIGN.md columns.
Part of epic #20 · Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md) ## Context Pansy is a self-hostable garden planner: a Go backend serving a JSON API plus an embedded React frontend, shipped as one static binary. This issue creates the backend skeleton everything else builds on: Go module, env config, gin HTTP server, SQLite store with an embedded migration runner, and the full initial schema. ## Scope - [ ] `go.mod`: module `gitea.stevedudenhoeffer.com/steve/pansy`, Go 1.26.x. Pure-Go deps only — the build must work with `CGO_ENABLED=0`. - [ ] `internal/config`: load from env with defaults — `PANSY_PORT` (8080), `PANSY_DB` (`./pansy.db`), `PANSY_BASE_URL`, `PANSY_REGISTRATION` (`open`|`closed`, default open), `PANSY_LOCAL_AUTH` (default true), `PANSY_OIDC_ISSUER`/`CLIENT_ID`/`CLIENT_SECRET`/`BUTTON_LABEL`, trusted proxies. Auth values are plumbed now, consumed by #4/#5. - [ ] `internal/store/sqlite.go`: open `modernc.org/sqlite` with WAL + `busy_timeout=5000` pragmas. - [ ] `internal/store/migrate.go`: tiny migration runner — numbered `.sql` files via `embed.FS`, applied in order, tracked in `schema_migrations(version)`, run at startup, idempotent. - [ ] `internal/store/migrations/0001_init.sql`: full initial schema per DESIGN.md § Domain model — `users`, `sessions`, `gardens`, `garden_shares`, `garden_objects`, `plants`, `plantings`. All measurements in cm; `version INTEGER NOT NULL DEFAULT 1` on user-mutable tables; unique `(oidc_issuer, oidc_subject)` on users; unique `(garden_id, user_id)` on garden_shares; indexes on foreign keys. Reserve `shape='polygon'` + `points` JSON column on garden_objects even though v1 only uses rect/circle. - [ ] `internal/domain`: plain structs mirroring the schema + sentinel errors `ErrNotFound`, `ErrForbidden`, `ErrVersionConflict`. - [ ] `internal/api`: gin server — `gin.New()` + slog-gin + `gin.Recovery()`; route `GET /api/v1/healthz` returning `{"ok":true}`. - [ ] `cmd/pansy/main.go`: config → `store.Open` → migrate → api → `ListenAndServe` with graceful shutdown on SIGINT/SIGTERM. ## Out of scope - Any endpoints beyond healthz (auth is #4/#5, gardens #7). - Frontend (#2) and embedding/Makefile (#3). - Entity query methods — each feature issue adds its own store queries. ## Key files & patterns - gin + slog setup pattern: `/Users/steve/Projects/mort/pkg/ginserver/ginserver.go` (sibling repo on this machine). - sqlite open/pragma pattern: `/Users/steve/Projects/executus/contrib/store/sqlite.go`. - Schema source of truth: `DESIGN.md` § Domain model. ## Dependencies None — first issue. ## Acceptance criteria - `CGO_ENABLED=0 go build ./...` succeeds; `go vet ./...` is clean. - Running the binary creates and migrates the DB; `curl localhost:8080/api/v1/healthz` returns ok. - A second run is a no-op for migrations (idempotent). - Inspecting the DB shows all seven tables with the DESIGN.md columns.
steve added the infrabackend labels 2026-07-18 18:14:34 +00:00
steve closed this issue 2026-07-18 19:25:30 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#1