5
Sprint 0 Plan
Patrick Plate edited this page 2026-06-24 15:23:58 +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 0 — Implementation Plan

Status: Draft v1 Date: 2026-06-24 Owner: Patrick (plate-software) Based on: Sprint-0-Assessment.md, Architecture.md Target version: 0.1.0 (both Maven + npm)


Reading guide

This is the single longest document in the wiki. It is structured as:

  1. Scope + ground rules (this section)
  2. Workstream overview (the 7 parallel-ish workstreams)
  3. Backend extraction — step-by-step
  4. Frontend extraction — step-by-step
  5. Flyway migration consolidation
  6. Build + publishing pipeline
  7. Security review checklist (must pass before v0.1.0 tag)
  8. Rollout plan (InspectFlow + Sparkboard adoption)
  9. Acceptance criteria
  10. Open items deferred to v0.2+

Each step is numbered so the Code-mode worker can check off progress without ambiguity.


1. Scope + ground rules

1.1 In scope (Sprint 0)

  • New git repo plate-auth (already created — git.plate-software.de/pplate/plate-auth)
  • Backend artifact de.platesoft:plate-auth-starter:0.1.0 (Maven, Spring Boot 4 starter)
  • Frontend artifact @platesoft/auth@0.1.0 (npm, NextAuth v5 wiring)
  • 5 SPIs (OrgValidator, OrgDisplayNameResolver, InvitationMailer, AccessRequestMailer, OnboardingHook)
  • Flyway migrations under db/migration/auth/V1..V6 (6 migrations — see § 5 and Architecture.md § 8.1 for the canonical list)
  • Gitea Actions pipeline that publishes both artifacts on a v* git tag
  • Internal integration tests covering exchange + JWT + memberships
  • All 10 wiki docs reviewed + approved by Plan Reviewer + GO from Patrick

1.2 Out of scope (deferred — see Roadmap.md)

  • WebAuthn / passkeys (v0.2+)
  • Multi-replica nonce store (v0.3)
  • Refresh-token rotation table (v0.3)
  • JWT secret rotation via kid (v0.3)
  • External audit-log sink (v0.3)
  • LoginEventSink SPI (v0.3)
  • SAML, SCIM, OIDC server, mobile SDKs (post-1.0, demand-driven)

1.3 Ground rules

  • No new features. This sprint is an extraction. Anything beyond what InspectFlow already does in Sprint 14.114.6 is automatic v0.2 fodder.
  • Single git repo, two artifacts. Maven module + npm package live in one repo with a shared CHANGELOG.md and lockstep versions.
  • Tests must pass on day 1. No "TODO: write tests later" lines in committed code.
  • Plan Reviewer is mandatory before code. No code starts until the plan is APPROVED.
  • Branch policy: feature/sprint-0/<workstream> branches → PR → squash-merge to main.

2. Workstream overview

flowchart LR
    W1[W1: Repo scaffolding<br/>+ CI skeleton] --> W2[W2: Backend extraction<br/>code rename + repackage]
    W1 --> W3[W3: Frontend extraction<br/>factories + types]
    W2 --> W4[W4: SPI design + impl]
    W2 --> W5[W5: Flyway consolidation]
    W3 --> W6[W6: Build + publish pipeline]
    W4 --> W6
    W5 --> W6
    W6 --> W7[W7: Integration tests +<br/>InspectFlow dry-run]
# Workstream Owner Depends on Estimate
W1 Repo scaffolding (Maven + npm structure, CI skeleton, README) Code mode 0.5d
W2 Backend extraction — class moves, package renames, config namespace Code mode W1 1.5d
W3 Frontend extraction — createAuthConfig factory, proxy factory, types Code mode W1 1d
W4 SPI design — 5 interfaces + default no-op implementations + @ConditionalOnMissingBean wiring Code mode W2 1d
W5 Flyway migration consolidation + flyway_schema_history_auth decision Code mode W2 0.5d
W6 Gitea Actions publish pipeline — Maven + npm to Gitea Package Registry Code mode W2, W3, W4, W5 0.5d
W7 Integration tests + InspectFlow dry-run migration Code mode All 1.5d
Total ~6.5 code days + plan + review buffer = ~910 calendar days

3. Backend extraction — overview

See § 4 for step-by-step.

The backend extraction is fundamentally:

  1. Move every class in inspectflow/backend/src/main/java/de/platesoft/inspectflow/{config,filter,service,controller,entity,repository,dto,enums}/... that is auth-related into plate-auth/plate-auth-starter/src/main/java/de/platesoft/auth/...
  2. Rename every import, @Value("${jwt....}")@Value("${plate.auth.jwt....}") etc.
  3. Replace every direct companies repository call inside MembershipService etc. with a call through OrgValidator / OrgDisplayNameResolver SPI.
  4. Add PlateAuthAutoConfiguration + PlateAuthProperties + META-INF/spring/...AutoConfiguration.imports.
  5. Add default no-op SPI implementations annotated @ConditionalOnMissingBean.

No new domain logic. No new endpoints. No new entity fields.


