1
Sprint 1 Plan
Patrick Plate edited this page 2026-06-24 14:49:29 +02:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (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 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_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:

<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:

  1. backend/src/main/resources/application.yml with plate.auth.* block — see Architecture §10.1 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.

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:

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.