docs(sprint-1-plan, chunk 1/4): background, repo layout, W0 skeleton, W1 plate-auth wire-up

Patrick Plate
2026-06-24 14:49:29 +02:00
parent 7ccdfcfc70
commit 3b88b603b1
+374
@@ -0,0 +1,374 @@
# Sprint 1 — Plan ("Spark")
**Date:** Pre-Sprint 1 planning
**Module:** Sparkboard (greenfield)
**Author:** Patrick Plate / Lumen (Planner)
**Status:** Entwurf v1
**Basis:** [Sprint 1 Assessment](Sprint-1-Assessment.md) — Option A approved
**Predecessor:** [plate-auth v0.1.0](https://git.plate-software.de/pplate/plate-auth/wiki/Roadmap#v010--ship-sprint-0-deliverable) must be published before W2.
---
## 0. How to read this plan
The plan is broken into **seven workstreams** (W0W6). They are partially parallel — W0/W1 must complete before W2, but W2/W3/W4 can be developed in parallel by the same human, and W5/W6 are tail-end. Each workstream lists:
- **Goal** — what the workstream produces
- **Deliverables** — files / artifacts that must exist
- **Acceptance gate** — what proves the workstream is done
- **Code sketches** — illustrative, not final
The acceptance criteria A1A6 from the [Assessment](Sprint-1-Assessment.md#7-acceptance-criteria-a1a6) are mapped to workstreams in the [implementation order](Sprint-1-Plan-Part-4.md#implementation-order) section at the end.
Code paths shown as `[`backend/...`](../backend/...)` will resolve once the `sparkboard` code repo is cloned alongside this wiki.
---
## 1. Background
Sparkboard does not exist yet. There is no codebase, no DB, no DNS. Sprint 1 builds the walking skeleton: a Spring Boot 4.1 + Java 25 backend consuming `plate-auth-starter:0.1.0`, a Next.js 15 frontend consuming `@platesoft/auth:0.1.0`, a Postgres database, and a deployment to `sparkboard.plate-software.de`.
The whole sprint is an integration exercise. The product surface (one entity, two endpoints, two pages) is intentionally tiny so that all the difficulty surfaces in the integration. See [Assessment §1](Sprint-1-Assessment.md#1-problem-analysis) for the strategic reasoning.
---
## 2. Architecture summary
See [Architecture](Architecture.md) for the full picture. Sprint 1 instantiates exactly one section of it:
- One Spring Boot app: `de.plate.sparkboard.SparkboardApplication` — embeds plate-auth's auto-config.
- One Postgres database: `sparkboard` — with two Flyway histories side-by-side (`flyway_schema_history` for Sparkboard's tables, `flyway_schema_history_auth` for plate-auth's).
- One Next.js app: `frontend/` — embeds `@platesoft/auth/next-auth` factory + `@platesoft/auth/proxy` handlers.
- One Sparkboard SPI implementation: `SparkboardOnboardingHook`.
- One Sparkboard domain table: `ideas` (and one Sparkboard-owned reference table: `spark_org`).
- Deployed to TrueNAS via Gitea Actions, exposed via frps port 30011 + IONOS Apache.
---
## 3. Repository layout
```
sparkboard/
├── README.md
├── docker-compose.yml # local dev: backend + frontend + postgres
├── docker-compose.prod.yml # TrueNAS: backend + frontend + postgres + caddy/apache
├── .gitea/workflows/
│ ├── ci.yml # build + test on every push
│ └── deploy.yml # SSH deploy to TrueNAS on main
├── deploy/
│ ├── caddy/Caddyfile # internal reverse proxy on TrueNAS
│ ├── deploy.sh # SSH-driven deploy
│ └── smoke-test.sh # post-deploy health checks
├── backend/
│ ├── pom.xml
│ ├── Dockerfile
│ └── src/main/
│ ├── java/de/plate/sparkboard/
│ │ ├── SparkboardApplication.java
│ │ ├── onboarding/{SparkboardOnboardingHook, SparkboardAdminProperties}.java
│ │ ├── idea/{Idea, IdeaStatus, IdeaRepository, IdeaService, IdeaController, IdeaDto, CreateIdeaRequest}.java
│ │ └── config/SparkboardSecurityCustomizations.java # optional
│ └── resources/
│ ├── application.yml
│ ├── application-prod.yml
│ └── db/migration/
│ ├── V1__init.sql # ideas + spark_org tables
│ └── V2__seed_family_spark_org.sql
└── frontend/
├── package.json # depends on @platesoft/auth: 0.1.0
├── next.config.ts
├── auth.ts # createAuthConfig from @platesoft/auth/next-auth
├── middleware.ts # optional re-export
├── Dockerfile
└── app/
├── layout.tsx
├── globals.css
├── api/
│ ├── auth/[...nextauth]/route.ts # exports handlers from auth.ts
│ └── backend/[...path]/route.ts # createProxyHandlers
├── (auth)/login/page.tsx
└── (app)/
├── layout.tsx
├── page.tsx # redirect to /ideas
└── ideas/
├── page.tsx # list (server component)
├── new/page.tsx # create form
└── components/{idea-form, idea-list, idea-card}.tsx
```
---
## 4. Workstreams
### W0 — Skeleton
**Goal:** an empty but buildable Sparkboard. `mvn package` produces a runnable JAR. `pnpm build` produces a Next.js standalone build. CI runs both on every push.
**Deliverables:**
1. Gitea repo `pplate/sparkboard` created.
2. `backend/pom.xml` declaring Spring Boot 4.1.0 parent, Java 25 source/target, Maven build.
3. `backend/src/main/java/de/plate/sparkboard/SparkboardApplication.java` — empty `@SpringBootApplication`.
4. `frontend/package.json` declaring Next.js 15, React 19, TypeScript 5.
5. `frontend/app/page.tsx` — placeholder "Sparkboard" string.
6. `docker-compose.yml` for local dev.
7. `.gitea/workflows/ci.yml` building both subprojects.
8. `README.md` with quick-start.
**Acceptance gate:**
- `git push` triggers CI; CI builds backend + frontend in < 5 min wall-clock; CI is **green**.
**Code sketch — backend `pom.xml` excerpt:**
```xml
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>de.plate</groupId>
<artifactId>sparkboard-backend</artifactId>
<version>0.1.0-SNAPSHOT</version>
<properties>
<java.version>25</java.version>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<repositories>
<repository>
<id>plate-software-gitea</id>
<url>https://git.plate-software.de/api/packages/platesoft/maven</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<!-- THE one dependency that makes Sparkboard a plate-auth consumer -->
<dependency>
<groupId>de.platesoft</groupId>
<artifactId>plate-auth-starter</artifactId>
<version>0.1.0</version>
</dependency>
<!-- test scope -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.20.6</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
```
**Code sketch — frontend `package.json` excerpt:**
```json
{
"name": "sparkboard-frontend",
"version": "0.1.0",
"scripts": {
"dev": "next dev --port 3000",
"build": "next build",
"start": "node .next/standalone/server.js",
"lint": "next lint"
},
"dependencies": {
"@platesoft/auth": "0.1.0",
"next": "15.1.0",
"next-auth": "5.0.0-beta.25",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@playwright/test": "1.50.0",
"tailwindcss": "3.4.17",
"typescript": "5.6.3"
}
}
```
**Code sketch — `.gitea/workflows/ci.yml`:**
```yaml
name: CI
on:
push:
branches: [main, '**']
pull_request:
jobs:
backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
- run: mvn -B verify
working-directory: backend
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
working-directory: frontend
- run: pnpm build
working-directory: frontend
```
---
### W1 — plate-auth wire-up
**Goal:** the four allowlisted Google users can sign in. Sparkboard has not yet implemented `OnboardingHook`, so a successful login leaves them with `auth_identities` row but no `memberships` row — they are signed in but "homeless". The list page will look empty until W2 ships the hook.
**Pre-requisite:** [plate-auth v0.1.0](https://git.plate-software.de/pplate/plate-auth/wiki/Roadmap) is **published and pullable** from `https://git.plate-software.de/api/packages/platesoft/maven` and `https://git.plate-software.de/api/packages/platesoft/npm`. If this is not the case at the start of W1, **stop and ping plate-auth Sprint 0**.
**Deliverables:**
1. `backend/src/main/resources/application.yml` with `plate.auth.*` block — see [Architecture §10.1](Architecture.md#101-backend-applicationyml) for the full block.
2. `backend/src/main/resources/application-prod.yml` overriding env-derived values for production.
3. `frontend/auth.ts` calling `createAuthConfig({ ... })` from `@platesoft/auth/next-auth`.
4. `frontend/app/api/auth/[...nextauth]/route.ts` exporting the NextAuth handlers from `auth.ts`.
5. `frontend/app/api/backend/[...path]/route.ts` calling `createProxyHandlers({ ... })` from `@platesoft/auth/proxy`.
6. `frontend/app/(auth)/login/page.tsx` with a "Sign in with Google" button (calls `signIn('google')`).
7. Google Cloud Console: OAuth client with redirect URIs:
- `http://localhost:3000/api/auth/callback/google` (dev)
- `https://sparkboard.plate-software.de/api/auth/callback/google` (prod)
8. Local `.env.local` and TrueNAS `/mnt/tank/sparkboard/.env` with all required secrets — see [Integration Guide §2](Integration-Guide.md#2-secrets-and-env-vars).
**Acceptance gate:**
- A Gmail account in `plate.auth.allowlist.emails` can sign in **locally** (`http://localhost:3000`) and reach `/ideas` (which is still placeholder content).
- A Gmail account NOT in the allowlist gets a "not authorised" message and **no row is created** in `auth_identities`.
- The `auth_identities` table contains one row per successful login (idempotent on subsequent logins).
- The `memberships` table is **empty** (W2 will fill it).
- This satisfies **A1** (allowlisted user can sign in) and **A2** (non-allowlisted user is rejected) **except** for the post-login `/ideas` page being meaningful — that part of A1 lands in W4.
**Code sketch — `frontend/auth.ts`:**
```typescript
import NextAuth from "next-auth";
import { createAuthConfig } from "@platesoft/auth/next-auth";
export const { handlers, auth, signIn, signOut } = NextAuth(
createAuthConfig({
backendUrl: process.env.PLATE_AUTH_BACKEND_URL!, // http://backend:8080
exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!,
nextAuthSecret: process.env.NEXTAUTH_SECRET!,
providers: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
pages: { signIn: "/login" },
})
);
```
**Code sketch — `frontend/app/api/auth/[...nextauth]/route.ts`:**
```typescript
export { handlers as GET, handlers as POST } from "@/auth";
```
**Code sketch — `frontend/app/api/backend/[...path]/route.ts`:**
```typescript
import { createProxyHandlers } from "@platesoft/auth/proxy";
export const { GET, POST, PUT, PATCH, DELETE } = createProxyHandlers({
backendUrl: process.env.PLATE_AUTH_BACKEND_URL!,
exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!,
});
export const runtime = "nodejs"; // proxy requires Node runtime (duplex: 'half')
```
**Code sketch — `frontend/app/(auth)/login/page.tsx`:**
```tsx
"use client";
import { signIn } from "next-auth/react";
export default function LoginPage() {
return (
<main className="min-h-screen flex items-center justify-center">
<button
onClick={() => signIn("google", { callbackUrl: "/ideas" })}
className="rounded-lg bg-black px-6 py-3 text-white"
>
Sign in with Google
</button>
</main>
);
}
```
**Code sketch — `backend/src/main/resources/application.yml` (plate.auth excerpt only):**
```yaml
plate:
auth:
jwt:
secret: ${PLATE_AUTH_JWT_SECRET}
access-expiration: PT15M
refresh-expiration: P30D
exchange:
secret: ${PLATE_AUTH_EXCHANGE_SECRET}
registration:
enabled: false
allowlist:
enabled: true
emails: ${PLATE_AUTH_ALLOWLIST_EMAILS} # comma-separated env var
providers:
google:
enabled: true
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
```
Note: zero Java code is written in W1. Everything is configuration and frontend wiring. **That is the win.**
---
_Continued in [Sprint-1-Plan-Part-2](Sprint-1-Plan-Part-2.md): W2 (domain) and W3 (API)._
_Continued in [Sprint-1-Plan-Part-3](Sprint-1-Plan-Part-3.md): W4 (frontend), W5 (seed data), W6 (deploy + CI/CD)._
_Continued in [Sprint-1-Plan-Part-4](Sprint-1-Plan-Part-4.md): implementation order, acceptance mapping, open questions._