4. Frontend extraction — overview

See § 6 for step-by-step.

The frontend extraction is:

  1. Move the NextAuth config from inspectflow/frontend/lib/auth-config.ts into a createAuthConfig(opts) factory in plate-auth/packages/auth/src/config/index.ts
  2. Move the proxy logic from inspectflow/frontend/app/api/[...path]/route.ts into a createProxyHandlers(opts) factory
  3. Move the HMAC exchange logic from inspectflow/frontend/lib/exchange.ts into plate-auth/packages/auth/src/exchange/
  4. Replace the InspectFlow-specific localStorage keys with configurable prefixes
  5. Export typed hooks (useAccessToken, useMemberships) and re-export NextAuth's useSession, signIn, signOut

5. Flyway strategy — overview

See § 7 for step-by-step.

Decision (to be ratified by Patrick — see Open-Questions.md Q03):

  • plate-auth ships migrations under classpath:db/migration/auth/V1..V5
  • Consumers configure Flyway with multiple locations + separate history table for plate-auth:
    spring.flyway.locations=classpath:db/migration,classpath:db/migration/auth
    # Default app history table stays "flyway_schema_history"
    # plate-auth's history is tracked in the SAME table but with V1..V5 from auth/ prefixed via
    # an installed_rank shift OR — preferred — a *second* Flyway managed by plate-auth's auto-config.
    
  • For InspectFlow, V26V31 already ran. We do not re-run them. Migration recipe:
    • Insert a "baseline" row into flyway_schema_history_auth marking V1..V5 as already applied
    • Detailed steps in Migration-InspectFlow.md

Final pattern is locked in § 7. The Plan Reviewer must approve the chosen approach before code starts.


6. Build + publish pipeline — overview

See § 8 for step-by-step.

  • Single repo. mvn -pl plate-auth-starter package builds the JAR. npm -w @platesoft/auth build builds the npm package.
  • A git tag v0.1.0 triggers a Gitea Actions workflow that:
    1. Builds + tests both artifacts
    2. Publishes the JAR to Gitea Package Registry (Maven)
    3. Publishes the npm tarball to Gitea Package Registry (npm)
    4. Updates the CHANGELOG.md with the release date
  • Snapshot builds (on every push to main) publish to 0.1.0-SNAPSHOT / 0.1.0-snapshot.N.

Plan continues in the next section — backend extraction step-by-step.


4. Backend extraction — step-by-step

4.1 W1 — Repo scaffolding

Goal: Create the directory layout + skeleton pom.xml files + npm workspace structure.

