docs(sprint-1-assessment): walking skeleton recommendation, 11 risks, A1-A6 acceptance

Patrick Plate
2026-06-24 14:48:18 +02:00
parent 461b8ea1cf
commit 7ccdfcfc70
+233
@@ -0,0 +1,233 @@
# Sprint 1 — Assessment ("Spark")
**Date:** Pre-Sprint 1 planning
**Module:** Sparkboard (greenfield)
**Author:** Patrick Plate / Lumen (Planner)
**Status:** Entwurf v1 — ready for Patrick GO
**Sprint codename:** Spark
**Sprint goal:** Walking skeleton consuming [plate-auth](https://git.plate-software.de/pplate/plate-auth/wiki) v0.1.0, deployed to `sparkboard.plate-software.de`, four allowlisted users can sign in with Google and post ideas.
---
## 1. Problem analysis
Sparkboard does not exist yet. There is no codebase, no database, no deployment, no DNS, no CI pipeline. The Sprint 1 problem is therefore not "fix a bug" or "extend a feature" — it is "go from a `git init` to a four-human production system in one sprint."
The unique pressure on Sprint 1 is that Sparkboard must be **the first real consumer of plate-auth**. Every assumption plate-auth made about how its starter would be used is, by definition, validated or falsified here. If plate-auth's auto-configuration leaves a sharp edge, Sparkboard will hit it. The benefit is that we are doing this hand-in-hand with plate-auth (same developer, same wiki, same week) — defects in plate-auth's contract surface immediately and can be patched in plate-auth, not worked around in Sparkboard.
The product surface is intentionally tiny: one entity (`Idea`), two endpoints (`GET /api/ideas`, `POST /api/ideas`), two pages (`/login`, `/ideas`). Everything _else_ in Sprint 1 is infrastructure: build, deploy, auth wire-up, PWA, CI/CD. The fact that the product surface is small **is the strategy** — it focuses Sprint 1 entirely on the integration story.
---
## 2. Affected components
Because Sparkboard is greenfield, "affected" means "created". The full v1 component inventory:
| Layer | Component | New / Library | Owner |
|------|-----------|--------------|-------|
| Backend | `SparkboardApplication.java` | New | Sparkboard |
| Backend | `Idea` entity + `IdeaRepository` / `IdeaService` / `IdeaController` | New | Sparkboard |
| Backend | `SparkboardOnboardingHook` (`OnboardingHook` SPI impl) | New | Sparkboard |
| Backend | `SparkboardAdminProperties` (`@ConfigurationProperties("sparkboard")`) | New | Sparkboard |
| Backend | Flyway migrations: `V1__init.sql` (Sparkboard tables only) + `V2__seed_family_spark_org.sql` | New | Sparkboard |
| Backend | `de.platesoft:plate-auth-starter:0.1.0` | Library | plate-auth |
| Backend | `application.yml` with `plate.auth.*` configuration | New | Sparkboard |
| Frontend | `auth.ts` (calls `createAuthConfig` from `@platesoft/auth/next-auth`) | New | Sparkboard |
| Frontend | `app/api/auth/[...nextauth]/route.ts` (NextAuth handlers re-export) | New | Sparkboard |
| Frontend | `app/api/backend/[...path]/route.ts` (calls `createProxyHandlers` from `@platesoft/auth/proxy`) | New | Sparkboard |
| Frontend | `app/(app)/ideas/page.tsx` (list, server component) | New | Sparkboard |
| Frontend | `app/(app)/ideas/new/page.tsx` + `<IdeaForm/>` | New | Sparkboard |
| Frontend | `app/(auth)/login/page.tsx` | New | Sparkboard |
| Frontend | `public/manifest.json` + `public/sw.js` + icon set + `lib/sw-register.ts` | New | Sparkboard |
| Frontend | `@platesoft/auth@0.1.0` npm package | Library | plate-auth |
| Infra | `docker-compose.yml` (local dev) + `docker-compose.prod.yml` (TrueNAS) | New | Sparkboard |
| Infra | IONOS Apache vhost for `sparkboard.plate-software.de` | New | Sparkboard (same template as InspectFlow/CannaManage) |
| Infra | `frpc` entry on TrueNAS, port **30011** | New | Sparkboard |
| Infra | DNS: `sparkboard.plate-software.de` CNAME | New | Sparkboard |
| CI | `.gitea/workflows/ci.yml` (build + test) | New | Sparkboard |
| CI | `.gitea/workflows/deploy.yml` (SSH deploy to TrueNAS) | New | Sparkboard |
| Docs | This wiki | New | Sparkboard |
What is **deliberately not built** (because plate-auth ships it):
- Any `User` entity / table / repository
- Any JWT-signing or JWT-validating filter
- Any allowlist enforcement
- Any membership / invitation / access-request table or controller
- Any auth-exchange controller
- Any hand-rolled proxy that injects bearer tokens
- Any NextAuth provider config beyond `createAuthConfig({ providers: { google: { ... } } })`
The size of that **not built** list is the integration thesis.
---
## 3. Current state
There is no current state. Sprint 1 starts from the empty Gitea repo. The closest analogues are:
- **plate-auth v0.1.0** (sibling repo, expected to ship before Sprint 1 starts) — provides every auth primitive.
- **InspectFlow Sprint 14** — the origin of every auth pattern that now lives in plate-auth. Sparkboard does **not** copy code from InspectFlow; it consumes plate-auth instead. The patterns are similar by ancestry, not by code-reuse.
- **CannaManage** — sibling project; provides the deployment shape (TrueNAS + frps + IONOS Apache + Caddy/Apache vhost + Gitea Actions). Sparkboard's `docker-compose.prod.yml`, `Caddyfile` (or Apache equivalent), and `deploy.sh` are pattern-mirrored from CannaManage.
---
## 4. Risk assessment
| # | Risk | Probability | Impact | Mitigation |
|---|------|-------------|--------|-----------|
| R1 | **plate-auth v0.1.0 ships incomplete or with a bug in single-org mode** — Sparkboard is the first consumer, so it finds problems first | Medium | High | Sprint 1 hard-blocks until plate-auth v0.1.0 is green. Sparkboard and plate-auth share a developer (Patrick), so plate-auth bugs become same-day patches, not multi-week tickets. |
| R2 | **Spring Boot 4.1 regression** — Sparkboard is the SB 4.1 guinea pig in plate-software | Low | Medium | Pin to the GA release, not RC/snapshot. If a regression appears, downgrade to SB 4.0 LTS is a one-line `pom.xml` change. plate-auth tests against SB 4.1 in its own CI before publishing. |
| R3 | **Google OAuth misconfiguration** — wrong redirect URI, wrong scopes, wrong consent screen | Medium | Medium | Validate end-to-end in dev with `http://localhost:3000` redirect first, then add the production redirect. Document required Google Cloud Console steps in [Integration Guide §1](Integration-Guide.md). |
| R4 | **NextAuth v5 wire-up confusion** — NextAuth v5 is still relatively new and has notable differences from v4 | Medium | Medium | `@platesoft/auth/next-auth` is the abstraction that hides v5 specifics. As long as that factory is correct, Sparkboard never touches NextAuth v5 internals. |
| R5 | **frps tunnel + IONOS Apache double-proxy** breaks for the new port 30011 | Low | Medium | Pattern is already validated for InspectFlow (30009) and CannaManage (30010). Copy the working frpc.toml and Apache vhost block; only change the port and hostname. |
| R6 | **TrueNAS Docker volume permissions** for the Postgres data dir | Medium | Low | Postgres image runs as UID 999 by default; the TrueNAS dataset must be chowned. Document in `deploy.sh`. Same lesson learned twice already (InspectFlow + CannaManage). |
| R7 | **Gitea Actions SSH deploy step** — SSH key, known_hosts, sudo password | Medium | Medium | Reuse the CannaManage workflow (`appleboy/ssh-action@v1.0.0`). Test the deploy script locally before committing the workflow. |
| R8 | **PWA service-worker caching the login page** — users get stuck on a stale `/login` | Low | High | Service worker is **stub-only** in v1: it registers, claims clients, but caches nothing. Caching strategy is Sprint 4 work, not Sprint 1. |
| R9 | **Allowlist typo locks out one of the four users** | Medium | Low | Allowlist is in `application.yml` env-substituted (`PLATE_AUTH_ALLOWLIST_EMAILS`). Add an admin override mechanism in Sprint 2; for Sprint 1, accept that a redeploy fixes typos. |
| R10 | **Flyway version drift between Sparkboard's history and plate-auth's history** | Low | Medium | Two separate histories (`flyway_schema_history` + `flyway_schema_history_auth`) means they cannot collide. Configured via plate-auth's `plate.auth.flyway.table` property. |
| R11 | **The OnboardingHook fires before plate-auth has finished persisting auth_identities** — race condition on first login | Low | High | plate-auth's contract is that the hook runs **after** `auth_identities` INSERT, in the same transaction. Verified by reading plate-auth's tests. Hypothesis logged in BigMind for resolution during Sprint 1 W1. |
---
## 5. Solution options
Three options for Sprint 1 scope, scored on how aggressively they prove plate-auth.
### Option A — Walking skeleton with one read + one write _(RECOMMENDED)_
- **Scope:** `Idea` entity only. `GET /api/ideas` + `POST /api/ideas`. List page + create form. PWA installable. Deployed.
- **Plate-auth touch surface:** maximum. Auth, allowlist, membership row, JWT, proxy, single-org config — all exercised.
- **Pro:** smallest possible product surface focuses all energy on integration.
- **Pro:** every Sprint 1 acceptance criterion exercises plate-auth.
- **Pro:** if any Sprint 1 acceptance fails, it's almost certainly an integration issue, not a product issue — easy to localise.
- **Con:** the visible product looks _very_ thin after a sprint. Four users get an idea list and a "+" button and nothing else.
- **Effort:** 23 weekends after plate-auth v0.1.0 lands.
### Option B — Backend + frontend in isolation, no deploy
- **Scope:** Build the backend and frontend, run them locally, demo locally. Skip the TrueNAS / frps / IONOS / Gitea Actions work.
- **Pro:** less moving parts in one sprint.
- **Con:** doesn't validate the most operationally risky bits (R5, R6, R7). Defers risk.
- **Con:** delivers nothing the four real users can touch — defeats the point of "walking" skeleton.
- **Effort:** 12 weekends.
### Option C — Frontend-only mock with localStorage
- **Scope:** No backend. Next.js app stores ideas in localStorage. Auth is stubbed.
- **Pro:** trivially deployable as a static site.
- **Con:** doesn't validate plate-auth at all. Sparkboard's entire strategic reason for existing collapses.
- **Effort:** 1 weekend.
---
## 6. Recommendation
**Option A — Walking skeleton.**
The visible thinness of Option A is **the cost**, and it is the right cost. Sparkboard's strategic value is in validating plate-auth, not in shipping a four-feature product surface to four humans on weekend two. If Sprint 1 ships a real auth flow, a real membership row, a real JWT, a real proxy, and a real production deploy — _and only one CRUD operation each direction_ — the integration thesis is proven. Sprint 2 (Kindling) adds the product features.
Option B defers the highest-risk operational work into Sprint 2, where it would compete for attention with reactions/status/comments. Bad trade.
Option C undermines the whole project. Skip.
---
## 7. Acceptance criteria (A1A6)
These are the formal gates Sprint 1 must clear before it is "done". Each is testable; the [Sprint 1 Testplan](Sprint-1-Testplan.md) maps tests onto each.
### A1 — Allowlisted user can sign in with Google
Given a Gmail account in `plate.auth.allowlist.emails`, when the user visits `https://sparkboard.plate-software.de/login` and clicks "Sign in with Google", they are redirected to Google, consent, redirected back, and land on `/ideas` as an authenticated session. Tested by **MT-01**.
### A2 — Non-allowlisted user is rejected with a clean error
Given a Gmail account NOT in the allowlist, when the user attempts the same flow, they see a generic "not authorised" message and **no Sparkboard user record is created** in any table. Tested by **MT-02**, **IT-03**.
### A3 — First login auto-creates membership in Family Spark org
Given a successful first sign-in by an allowlisted user, after the flow completes, the `memberships` table contains exactly one row for that `user_id`: `(user_id, 'SPARK_ORG', FAMILY_SPARK_ID, role)` where `role = ADMIN` if the email is in `sparkboard.admins[]`, otherwise `MEMBER`. Second sign-in does **not** create a duplicate. Tested by **UT-04**, **IT-01**, **MT-03**.
### A4 — Any authenticated user can create and list ideas
`POST /api/ideas` with `{ title, description? }` and a valid bearer token creates an `Idea` row with `org_id = FAMILY_SPARK_ID`, `author_id = <token user_id>`, `status = RAW`, and returns the new idea. `GET /api/ideas` returns all ideas in the Family Spark org, newest first. Tested by **UT-02**, **UT-03**, **IT-02**, **E2E-01**.
### A5 — PWA installable on iOS Safari and Android Chrome
The deployed app serves a valid `manifest.json` with `name`, `short_name`, `start_url`, `display: standalone`, `theme_color`, and at least a 192×192 and 512×512 icon. The "Add to Home Screen" affordance appears on iOS Safari and Android Chrome. Once installed, the app launches in standalone mode without the browser chrome. Tested by **E2E-03**, **MT-04**, **MT-05**.
### A6 — Deployed to `https://sparkboard.plate-software.de` via Gitea Actions
A merge to `main` triggers Gitea Actions, which builds backend and frontend images, pushes to the Gitea container registry, SSH-deploys to TrueNAS, the new containers come up healthy, and the smoke test (`/api/health` returns 200, `/login` returns 200) passes. The whole pipeline is < 10 minutes wall-clock. Tested by **MT-06**, **MT-07**.
---
## 8. plate-auth integration footprint (Sprint 1)
What Sparkboard touches in plate-auth's surface. If any of these contracts changes in v0.2, Sparkboard knows where to look:
| plate-auth surface | Sparkboard usage |
|---|---|
| `de.platesoft:plate-auth-starter` Maven artifact | Declared in `backend/pom.xml` |
| `plate.auth.jwt.secret` config | Set from env var `PLATE_AUTH_JWT_SECRET` |
| `plate.auth.exchange.secret` config | Set from env var `PLATE_AUTH_EXCHANGE_SECRET` |
| `plate.auth.allowlist.emails` config | List of 4 emails, env-substituted |
| `plate.auth.providers.google.{client-id, client-secret}` config | Env-substituted |
| `plate.auth.registration.enabled = false` | Explicit |
| `OnboardingHook` SPI | Implemented by `SparkboardOnboardingHook` |
| `MembershipService.upsert(userId, orgType, orgId, role)` API | Called from the hook |
| `Role` enum (`ADMIN`, `MEMBER`) | Used by the hook |
| `@platesoft/auth` npm artifact | Declared in `frontend/package.json` |
| `createAuthConfig(...)` factory | Called from `frontend/auth.ts` |
| `createProxyHandlers(...)` factory | Called from `frontend/app/api/backend/[...path]/route.ts` |
| `flyway_schema_history_auth` table | Owned by plate-auth; Sparkboard's Flyway never touches it |
Untouched in v1 (deliberately deferred or unneeded):
- `OrgValidator` SPI (default no-op accepted)
- `OrgDisplayNameResolver` SPI (default `org_id` rendering accepted)
- `InvitationMailer` SPI (no invitations in v1)
- `AccessRequestMailer` SPI (no access requests in v1)
- React hooks from `@platesoft/auth/react` (Sprint 2 candidate if needed for profile page)
- `/api/auth/invite` / `/api/auth/access-requests` HTTP endpoints (not called)
---
## 9. Out of scope (deferred to Sprint 2+)
| Item | Sprint |
|------|--------|
| Reactions (💡⚡🔥❓👀) | 2 (Kindling) |
| Status transitions in UI | 2 |
| Idea archive / soft-delete UI | 2 |
| Idea edit | 2 |
| Idea detail page | 2 |
| Profile / account-settings page | 2 |
| Comments | 3 (Flame) |
| Search | 3 |
| Notifications (email / web-push) | 4 (Ember) |
| Offline cache (real SW caching) | 4 |
| Native APK (Capacitor) | 5 (Wildfire) |
| Image attachments | 5 |
---
## 10. Open questions
Cross-referenced to [Open Questions](Open-Questions.md). The four that **must** be answered before Sprint 1 implementation starts:
- [Q01 — Single-org name and ID strategy](Open-Questions.md#q01-single-org-name-and-id-strategy) — what is the magic UUID? "Family Spark" the display name?
- [Q02 — Allowlist management](Open-Questions.md#q02-allowlist-management) — env var? config file? DB seed? admin endpoint?
- [Q03 — Admin promotion model](Open-Questions.md#q03-admin-promotion-model) — auto on first login? Flyway seed? `sparkboard.admins[]` config?
- [Q07 — PWA assets pipeline](Open-Questions.md#q07-pwa-assets-pipeline) — hand-drawn icons? generator? defer to Sprint 2?
The other six on the [Open Questions](Open-Questions.md) page affect Sprint 2+ and do not block Sprint 1.
---
## 11. Verdict
**Recommendation:** APPROVE Option A, A1A6 acceptance, with [Q01Q03 and Q07](#10-open-questions) answered before W2 of Sprint 1 starts.
**Next document:** [Sprint 1 — Plan](Sprint-1-Plan.md) breaks the recommendation into workstreams W0W6.