# 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 │ │ ├── auth/ # PHASE B – Password + Session + OIDC │ │ │ ├── password.go # Argon2id im PHC-Format (HashPassword/VerifyPassword) │ │ │ ├── password_test.go # PHC-Roundtrip-Tests │ │ │ ├── user.go # UserStore: CreateUser/GetByEmail/GetByID/GetByOIDCSubject/CreateOIDCUser │ │ │ ├── session.go # SessionStore: opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke │ │ │ ├── oidc.go # OIDCService: id_token-Verifikation (JWKS/iss/aud/exp), Provider-Caching │ │ │ └── pgcode.go # isUniqueViolation (SQLSTATE 23505) │ │ └── httpapi/ │ │ ├── api.go # API-Objekt + Router (Health + /auth/* aktiv, Rest auskommentiert) │ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON │ │ ├── middleware.go # requestID/logging/recover/cors + Chain │ │ ├── health.go # /healthz + /readyz │ │ └── auth.go # Register/Login/Logout/OIDC-Handler + RequireAuth-Middleware │ ├── 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 (Password):** Argon2id im PHC-Format + `UserStore` (Create/GetByEmail/GetByID) + `SessionStore` (opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke) + Handler `Register`/`Login`/`Logout` + `RequireAuth`-Middleware. **Verifiziert:** `go test ./internal/auth/...` grün, E2E-Smoke-Test gegen echtes PostgreSQL via Docker (Register/Login/Logout/Duplicate/Short-PW/Wrong-PW alle korrekt). - ✅ **Phase B – OIDC:** `OIDCService` (go-oidc v3, JWKS-Signatur, iss/aud/exp, Provider-Caching) + `POST /auth/oidc` (find-or-create User via `(oidc_issuer, oidc_subject)`, reuses `issueSession`) + Config-Validierung (enabled → client_id/issuer Pflicht). **Verifiziert:** E2E-Flow gegen lokalen Mock-IdP (Discovery → JWKS → signiertes id_token → User angelegt → 2. Login findet gleichen User → tampered Token → 401). Alle Fehlerpfade geprüft (disabled→400, unknown provider→400, missing fields→400, invalid→401). - ⬜ **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? **Phase B ist komplett ✅. Nächster Schritt = Phase C (Sync-Kern).** Phase B erledigt: - ✅ `internal/auth/password.go` – Argon2id im PHC-Format (HashPassword/VerifyPassword). - ✅ `internal/auth/user.go` – UserStore (CreateUser/GetByEmail/GetByID/GetByOIDCSubject/CreateOIDCUser). - ✅ `internal/auth/session.go` – SessionStore (crypto/rand + base64url + SHA-256, Create/Lookup/Revoke). - ✅ `internal/auth/oidc.go` – OIDCService (id_token-Verifikation via go-oidc v3, Provider-Caching). - ✅ `internal/httpapi/auth.go` – Register/Login/Logout/OIDC + RequireAuth-Middleware. - ✅ `config.go` – OIDC-Validierung (enabled → client_id/issuer Pflicht). - ✅ Alle `/auth/*`-Routen aktiv verdrahtet. Phase C – Sync-Kern (offen): 1. `internal/sync/hlc.go` – Hybrid Logical Clock (client- + server-seitig, `(ts, counter)`). 2. `op_log`-Append in `internal/store/opstore.go`: `AppendOp` idempotent via `UNIQUE(client_id, client_seq)`, zurück: server-seitiger `seq` + `now()`-basierte HLC. 3. Projektion op→items/lists: `internal/store/liststore.go`/`itemstore.go` mit LWW (`hlc_ts`) + Tombstones (`deleted_at`). 4. Endpoints `GET/POST /api/lists`, `GET/POST /api/lists/{id}/ops` (Pull `?since=`, Push idempotent). 5. Suggestions: `item_names`-Trigger + `GET /api/suggestions?q=`. --- ## 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 \ -e DATABASE_URL="postgres://app:PW@:5432/appdb?sslmode=disable" \ mitbringsl-backend:test /app/migrate up ``` ## Git-Status - Repo initialisiert, Branch `main`. Remote ist konfiguriert (`origin`). - Phase A + Phase B (1/2: Password/Sessions) committed und gepusht. - Phase B (2/2: OIDC) committed und gepusht. **Phase B vollständig ✅.** - Nächster Schritt: **Phase C – Sync-Kern** (siehe Roadmap oben).