Steps:

  1. W1-1 In the plate-auth repo root, create:
    plate-auth/
    ├── pom.xml                          # parent POM, packaging=pom
    ├── plate-auth-starter/
    │   ├── pom.xml                      # the published artifact
    │   └── src/{main,test}/{java,resources}/
    ├── packages/
    │   └── auth/
    │       ├── package.json             # @platesoft/auth
    │       └── src/
    ├── package.json                     # workspace root
    ├── pnpm-workspace.yaml              # if pnpm; npm/yarn equivalent works
    ├── .gitea/workflows/                # Gitea Actions
    ├── CHANGELOG.md
    └── README.md
    
  2. W1-2 Parent pom.xml: <groupId>de.platesoft</groupId>, <artifactId>plate-auth-parent</artifactId>, <version>${revision}</version> (CI-injected), <packaging>pom</packaging>, <modules><module>plate-auth-starter</module></modules>. Inherit from spring-boot-starter-parent:4.1.0.
  3. W1-3 plate-auth-starter/pom.xml: <artifactId>plate-auth-starter</artifactId>, deps copied from InspectFlow backend/pom.xml minus app-specific (no ONNX, no openpdf, no pdfbox):
    web, data-jpa, security, validation, actuator, mail,
    hibernate-envers, postgresql, flyway,
    jjwt-api/impl/jackson,
    mapstruct, lombok, logstash-logback,
    h2 (test scope), testcontainers (test scope)
    
  4. W1-4 Root package.json + pnpm-workspace.yaml configured to pick up packages/*.
  5. W1-5 packages/auth/package.json: "name": "@platesoft/auth", "version": "0.1.0", peerDeps on next@>=15, next-auth@^5.0.0-beta, react@>=19.
  6. W1-6 Add .gitignore, .editorconfig, basic README.md linking to the wiki.
  7. W1-7 Commit + push to main. CI must pass on empty modules (just compiles).

Done when: mvn -B verify and pnpm -r build both succeed on a fresh clone.

4.2 W2-A — Backend: copy + rename classes

Goal: Mechanical move from de.platesoft.inspectflow.*de.platesoft.auth.* for every auth class.

Steps (per source class — repeat for the inventory in Sprint-0-Assessment § 1.1):

  1. W2-1 Copy class file from inspectflow/backend/src/main/java/de/platesoft/inspectflow/<pkg>/<X>.java to plate-auth/plate-auth-starter/src/main/java/de/platesoft/auth/<pkg>/<X>.java.
  2. W2-2 Update package declaration: package de.platesoft.auth.<pkg>;.
  3. W2-3 Update internal imports (i.e. imports of other classes we are also moving) to the new package. Use search-replace, but verify each file compiles independently after.
  4. W2-4 Leave behind: any reference to Company, OnboardingService, TenantAutoMapService — replace with an SPI call (see W4 below). DO NOT move these classes.

Class-by-class checklist:

  • filter/JwtAuthenticationFilter.java — direct move, no rewrites
  • filter/OrgContextResolver.java — direct move
  • config/SecurityConfig.java — direct move, but: replace InspectFlow-specific permit-list (/api/companies/*/public-info) with config-driven list from PlateAuthProperties.cors.additionalPermitPaths
  • service/JwtService.java — direct move; rename @Value("${jwt.secret}")@Value("${plate.auth.jwt.secret}")
  • service/OAuthService.java — move + swap tenantAutoMapService.maybeAutoMap(...) call for onboardingHook.onFirstSignIn(...) (SPI)
  • service/ExchangeService.java — direct move
  • service/MembershipService.java — move + swap companyRepository.findById(...) for orgValidator.exists(orgType, orgId) / orgDisplayNameResolver.displayName(...) calls
  • service/InvitationService.java — direct move (Sprint 14.3 already abstracted the mailer)
  • service/AccessRequestService.java — direct move
  • service/LoginEventService.java — direct move
  • service/AuthService.java — direct move (password login/register)
  • controller/AuthController.java — direct move
  • controller/OAuthController.java — direct move
  • controller/InvitationController.java — direct move
  • controller/AccessRequestController.java — direct move
  • controller/AdminAuditController.java — direct move
  • entity/User.java — direct move
  • entity/UserIdentity.java — direct move
  • entity/Membership.java — direct move
  • entity/Invitation.java — direct move
  • entity/AccessRequest.java — direct move
  • entity/LoginEvent.java — direct move
  • entity/RevInfo.java (Envers revinfo with actor) — direct move
  • repository/* — all auth-related repositories — direct move
  • dto/request/* and dto/response/* — direct move
  • enums/* — direct move (Role, OrgType, MembershipRole, MembershipStatus, InvitationStatus, AccessRequestStatus, LoginProvider)

Done when: plate-auth-starter compiles in isolation. No references to de.platesoft.inspectflow.* remain in moved classes.

4.3 W2-B — Configuration namespace

Goal: Consolidate all @Value("${...}") injections into a single @ConfigurationProperties("plate.auth") class.

Steps:

  1. W2-8 Create de.platesoft.auth.PlateAuthProperties:
    @ConfigurationProperties(prefix = "plate.auth")
    @Data
    public class PlateAuthProperties {
        private Jwt jwt = new Jwt();
        private Exchange exchange = new Exchange();
        private Registration registration = new Registration();
        private Cors cors = new Cors();
        private Providers providers = new Providers();
    
        @Data public static class Jwt {
            private String secret;
            private Duration accessExpiration = Duration.ofMinutes(15);
            private Duration refreshExpiration = Duration.ofDays(30);
            private String issuer = "plate-auth";
        }
        @Data public static class Exchange {
            private String secret;
            private Duration maxAge = Duration.ofSeconds(60);
            private Duration nonceTtl = Duration.ofMinutes(5);
        }
        @Data public static class Registration {
            private boolean enabled = false;
        }
        @Data public static class Cors {
            private List<String> allowedOrigins = new ArrayList<>();
            private List<String> additionalPermitPaths = new ArrayList<>();
        }
        @Data public static class Providers {
            private ProviderToggle google = new ProviderToggle(true);
            private ProviderToggle microsoft = new ProviderToggle(false);
            private ProviderToggle emailMagicLink = new ProviderToggle(false);
        }
        @Data @AllArgsConstructor @NoArgsConstructor public static class ProviderToggle {
            private boolean enabled;
        }
    }
    
  2. W2-9 Inject PlateAuthProperties (constructor injection) into every service that previously read @Value("${jwt....}") etc. Replace @Value annotations.
  3. W2-10 Add META-INF/spring-configuration-metadata.json (or rely on spring-boot-configuration-processor annotation processor — preferred — add it to the build).
  4. W2-11 Bean-validate critical fields:
    @NotBlank @Size(min=32) private String secret;       // both jwt.secret and exchange.secret
    

Done when: No @Value("${jwt....}") or @Value("${nextauth....}") strings remain. All config flows through PlateAuthProperties. App fails fast at startup if secret is missing or <32 chars.

4.4 W2-C — Auto-configuration

Goal: Make the starter "just work" when added as a dependency.

Steps:

  1. W2-12 Create de.platesoft.auth.PlateAuthAutoConfiguration:
    @AutoConfiguration
    @EnableConfigurationProperties(PlateAuthProperties.class)
    @ComponentScan(basePackages = "de.platesoft.auth")
    @EntityScan(basePackages = "de.platesoft.auth.entity")
    @EnableJpaRepositories(basePackages = "de.platesoft.auth.repository")
    @ConditionalOnProperty(prefix = "plate.auth", name = "enabled", havingValue = "true", matchIfMissing = true)
    public class PlateAuthAutoConfiguration {
        // Default SPI beans, ConditionalOnMissingBean — see W4
    }
    
  2. W2-13 Register the auto-config:
    src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    
    Contents: single line de.platesoft.auth.PlateAuthAutoConfiguration.
  3. W2-14 Verify that adding the starter to a minimal Spring Boot 4 app, setting plate.auth.jwt.secret
    • plate.auth.exchange.secret env vars, and spring.datasource.url to a Postgres, makes the app boot and serve /api/auth/config.

Done when: Smoke test app (in plate-auth-tests/) boots and responds to /api/auth/config.

4.5 W4 — SPI design + default implementations

Goal: Five SPI interfaces with sensible defaults so consumers can override selectively.

Steps:

  1. W4-1 Create the 5 SPI interfaces under de.platesoft.auth.spi:
    public interface OrgValidator {
        boolean exists(OrgType type, UUID orgId);
    }
    public interface OrgDisplayNameResolver {
        String displayName(OrgType type, UUID orgId);
    }
    public interface InvitationMailer {
        void sendInvitation(Invitation invitation, String acceptUrl);
    }
    public interface AccessRequestMailer {
        void notifyAdmins(AccessRequest request);
        void notifyRequester(AccessRequest request);  // on decision
    }
    public interface OnboardingHook {
        void onFirstSignIn(User user, LoginProvider provider);
        default void onSubsequentSignIn(User user, LoginProvider provider) { /* no-op */ }
    }
    
  2. W4-2 Default implementations (annotated @ConditionalOnMissingBean, registered in PlateAuthAutoConfiguration):
    • PermissiveOrgValidatorships as the default. exists(...) always returns true and logs a WARN on every call: "OrgValidator default permissive — override de.platesoft.auth.spi.OrgValidator bean before production". Rationale: the starter must boot green with zero consumer code; the per-call WARN makes it impossible to ship to production without noticing that real validation is missing.
    • DefaultOrgDisplayNameResolver — returns type + ":" + orgId.toString()
    • LoggingInvitationMailer — logs the accept URL at INFO level
    • LoggingAccessRequestMailer — logs notifications at INFO level
    • NoOpOnboardingHook — no-op
  3. W4-3 Wire each service to its SPI dep via constructor injection:
    • MembershipServiceOrgValidator, OrgDisplayNameResolver
    • InvitationServiceInvitationMailer, OrgDisplayNameResolver
    • AccessRequestServiceAccessRequestMailer, OrgDisplayNameResolver
    • OAuthServiceOnboardingHook
  4. W4-4 Document each SPI with Javadoc including:
    • When it is called
    • What happens if the consumer doesn't provide one (which default kicks in)
    • Migration: if a previous version's signature changed, link to CHANGELOG

