Backend Phase A: foundation, migrations, Docker setup
- Go backend skeleton: config (caarlos0/env), slog JSON logging, pgxpool store, HTTP server with graceful shutdown. - httpapi: render helpers, Problem errors, middleware chain (requestID / logging / recover / CORS), /healthz and /readyz. - Migrations: full initial schema (users, sessions, lists, list_members, items, op_log SOURCE OF TRUTH, item_names) + golang-migrate runner binary using source/iofs (embedded). - Docker: multi-stage Dockerfile (Go 1.26 -> distroless nonroot), builds both server and migrate binaries. - deploy: docker-compose (caddy + backend + migrate + postgres:16), Caddyfile (auto-HTTPS), .env.example, pg extensions init script. - AGENTS.md: project context + roadmap for AI agents. Verified: image builds, both binaries run in container (smoke test).
This commit is contained in:
commit
2899eb205b
23 changed files with 1372 additions and 0 deletions
191
AGENTS.md
Normal file
191
AGENTS.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# AGENTS.md – Kontext & Fortschritt für KI-Agenten
|
||||
|
||||
> Diese Datei hält den Projekt-Stand und die Architekturentscheidungen fest, damit
|
||||
> ein Agent (z.B. zuhause) sofort weiterarbeiten kann. Sie ist Teil des Repos.
|
||||
|
||||
## Projekt: Mitbringsl
|
||||
|
||||
Local-First Einkaufslisten-App (Bring-Alternative, werbefrei).
|
||||
**Backend:** Go (net/http, pgx, golang-migrate, go-oidc) als Docker-Container.
|
||||
**App:** Kotlin + Jetpack Compose, Room, Hilt, WorkManager, offline-first.
|
||||
**Deployment:** docker-compose (Caddy + backend + migrate + postgres:16), auto-HTTPS.
|
||||
|
||||
Vollständiger Plan liegt als genehmigtem Plan zugrunde (siehe Abschnitt "Roadmap").
|
||||
|
||||
---
|
||||
|
||||
## Getroffene Architekturentscheidungen (verbindlich)
|
||||
|
||||
- **Backend-Sprache:** Go (nicht Rust).
|
||||
- **DB:** PostgreSQL (`postgres:16-alpine`), Extensions `pgcrypto` + `pg_trgm`.
|
||||
- **Sync:** Local-First, **keine Datenverluste** – Append-only `op_log` (SOURCE OF TRUTH),
|
||||
`items`/`lists` sind Projektionen; Konfliktlösung per LWW-Register `(hlc_ts, client_id)`
|
||||
+ Tombstones (`deleted_at`). HLC = Hybrid Logical Clock.
|
||||
- Push: `POST /api/lists/{id}/ops` (idempotent via `UNIQUE(client_id, client_seq)`).
|
||||
- Pull: `GET /api/lists/{id}/ops?since={seq}`.
|
||||
- **Auth:**
|
||||
- Eigene User: E-Mail/Passwort, **Argon2id** (PHC-Format).
|
||||
- OIDC: **Google** + **Generic OIDC**. Die **App macht den Code+PKCE-Flow selbst**
|
||||
und schickt nur das `id_token` ans Backend (`POST /auth/oidc`).
|
||||
Backend verifiziert via `github.com/coreos/go-oidc/v3` (Signatur gegen JWKS,
|
||||
iss/aud/exp) und stellt **eigene opaque Session-Tokens** aus
|
||||
(`sessions`-Tabelle, SHA-256-Hash gespeichert).
|
||||
- **AppAuth-Android bewusst NICHT verwendet** (seit 2021 verwaist). Stattdessen:
|
||||
Google via **Credential Manager**, Generic OIDC via **Custom Tabs + eigenes PKCE**.
|
||||
- **Vorschläge:** aggregiert aus Item-Namen aller User (`item_names`-Tabelle,
|
||||
pg_trgm fuzzy search). Endpoint `GET /api/suggestions?q=`.
|
||||
- **MVP-Scope:** Single-Owner-Listen (`list_members` existiert, wird in Phase 2 für
|
||||
geteilte Listen genutzt); keine Echtzeit-Push (nur Periodic-Pull 15 min + Pull-on-Online);
|
||||
keine Web-UI.
|
||||
|
||||
---
|
||||
|
||||
## Technologie-Stacks (final)
|
||||
|
||||
### Backend
|
||||
| Bereich | Wahl |
|
||||
|---|---|
|
||||
| Go-Version | **1.26** (`go.mod` hat `go 1.26`; Docker-Image `golang:1.26-alpine`) |
|
||||
| HTTP | stdlib `net/http` (Go 1.22+ Routing mit Methoden + Path-Vars) |
|
||||
| DB-Driver | `github.com/jackc/pgx/v5` (pgxpool) |
|
||||
| Migrationen | `github.com/golang-migrate/migrate/v4` + `source/iofs` (eingebettet via `embed.FS`) |
|
||||
| OIDC | `github.com/coreos/go-oidc/v3` + `golang.org/x/oauth2` |
|
||||
| Passwörter | Argon2id (`golang.org/x/crypto/argon2`) |
|
||||
| Sessions | opaque Tokens (32 B base64url), `sessions`-Tabelle, SHA-256-Hash |
|
||||
| Config | `github.com/caarlos0/env/v11` (struct-tag env) |
|
||||
| Logging | `log/slog` mit `NewJSONHandler` → stdout |
|
||||
| Docker | Multi-Stage, `CGO_ENABLED=0`, `gcr.io/distroless/static-debian12:nonroot` |
|
||||
| Queries | **direkt mit pgx** (sqlc wurde aus Skalierbarkeit bewusst auf später verschoben) |
|
||||
|
||||
### Android (noch nicht begonnen)
|
||||
| Bereich | Wahl |
|
||||
|---|---|
|
||||
| Build | Kotlin DSL + `libs.versions.toml`, Single-Module `:app` |
|
||||
| Kotlin/AGP/Gradle | Kotlin 2.x (K2), AGP 8.9+, Gradle 8.11+, JDK 17 |
|
||||
| UI | Compose BOM (2025.x) + Material 3 |
|
||||
| Arch | MVVM + `ViewModel` + `StateFlow`, UDF |
|
||||
| DB | Room (KSP), Source of Truth via `Flow` |
|
||||
| Netzwerk | Retrofit + OkHttp + kotlinx.serialization |
|
||||
| Sync | WorkManager `CoroutineWorker` (Outbox-Drain + Cursor-Pull) |
|
||||
| DI | Hilt (KSP) + `hilt-navigation-compose` + `hilt-work` |
|
||||
| IDs | Client-seitige UUIDs |
|
||||
| Auth | Google: Credential Manager; Generic OIDC: Custom Tabs + PKCE selbst |
|
||||
| Suche | `OutlinedTextField` + `DropdownMenu`, Room-FTS, Server-Fallback |
|
||||
| SDK | minSdk 26, compile/target 36 |
|
||||
|
||||
---
|
||||
|
||||
## Repository-Struktur
|
||||
|
||||
```
|
||||
mitbringsl/
|
||||
├── AGENTS.md ← DIES DATEI
|
||||
├── README.md
|
||||
├── .gitignore
|
||||
├── backend/
|
||||
│ ├── cmd/
|
||||
│ │ ├── server/main.go # HTTP-Server-Einstieg (verdrahtet cfg/logger/pool/api)
|
||||
│ │ └── migrate/main.go # Migrations-Runner (iofs-embedded)
|
||||
│ ├── internal/
|
||||
│ │ ├── config/config.go # Config (caarlos0/env)
|
||||
│ │ ├── logging/logging.go # slog JSON-Setup
|
||||
│ │ ├── store/db.go # pgxpool-Setup
|
||||
│ │ └── httpapi/
|
||||
│ │ ├── api.go # API-Objekt + Router (Health aktiv, Rest noch auskommentiert)
|
||||
│ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON
|
||||
│ │ ├── middleware.go # requestID/logging/recover/cors + Chain
|
||||
│ │ └── health.go # /healthz + /readyz
|
||||
│ ├── migrations/
|
||||
│ │ ├── embed.go # //go:embed *.sql
|
||||
│ │ ├── 000001_init_schema.up.sql # users/sessions/lists/list_members/items/op_log/item_names
|
||||
│ │ └── 000001_init_schema.down.sql
|
||||
│ ├── Dockerfile # Multi-Stage, baut server + migrate
|
||||
│ ├── .dockerignore
|
||||
│ ├── go.mod / go.sum
|
||||
├── deploy/
|
||||
│ ├── docker-compose.yml # caddy + backend + migrate + db (mit YAML-anchors)
|
||||
│ ├── Caddyfile # auto-HTTPS
|
||||
│ ├── .env.example # alle env-Vars dokumentiert
|
||||
│ └── db/init/001_extensions.sql # CREATE EXTENSION pgcrypto, pg_trgm
|
||||
├── docs/ # NOCH LEER (folgt Phase F)
|
||||
└── android/ # NOCH LEER (folgt Phase D)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Roadmap / Fortschritt
|
||||
|
||||
Legende: ✅ erledigt · 🚧 in Arbeit · ⬜ offen
|
||||
|
||||
- ✅ Repo-Struktur + `.gitignore` + `README.md` + `git init` (Branch `main`)
|
||||
- ✅ **Phase A – Backend-Fundament:** Config, slog, pgxpool, `/healthz`+`/readyz`, Server-Main.
|
||||
- ✅ **Phase A – Migrationen:** vollständiges Init-Schema (up+down) + `migrate`-Binary (iofs).
|
||||
- ✅ **Phase A – Docker:** Dockerfile (Go 1.26 → distroless nonroot), docker-compose
|
||||
(caddy/backend/migrate/db), Caddyfile, `.env.example`.
|
||||
**Verifiziert:** Image baut, beide Binaries laufen im Container (Smoke-Test OK).
|
||||
- ⬜ **Phase B – Auth:** Argon2id + eigene User (register/login) + Session-Middleware.
|
||||
- ⬜ **Phase B – OIDC:** go-oidc-Verifikation (Google + Generic) + `POST /auth/oidc`.
|
||||
- ⬜ **Phase C – Sync-Kern:** `op_log`-Append (idempotent), HLC, Projektion op→items/lists (LWW+Tombstones).
|
||||
- ⬜ **Phase C – Endpoints:** `/api/lists`, `/api/lists/{id}/ops` (push+pull).
|
||||
- ⬜ **Phase C – Suggestions:** `item_names`-Trigger + `/api/suggestions`.
|
||||
- ⬜ **Phase D – Android-Fundament:** Gradle (Kotlin DSL, Version Catalog, Hilt/KSP, Compose BOM),
|
||||
Theme, Nav, Room.
|
||||
- ⬜ **Phase D – Repository + Retrofit-API + DTOs.**
|
||||
- ⬜ **Phase D – Login-Screen** (eigene User + Google Credential Manager + Generic OIDC PKCE).
|
||||
- ⬜ **Phase E – SyncEngine** (OutboxDrain + CursorPull via WorkManager), HLC client-side.
|
||||
- ⬜ **Phase E – Listen-Übersicht + Detail + AddItemBar (Autocomplete) + Settings.**
|
||||
- ⬜ **Phase F – Polish** (Fehlerbehandlung, Offline-Indikator, Empty States, Tests).
|
||||
- ⬜ **Phase F – README + docs** (ARCHITECTURE/SYNC/API).
|
||||
|
||||
### Wo genau weitermachen?
|
||||
**Nächster Schritt = Phase B (Auth):**
|
||||
1. `internal/auth/password.go` – Argon2id (PHC-Format) Hashen/Verifizieren.
|
||||
2. `internal/auth/session.go` – Token generieren (crypto/rand, base64url), SHA-256-Hash,
|
||||
in `sessions` einfügen, Middleware `requireAuth` (lädt `user_id` in Context).
|
||||
3. `internal/httpapi/auth.go` – Handler `Register`/`Login`/`Logout`.
|
||||
4. In `api.go` die `/auth/*`-Routen einkommentieren + verdrahten.
|
||||
5. Dann Phase B Teil 2: OIDC (`internal/auth/oidc.go` + `POST /auth/oidc`).
|
||||
|
||||
---
|
||||
|
||||
## Wichtige technische Notizen / Fallstricke (bereits gelöst)
|
||||
|
||||
- **golang-migrate `iofs`-Pfad:** Der Import ist
|
||||
`github.com/golang-migrate/migrate/v4/source/iofs` (MIT `/source/`).
|
||||
Die Pfade ohne `/source/` gibt es nicht mehr → Build-Fehler.
|
||||
- **Go-Version:** `pgx/v5 v5.10.0` braucht Go ≥ 1.25, deshalb Go 1.26
|
||||
(lokal + Docker-Image `golang:1.26-alpine`). `go.mod` hat `go 1.26`.
|
||||
- **Docker-Build auf Windows/Docker Desktop:** `--network host` im Build-Container
|
||||
funktioniert NICHT (WSL2-NAT). Für Tests: echtes `docker network create` + Container-Namen nutzen.
|
||||
- **Compose braucht zwingend `POSTGRES_PASSWORD`** (`.env` oder Env), da
|
||||
`db.environment` mit `${POSTGRES_PASSWORD:?...}`-Assertion vor dem Build validiert wird.
|
||||
- **Healthcheck im `backend`-Service wurde entfernt**, weil distroless/static kein
|
||||
`wget`/`curl` enthält. Später: eigenes Binary, das `/healthz` per Go-HTTP prüft,
|
||||
oder `healthcheck` über Caddy/extern.
|
||||
- **SQL-Queries werden direkt mit pgx geschrieben** (kein sqlc im MVP), da sqlc lokal
|
||||
nicht installiert ist und ein extra Code-Gen-Schritt nötig wäre. Bei Bedarf später
|
||||
problemlos nachrüstbar.
|
||||
|
||||
---
|
||||
|
||||
## Build- & Test-Befehle
|
||||
|
||||
```bash
|
||||
# Backend lokal bauen
|
||||
cd backend && go build ./... && go vet ./...
|
||||
|
||||
# Backend-Image bauen
|
||||
cd backend && docker build -t mitbringsl-backend:test .
|
||||
|
||||
# Komplettes Stack starten (braucht deploy/.env)
|
||||
cd deploy && cp .env.example .env && docker compose up -d --build
|
||||
|
||||
# Migrationen manuell gegen bestehende DB anwenden
|
||||
docker run --rm --network <net> \
|
||||
-e DATABASE_URL="postgres://app:PW@<db-host>:5432/appdb?sslmode=disable" \
|
||||
mitbringsl-backend:test /app/migrate up
|
||||
```
|
||||
|
||||
## Git-Status
|
||||
- Repo initialisiert, Branch `main`. **Remote ist noch NICHT konfiguriert.**
|
||||
- Es wurde **noch nicht committet** (Stand beim Schreiben dieser Datei).
|
||||
Loading…
Add table
Add a link
Reference in a new issue