Sprint 1 — Plan ("Spark")
Date: Pre-Sprint 1 planning Module: Sparkboard (greenfield) Author: Patrick Plate / Lumen (Planner) Status: Entwurf v1 Basis: Sprint 1 Assessment — Option A approved Predecessor: plate-auth v0.1.0 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 are mapped to workstreams in the 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 for the strategic reasoning.
2. Architecture summary
See Architecture 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_historyfor Sparkboard's tables,flyway_schema_history_authfor plate-auth's). - One Next.js app:
frontend/— embeds@platesoft/auth/next-authfactory +@platesoft/auth/proxyhandlers. - 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:
- Gitea repo
pplate/sparkboardcreated. backend/pom.xmldeclaring Spring Boot 4.1.0 parent, Java 25 source/target, Maven build.backend/src/main/java/de/plate/sparkboard/SparkboardApplication.java— empty@SpringBootApplication.frontend/package.jsondeclaring Next.js 15, React 19, TypeScript 5.frontend/app/page.tsx— placeholder "Sparkboard" string.docker-compose.ymlfor local dev..gitea/workflows/ci.ymlbuilding both subprojects.README.mdwith quick-start.
Acceptance gate:
git pushtriggers CI; CI builds backend + frontend in < 5 min wall-clock; CI is green.
Code sketch — backend pom.xml excerpt:
<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:
{
"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:
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 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:
backend/src/main/resources/application.ymlwithplate.auth.*block — see Architecture §10.1 for the full block.backend/src/main/resources/application-prod.ymloverriding env-derived values for production.frontend/auth.tscallingcreateAuthConfig({ ... })from@platesoft/auth/next-auth.frontend/app/api/auth/[...nextauth]/route.tsexporting the NextAuth handlers fromauth.ts.frontend/app/api/backend/[...path]/route.tscallingcreateProxyHandlers({ ... })from@platesoft/auth/proxy.frontend/app/(auth)/login/page.tsxwith a "Sign in with Google" button (callssignIn('google')).- 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)
- Local
.env.localand TrueNAS/mnt/tank/sparkboard/.envwith all required secrets — see Integration Guide §2.
Acceptance gate:
- A Gmail account in
plate.auth.allowlist.emailscan 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_identitiestable contains one row per successful login (idempotent on subsequent logins). - The
membershipstable 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
/ideaspage being meaningful — that part of A1 lands in W4.
Code sketch — frontend/auth.ts:
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:
export { handlers as GET, handlers as POST } from "@/auth";
Code sketch — frontend/app/api/backend/[...path]/route.ts:
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:
"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):
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: W2 (domain) and W3 (API). Continued in Sprint-1-Plan-Part-3: W4 (frontend), W5 (seed data), W6 (deploy + CI/CD). Continued in Sprint-1-Plan-Part-4: implementation order, acceptance mapping, open questions.