Done when: Starter boots green with zero SPI beans (default PermissiveOrgValidator accepts all (org_type, org_id) and emits per-call WARN). A consumer can register a single OrgValidator bean to replace it and have T2 fully validated. Default mailers log instead of crashing.


Plan continues — frontend extraction, Flyway, publishing.


6. Frontend extraction — step-by-step

6.1 W3-A — npm package skeleton

Goal: A buildable, publishable @platesoft/auth@0.1.0 with TypeScript + ESM/CJS dual build.

Steps:

  1. W3-1 Configure packages/auth/package.json:
    {
      "name": "@platesoft/auth",
      "version": "0.1.0",
      "type": "module",
      "main": "./dist/index.cjs",
      "module": "./dist/index.js",
      "types": "./dist/index.d.ts",
      "exports": {
        ".":           { "import": "./dist/index.js",       "require": "./dist/index.cjs", "types": "./dist/index.d.ts" },
        "./config":    { "import": "./dist/config/index.js",    "types": "./dist/config/index.d.ts" },
        "./exchange":  { "import": "./dist/exchange/index.js",  "types": "./dist/exchange/index.d.ts" },
        "./proxy":     { "import": "./dist/proxy/index.js",     "types": "./dist/proxy/index.d.ts" },
        "./middleware":{ "import": "./dist/middleware/index.js","types": "./dist/middleware/index.d.ts" },
        "./client":    { "import": "./dist/client/index.js",    "types": "./dist/client/index.d.ts" }
      },
      "peerDependencies": {
        "next":      ">=15.0.0",
        "next-auth": "^5.0.0-beta",
        "react":     ">=19.0.0"
      },
      "files": ["dist", "README.md", "LICENSE"]
    }
    
  2. W3-2 Bundler choice: tsup (zero-config dual ESM/CJS, fast). Add tsup.config.ts targeting Node 20 + Edge runtime.
  3. W3-3 TypeScript strict config, "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "declaration": true.
  4. W3-4 Add a publishConfig block pointing to the Gitea npm registry (set in W6).

Done when: pnpm -F @platesoft/auth build produces dist/ with ESM + CJS + .d.ts files.

