docs(sprint-1-plan, chunk 1/4): background, repo layout, W0 skeleton, W1 plate-auth wire-up
+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** (W0–W6). 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 A1–A6 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._
|
||||
Reference in New Issue
Block a user