6.2 W3-B — Move + factor frontend code

Steps (per file from inspectflow/frontend/):

  1. W3-5 Copy frontend/lib/exchange.tspackages/auth/src/exchange/client.ts.
    • Replace import { ... } from "@/lib/..." patterns with relative imports inside the package.
    • Extract the envelope-signing logic into packages/auth/src/exchange/envelope.ts:
      export interface ExchangeEnvelope {
        provider: 'google' | 'microsoft' | 'email' | 'password';
        providerSubject: string;
        email: string;
        name?: string;
        inviteToken?: string;
        nonce: string;
        iat: number;          // unix seconds
      }
      export function signEnvelope(env: ExchangeEnvelope, secret: string):
        { envelope: string; signature: string };
      export function makeNonce(): string;            // crypto.randomUUID()
      
    • Use Web Crypto API (crypto.subtle.importKey + sign("HMAC", ...)) so the code runs in the Edge runtime as well as Node.
  2. W3-6 Copy frontend/lib/auth-config.tspackages/auth/src/config/index.ts.
    • Refactor into a factory:
      export interface PlateAuthConfigOptions {
        providers: { google?: GoogleOpts; microsoft?: MicrosoftOpts; email?: EmailOpts };
        exchange: { backendUrl: string; secret: string; appLabel?: string };
        session?: { strategy?: 'jwt'; maxAge?: number };
        callbacks?: { afterSignIn?: (user: PlateAuthUser) => Promise<void> };
        trustHost?: boolean;
      }
      export function createAuthConfig(opts: PlateAuthConfigOptions): NextAuthConfig {
        // builds provider list from opts.providers
        // signIn callback calls exchangeWithBackend(envelope) using opts.exchange
        // jwt callback persists access_token + memberships from backend response
        // session callback exposes accessToken to client
      }
      
    • Provider modules under packages/auth/src/config/providers/{google,microsoft,email}.ts for clean tree-shaking.
  3. W3-7 Copy frontend/app/api/[...path]/route.tspackages/auth/src/proxy/handlers.ts.
    • Refactor:
      export interface ProxyOptions {
        backendUrl: string;
        stripHeaders?: string[];   // default: hop-by-hop list
        authHeaderName?: string;   // default: 'Authorization'
      }
      export function createProxyHandlers(opts: ProxyOptions): {
        GET: RouteHandler; POST: RouteHandler; PUT: RouteHandler;
        PATCH: RouteHandler; DELETE: RouteHandler; OPTIONS: RouteHandler;
      };
      
    • Must use NextAuth v5 auth() not getToken(). Body forwarding must include duplex: "half" for streaming POST/PUT.
  4. W3-8 Copy frontend/middleware.tspackages/auth/src/middleware/index.ts.
    • Factor as createAuthMiddleware(opts?: { publicPaths?: string[] }) returning a NextMiddleware.
  5. W3-9 Move frontend/contexts/auth-context.tsx logic into packages/auth/src/client/hooks.ts — but as hooks only, no React Context wrapper. Consumers build their own provider if needed.
    • Expose:
      export function useAccessToken(): string | null;
      export function useMemberships(): Membership[];
      export type { Membership, OrgType, MembershipRole, MembershipStatus };
      
  6. W3-10 Re-export NextAuth client surface in packages/auth/src/client/index.ts:
    export { useSession, signIn, signOut, SessionProvider } from "next-auth/react";
    export * from "./hooks";
    

Done when: Library compiles, exports listed above resolve, and a sample Next.js app can import { createAuthConfig } from '@platesoft/auth/config'.

6.3 W3-C — Boilerplate Next.js route file

Consumers need a one-line route file. We ship documentation, not the file itself (it must live in their app/api/auth/[...nextauth]/route.ts):

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { createAuthConfig } from '@platesoft/auth/config';

const config = createAuthConfig({
  providers: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! } },
  exchange:  { backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL!, secret: process.env.NEXTAUTH_EXCHANGE_SECRET! },
});
export const { handlers, auth, signIn, signOut } = NextAuth(config);
export const { GET, POST } = handlers;

Documented in Integration-Guide.md.


7. Flyway migration consolidation

7.1 Strategy

After Plan Reviewer feedback on Open-Questions Q03, finalize the strategy. The recommended approach for v0.1 (subject to Plan Reviewer concurrence):

Separate Flyway history table for plate-auth migrations.

  • Consumer config: spring.flyway.locations=classpath:db/migration,classpath:db/migration/auth
  • plate-auth auto-configures a second Flyway bean named plateAuthFlyway with:
    • locations = classpath:db/migration/auth
    • table = flyway_schema_history_auth
    • Runs at startup before the application's own Flyway
  • Application's primary Flyway continues to manage flyway_schema_history for app migrations

Why: plate-auth's V1..V6 numbering is completely independent of any app's V1..VN. Both libraries can advance their own version space without collision. Consumers get a clean install from scratch, and InspectFlow's Migration-InspectFlow.md handles the in-place baseline.

If Plan Reviewer rejects this and prefers numbered-tail approach (e.g. plate-auth ships V1..V6 and relies on app migrations starting at V100), we revise to single-table strategy. Both approaches are viable; the separate-table one is more isolating.

7.2 W5 — Migration files

Steps:

  1. W5-1 Create plate-auth-starter/src/main/resources/db/migration/auth/ directory.
  2. W5-2 Copy V26 → V1__create_users_and_identities.sql. Edit:
    • Remove anything InspectFlow-specific (none expected)
    • Verify Postgres compatibility (no H2-only syntax)
  3. W5-3 Copy V27 → V2__create_memberships.sql. Drop the trigger fn_membership_org_fk() from the migration — that trigger references companies which is T3. Consumers add their own trigger or rely solely on the OrgValidator SPI for validation.
    • Document in Migration-InspectFlow.md: "InspectFlow's V27 trigger was migrated; if you previously relied on it, keep it in your app's migration."
  4. W5-4 Copy V28 → V3__create_invitations.sql.
  5. W5-5 Copy V29 → V4__create_access_requests.sql.
  6. W5-6 Create V5__add_microsoft_tenant_id_index.sql. This is a standalone migration that adds an index on user_identities.microsoft_tenant_id (the column itself lives in V1). It stays separate from the login-events migration so the index can ship as a hotfix without renumbering.
  7. W5-7 Copy V31 → V6__create_login_events_and_revinfo_actor.sql. (V30, companies.microsoft_tenant_id, stays in InspectFlow's migration set — T3.)
  8. W5-8 Add MigrationContentTest (integration test) that:
    • Spins up Testcontainers Postgres
    • Runs plate-auth Flyway against flyway_schema_history_auth
    • Asserts all 6 versions applied successfully (V1..V6)
    • Asserts no SQL errors in clean install

Done when: Migration test passes against Testcontainers Postgres in CI with 6 rows in flyway_schema_history_auth.

7.3 W5 — Auto-config the second Flyway bean

@Configuration
@ConditionalOnClass(Flyway.class)
public class PlateAuthFlywayConfig {

    @Bean
    public Flyway plateAuthFlyway(DataSource dataSource) {
        Flyway fw = Flyway.configure()
            .dataSource(dataSource)
            .locations("classpath:db/migration/auth")
            .table("flyway_schema_history_auth")
            .baselineOnMigrate(true)            // for fresh installs only
            .load();
        fw.migrate();
        return fw;
    }
}

Critical detail: this Bean's migrate() must run before any @Entity is touched. Spring Boot's default Flyway runs as part of JPA initialization; we run ours explicitly in the bean factory method. Integration tests verify ordering.


8. Build + publish pipeline

8.1 W6-A — Gitea Actions workflow

Steps:

  1. W6-1 Create .gitea/workflows/ci.yml:
    name: CI
    on:
      push: { branches: [main] }
      pull_request: { branches: [main] }
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-java@v4
            with: { java-version: '25', distribution: 'temurin' }
          - uses: actions/setup-node@v4
            with: { node-version: '22' }
          - run: npm install -g pnpm
          - run: mvn -B verify
          - run: pnpm install --frozen-lockfile
          - run: pnpm -r build
          - run: pnpm -r test
    
  2. W6-2 Create .gitea/workflows/release.yml:
    name: Release
    on:
      push: { tags: ['v*'] }
    jobs:
      publish-maven:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-java@v4
            with: { java-version: '25', distribution: 'temurin' }
          - name: Configure Maven for Gitea
            run: |
              mkdir -p ~/.m2
              cat > ~/.m2/settings.xml <<EOF
              <settings>
                <servers><server>
                  <id>gitea</id>
                  <username>${{ secrets.GITEA_USER }}</username>
                  <password>${{ secrets.GITEA_TOKEN }}</password>
                </server></servers>
              </settings>
              EOF
          - run: mvn -B -Drevision=${GITHUB_REF_NAME#v} deploy
      publish-npm:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: '22'
              registry-url: 'https://git.plate-software.de/api/packages/pplate/npm/'
          - run: npm install -g pnpm
          - run: pnpm install --frozen-lockfile
          - name: Set version from tag
            run: pnpm -F @platesoft/auth version ${GITHUB_REF_NAME#v} --no-git-tag-version
          - run: pnpm -F @platesoft/auth build
          - run: pnpm -F @platesoft/auth publish --no-git-checks
            env:
              NPM_CONFIG_TOKEN: ${{ secrets.GITEA_TOKEN }}
    
  3. W6-3 Add distributionManagement block to parent pom.xml pointing at the Gitea Maven endpoint (https://git.plate-software.de/api/packages/pplate/maven).
  4. W6-4 Snapshot publishing on every push to main:
    • Maven: mvn -Drevision=0.1.0-SNAPSHOT deploy (Gitea Package Registry allows SNAPSHOT-style for Maven)
    • npm: skip on snapshots, or use pnpm publish --tag snapshot with 0.1.0-snapshot.<sha> version

Done when: Pushing tag v0.0.1 publishes both de.platesoft:plate-auth-starter:0.0.1 (Maven) and @platesoft/auth@0.0.1 (npm) to the Gitea Package Registry. Verified by mvn dependency:get + npm view.

8.2 W6-B — Validation tag

Before cutting v0.1.0, cut v0.0.1 first:

  • Verifies the publish pipeline end-to-end
  • Lets InspectFlow team try mvn dependency:get de.platesoft:plate-auth-starter:0.0.1
  • Forces us to fix all the inevitable "wrong settings.xml / missing token" issues before the real release

After v0.0.1 lands cleanly and is consumed in a throwaway test app, cut v0.1.0 from the same commit.


Plan continues — security review, rollout, acceptance.


9. Security review checklist

The library wraps authentication, so the security review bar is higher than for a typical extraction. v0.1.0 cannot tag until every item below is verified.

9.1 Secrets

  • plate.auth.jwt.secret and plate.auth.exchange.secret are @NotBlank @Size(min=32). App boot fails if missing or too short. Verified by PlateAuthPropertiesValidationTest.
  • No default value for either secret anywhere — not in application.yml, not in test resources, not in @Value("${...:default}") fallback.
  • Secrets are read only from env vars / external config. Never logged. JwtService never logs the secret.
  • Test fixtures generate per-test secrets via UUID.randomUUID() — no fixed test secrets in repo.
  • .gitignore excludes .env* (except .env.example template).

9.2 HMAC exchange

  • HMAC algorithm = SHA-256, fixed, not configurable in v0.1.
  • Signature compare uses constant-time comparison (MessageDigest.isEqual on backend, crypto.subtle.timingSafeEqual equivalent or Buffer.compare+length-check on frontend).
  • Envelope iat checked against now - maxAge. Default maxAge=60s. Configurable.
  • Nonce dedup: every envelope's nonce is stored for nonceTtl (default 5min). Replay within that window is rejected with HTTP 401.
  • In-memory nonce store is documented as single-replica only. Multi-replica replay protection deferred to v0.3 (per Roadmap).

9.3 JWT

  • HMAC SHA-256 signing.
  • Issuer claim is validated against plate.auth.jwt.issuer (default "plate-auth").
  • Expiration validated. Tokens without exp rejected.
  • No claims contain PII beyond email + user id + role.
  • Refresh-token rotation: a successful refresh issues a new refresh token (and ideally invalidates the old). v0.1 keeps the InspectFlow-current behavior — tracked for hardening in v0.3.

9.4 SQL + persistence

  • All repository queries are JPA Criteria or @Query with named parameters — no string concat.
  • No entityManager.createNativeQuery("..." + userInput + "...") anywhere in moved services.
  • Migrations are SQL files only (no Java-callbacks doing reflection-fueled stuff).
  • Envers RevInfoListener populates actor_user_id from SecurityContextHolder (defensive null check).

9.5 Input validation

  • All controllers use @Valid on @RequestBody DTOs.
  • Email fields validated @Email.
  • Token fields (invitation, refresh) length-checked to expected size.
  • OrgType and other enums use Jackson default behavior (unknown values → 400).
  • No raw Map<String,Object> payloads accepted on auth endpoints.

9.6 Error responses

  • Failed login returns generic "invalid credentials" message — no leak of "user exists" vs "user doesn't exist."
  • LoginEvent.outcome=FAILURE is recorded even on unknown-user attempts.
  • 401 / 403 responses include no stack trace, no SQL fragment, no internal class names.
  • Logging: log.warn("Login failed for {}", email) — no password. log.debug("...") in JwtService never logs the secret or full token.

9.7 Audit

  • Every state-changing op in MembershipService, InvitationService, AccessRequestService, AuthService results in an Envers revision with actor_user_id set.
  • LoginEventService.recordSuccess and .recordFailure cover all four outcome enum values.
  • AdminAuditController enforces hasRole('ADMIN') via method-security annotations.

9.8 OAuth providers

  • Google clientId and clientSecret only configured via env, never default.
  • Microsoft Entra ID provider is @ConditionalOnProperty("plate.auth.providers.microsoft.enabled"). Default disabled. If enabled without configured creds → fail-fast at startup.
  • Email magic-link provider is @ConditionalOnProperty("plate.auth.providers.email-magic-link.enabled"). Same fail-fast.
  • allowDangerousEmailAccountLinking is false in NextAuth config — verified by snapshot test of createAuthConfig output.

9.9 Frontend

  • NEXTAUTH_SECRET documented as required. Library will not start if missing (NextAuth itself enforces).
  • NEXTAUTH_EXCHANGE_SECRET is never sent to the browser. Used server-side only in signIn callback. Validated by reading the bundled output of @platesoft/auth/config.
  • Proxy strips hop-by-hop headers (per RFC 7230 + custom Authorization override).
  • Proxy never echoes the bearer token in error responses.
  • Edge-runtime compatibility validated by running tests in @edge-runtime/jest-environment or vitest equivalent.

9.10 Dependencies

  • All deps have CVE scan clean at release tag (Gitea Actions runs Snyk or OWASP dep-check).
  • No transitive dep with known CVE > medium severity.
  • Renovate (or equivalent) configured to keep deps current post-release.

10. Rollout plan

10.1 Step 0 — internal validation tag

  1. Cut v0.0.1 from main after all workstreams W1W7 complete + tests green (6 Flyway migrations applied, default PermissiveOrgValidator emits WARN on every call).
  2. In a throwaway repo, consume de.platesoft:plate-auth-starter:0.0.1 + @platesoft/auth@0.0.1.
  3. Implement an OrgValidator that returns true for any input. Boot the app.
  4. Hit /api/auth/config — should return Google provider info.
  5. Sign in via Google. Verify exchange + JWT issued + /api/auth/me works.
  6. Cleanup: revoke + redeploy. Mark v0.0.1 as "validated."

10.2 Step 1 — Sparkboard adoption

Sparkboard is the easier consumer because it has no auth code yet.

  1. Add the Maven dep + npm dep at the chosen 0.1.0 version.
  2. Implement Sparkboard's OrgValidator against their workspaces table.
  3. Add env vars: PLATE_AUTH_JWT_SECRET, PLATE_AUTH_EXCHANGE_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, NEXTAUTH_SECRET, NEXTAUTH_EXCHANGE_SECRET, NEXT_PUBLIC_BACKEND_URL.
  4. Add app/api/auth/[...nextauth]/route.ts per Integration-Guide.md.
  5. Boot, sign in, ship.
  6. Time-from-zero-to-first-signin = key v0.1 success metric.

10.3 Step 2 — InspectFlow migration

InspectFlow is harder because it must replace in-tree code without losing data.

  1. Follow Migration-InspectFlow.md end-to-end.
  2. Critical path:
    • Add plate-auth-starter dep
    • Remove old de.platesoft.inspectflow.{filter,service,controller} auth classes
    • Rename inspectflow.* config props → plate.auth.*
    • Insert baseline rows into flyway_schema_history_auth for V1..V5 (data already exists from V26..V31)
    • Add OrgValidator impl wrapping CompanyRepository.existsById(...)
    • Add OnboardingHook impl that calls existing OnboardingService
    • Frontend: replace lib/auth-config.ts with a thin wrapper over createAuthConfig
    • Run full E2E suite — must pass
  3. Deploy to staging.
  4. Smoke test: sign-in for at least one user of each provider (Google, Microsoft if used, Email if used, password if registration was enabled).
  5. Deploy to production behind feature flag if available; otherwise off-hours deploy with rollback plan.

10.4 Rollback strategy

If v0.1.0 ships and InspectFlow's adoption breaks something we missed:

  1. Frontend rollback: revert the package.json dep + re-vendor the old auth-config.ts. No DB change required.
  2. Backend rollback: revert the pom.xml dep, redeploy. Database is unaffected (entities are the same — just the package names of the classes mapping to them changed). Hibernate @Table(name="...") keeps the SQL schema stable.
  3. Worst case: restore from pre-deploy database backup + redeploy old commit. Acceptable downtime window: ≤30min off-hours.

10.5 Acceptance criteria

v0.1.0 is "done" when:

# Criterion How verified
A1 Maven artifact published at de.platesoft:plate-auth-starter:0.1.0 mvn dependency:get from a fresh repo succeeds
A2 npm artifact published at @platesoft/auth@0.1.0 npm view @platesoft/auth@0.1.0 from a fresh repo succeeds
A3 InspectFlow runs full E2E suite green using the library CI green on InspectFlow's migration PR
A4 Sparkboard signs in a user with only Integration-Guide instructions Stopwatch < 30min from clean repo
A5 All 16 security checklist items in § 9 verified Security Review document (mode security-reviewer) APPROVED
A6 Plan Reviewer APPROVED the plan Plan-Review document committed to wiki repo
A7 All 10 wiki docs published + cross-references resolve Visual review by Patrick
A8 CHANGELOG.md released at 0.1.0 Tag pushed, release notes visible on Gitea

11. Items deferred to v0.2+

Tracked here so they don't get lost when Sprint 0 closes:

  • Per-tenant JWT issuer config (multi-app sharing a single Postgres)
  • Refresh-token rotation table + family-tracking
  • LoginEventSink SPI for external audit-log shipping
  • Multi-replica nonce store via Redis or Postgres UPSERT ... ON CONFLICT
  • WebAuthn / passkey support
  • RFC 7807 Problem Details on error responses
  • Configurable invitation expiration (currently hardcoded 7d)
  • Default JavaMailSender-backed InvitationMailer
  • Better Edge-runtime test coverage
  • TypeScript export of Membership, Invitation, AccessRequest types
  • Exported Zod / valibot schemas for envelope and DTOs

These will form the v0.2 backlog. They are not blockers for v0.1.


12. Cross-references


End of plan.

Status: Submitted for Plan Reviewer (architect mode) review. Patrick GO required before code starts.