Table of Contents
- Sprint 0 — Implementation Plan
- Reading guide
- 1. Scope + ground rules
- 2. Workstream overview
- 3. Backend extraction — overview
- 4. Frontend extraction — overview
- 5. Flyway strategy — overview
- 6. Build + publish pipeline — overview
- 4. Backend extraction — step-by-step
- 4.1 W1 — Repo scaffolding
- 4.2 W2-A — Backend: copy + rename classes
- 4.3 W2-B — Configuration namespace
- 4.4 W2-C — Auto-configuration
- 4.5 W4 — SPI design + default implementations
- 6. Frontend extraction — step-by-step
- 6.1 W3-A — npm package skeleton
- 6.2 W3-B — Move + factor frontend code
- 6.3 W3-C — Boilerplate Next.js route file
- 7. Flyway migration consolidation
- 8. Build + publish pipeline
- 9. Security review checklist
- 9.1 Secrets
- 9.2 HMAC exchange
- 9.3 JWT
- 9.4 SQL + persistence
- 9.5 Input validation
- 9.6 Error responses
- 9.7 Audit
- 9.8 OAuth providers
- 9.9 Frontend
- 9.10 Dependencies
- 10. Rollout plan
- 10.1 Step 0 — internal validation tag
- 10.2 Step 1 — Sparkboard adoption
- 10.3 Step 2 — InspectFlow migration
- 10.4 Rollback strategy
- 10.5 Acceptance criteria
- 11. Items deferred to v0.2+
- 12. Cross-references
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:
- Scope + ground rules (this section)
- Workstream overview (the 7 parallel-ish workstreams)
- Backend extraction — step-by-step
- Frontend extraction — step-by-step
- Flyway migration consolidation
- Build + publishing pipeline
- Security review checklist (must pass before v0.1.0 tag)
- Rollout plan (InspectFlow + Sparkboard adoption)
- Acceptance criteria
- 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 andArchitecture.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)
LoginEventSinkSPI (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.1–14.6 is automatic v0.2 fodder.
- Single git repo, two artifacts. Maven module + npm package live in one repo with a shared
CHANGELOG.mdand 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 tomain.
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 = ~9–10 calendar days |
3. Backend extraction — overview
See § 4 for step-by-step.
The backend extraction is fundamentally:
- Move every class in
inspectflow/backend/src/main/java/de/platesoft/inspectflow/{config,filter,service,controller,entity,repository,dto,enums}/...that is auth-related intoplate-auth/plate-auth-starter/src/main/java/de/platesoft/auth/... - Rename every import,
@Value("${jwt....}")→@Value("${plate.auth.jwt....}")etc. - Replace every direct
companiesrepository call insideMembershipServiceetc. with a call throughOrgValidator/OrgDisplayNameResolverSPI. - Add
PlateAuthAutoConfiguration+PlateAuthProperties+META-INF/spring/...AutoConfiguration.imports. - 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:
- Move the NextAuth config from
inspectflow/frontend/lib/auth-config.tsinto acreateAuthConfig(opts)factory inplate-auth/packages/auth/src/config/index.ts - Move the proxy logic from
inspectflow/frontend/app/api/[...path]/route.tsinto acreateProxyHandlers(opts)factory - Move the HMAC exchange logic from
inspectflow/frontend/lib/exchange.tsintoplate-auth/packages/auth/src/exchange/ - Replace the InspectFlow-specific
localStoragekeys with configurable prefixes - Export typed hooks (
useAccessToken,useMemberships) and re-export NextAuth'suseSession,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, V26–V31 already ran. We do not re-run them. Migration recipe:
- Insert a "baseline" row into
flyway_schema_history_authmarking V1..V5 as already applied - Detailed steps in
Migration-InspectFlow.md
- Insert a "baseline" row into
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 packagebuilds the JAR.npm -w @platesoft/auth buildbuilds the npm package. - A git tag
v0.1.0triggers a Gitea Actions workflow that:- Builds + tests both artifacts
- Publishes the JAR to Gitea Package Registry (Maven)
- Publishes the npm tarball to Gitea Package Registry (npm)
- Updates the
CHANGELOG.mdwith the release date
- Snapshot builds (on every push to
main) publish to0.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:
- W1-1 In the
plate-authrepo 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 - 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 fromspring-boot-starter-parent:4.1.0. - W1-3
plate-auth-starter/pom.xml:<artifactId>plate-auth-starter</artifactId>, deps copied from InspectFlowbackend/pom.xmlminus 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) - W1-4 Root
package.json+pnpm-workspace.yamlconfigured to pick uppackages/*. - W1-5
packages/auth/package.json:"name": "@platesoft/auth","version": "0.1.0", peerDeps onnext@>=15,next-auth@^5.0.0-beta,react@>=19. - W1-6 Add
.gitignore,.editorconfig, basicREADME.mdlinking to the wiki. - 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):
- W2-1 Copy class file from
inspectflow/backend/src/main/java/de/platesoft/inspectflow/<pkg>/<X>.javatoplate-auth/plate-auth-starter/src/main/java/de/platesoft/auth/<pkg>/<X>.java. - W2-2 Update
packagedeclaration:package de.platesoft.auth.<pkg>;. - 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.
- 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 rewritesfilter/OrgContextResolver.java— direct moveconfig/SecurityConfig.java— direct move, but: replace InspectFlow-specific permit-list (/api/companies/*/public-info) with config-driven list fromPlateAuthProperties.cors.additionalPermitPathsservice/JwtService.java— direct move; rename@Value("${jwt.secret}")→@Value("${plate.auth.jwt.secret}")service/OAuthService.java— move + swaptenantAutoMapService.maybeAutoMap(...)call foronboardingHook.onFirstSignIn(...)(SPI)service/ExchangeService.java— direct moveservice/MembershipService.java— move + swapcompanyRepository.findById(...)fororgValidator.exists(orgType, orgId)/orgDisplayNameResolver.displayName(...)callsservice/InvitationService.java— direct move (Sprint 14.3 already abstracted the mailer)service/AccessRequestService.java— direct moveservice/LoginEventService.java— direct moveservice/AuthService.java— direct move (password login/register)controller/AuthController.java— direct movecontroller/OAuthController.java— direct movecontroller/InvitationController.java— direct movecontroller/AccessRequestController.java— direct movecontroller/AdminAuditController.java— direct moveentity/User.java— direct moveentity/UserIdentity.java— direct moveentity/Membership.java— direct moveentity/Invitation.java— direct moveentity/AccessRequest.java— direct moveentity/LoginEvent.java— direct moveentity/RevInfo.java(Envers revinfo with actor) — direct moverepository/*— all auth-related repositories — direct movedto/request/*anddto/response/*— direct moveenums/*— 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:
- 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; } } - W2-9 Inject
PlateAuthProperties(constructor injection) into every service that previously read@Value("${jwt....}")etc. Replace@Valueannotations. - W2-10 Add
META-INF/spring-configuration-metadata.json(or rely onspring-boot-configuration-processorannotation processor — preferred — add it to the build). - 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:
- 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 } - W2-13 Register the auto-config:
Contents: single line
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsde.platesoft.auth.PlateAuthAutoConfiguration. - W2-14 Verify that adding the starter to a minimal Spring Boot 4 app, setting
plate.auth.jwt.secretplate.auth.exchange.secretenv vars, andspring.datasource.urlto 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:
- 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 */ } } - W4-2 Default implementations (annotated
@ConditionalOnMissingBean, registered inPlateAuthAutoConfiguration):PermissiveOrgValidator— ships as the default.exists(...)always returnstrueand 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— returnstype + ":" + orgId.toString()LoggingInvitationMailer— logs the accept URL at INFO levelLoggingAccessRequestMailer— logs notifications at INFO levelNoOpOnboardingHook— no-op
- W4-3 Wire each service to its SPI dep via constructor injection:
MembershipService←OrgValidator,OrgDisplayNameResolverInvitationService←InvitationMailer,OrgDisplayNameResolverAccessRequestService←AccessRequestMailer,OrgDisplayNameResolverOAuthService←OnboardingHook
- 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:
- 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"] } - W3-2 Bundler choice:
tsup(zero-config dual ESM/CJS, fast). Addtsup.config.tstargeting Node 20 + Edge runtime. - W3-3 TypeScript strict config,
"target": "ES2022","module": "ESNext","moduleResolution": "Bundler","declaration": true. - W3-4 Add a
publishConfigblock 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/):
- W3-5 Copy
frontend/lib/exchange.ts→packages/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.
- Replace
- W3-6 Copy
frontend/lib/auth-config.ts→packages/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}.tsfor clean tree-shaking.
- Refactor into a factory:
- W3-7 Copy
frontend/app/api/[...path]/route.ts→packages/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()notgetToken(). Body forwarding must includeduplex: "half"for streaming POST/PUT.
- Refactor:
- W3-8 Copy
frontend/middleware.ts→packages/auth/src/middleware/index.ts.- Factor as
createAuthMiddleware(opts?: { publicPaths?: string[] })returning aNextMiddleware.
- Factor as
- W3-9 Move
frontend/contexts/auth-context.tsxlogic intopackages/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 };
- Expose:
- 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
Flywaybean namedplateAuthFlywaywith:
locations = classpath:db/migration/authtable = flyway_schema_history_auth- Runs at startup before the application's own Flyway
- Application's primary
Flywaycontinues to manageflyway_schema_historyfor app migrationsWhy: plate-auth's
V1..V6numbering is completely independent of any app'sV1..VN. Both libraries can advance their own version space without collision. Consumers get a clean install from scratch, and InspectFlow'sMigration-InspectFlow.mdhandles 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:
- W5-1 Create
plate-auth-starter/src/main/resources/db/migration/auth/directory. - W5-2 Copy V26 →
V1__create_users_and_identities.sql. Edit:- Remove anything InspectFlow-specific (none expected)
- Verify Postgres compatibility (no H2-only syntax)
- W5-3 Copy V27 →
V2__create_memberships.sql. Drop the triggerfn_membership_org_fk()from the migration — that trigger referencescompanieswhich is T3. Consumers add their own trigger or rely solely on theOrgValidatorSPI 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."
- Document in
- W5-4 Copy V28 →
V3__create_invitations.sql. - W5-5 Copy V29 →
V4__create_access_requests.sql. - W5-6 Create
V5__add_microsoft_tenant_id_index.sql. This is a standalone migration that adds an index onuser_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. - W5-7 Copy V31 →
V6__create_login_events_and_revinfo_actor.sql. (V30,companies.microsoft_tenant_id, stays in InspectFlow's migration set — T3.) - 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:
- 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 - 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 }} - W6-3 Add
distributionManagementblock to parentpom.xmlpointing at the Gitea Maven endpoint (https://git.plate-software.de/api/packages/pplate/maven). - 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 snapshotwith0.1.0-snapshot.<sha>version
- Maven:
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.secretandplate.auth.exchange.secretare@NotBlank @Size(min=32). App boot fails if missing or too short. Verified byPlateAuthPropertiesValidationTest.- 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. .gitignoreexcludes.env*(except.env.exampletemplate).
9.2 HMAC exchange
- HMAC algorithm = SHA-256, fixed, not configurable in v0.1.
- Signature compare uses constant-time comparison
(
MessageDigest.isEqualon backend,crypto.subtle.timingSafeEqualequivalent orBuffer.compare+length-check on frontend). - Envelope
iatchecked againstnow - maxAge. DefaultmaxAge=60s. Configurable. - Nonce dedup: every envelope's
nonceis stored fornonceTtl(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
exprejected. - 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
@Querywith 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
RevInfoListenerpopulatesactor_user_idfromSecurityContextHolder(defensive null check).
9.5 Input validation
- All controllers use
@Validon@RequestBodyDTOs. - Email fields validated
@Email. - Token fields (invitation, refresh) length-checked to expected size.
OrgTypeand 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=FAILUREis 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,AuthServiceresults in an Envers revision withactor_user_idset. LoginEventService.recordSuccessand.recordFailurecover all fouroutcomeenum values.AdminAuditControllerenforceshasRole('ADMIN')via method-security annotations.
9.8 OAuth providers
- Google
clientIdandclientSecretonly 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. allowDangerousEmailAccountLinkingis false in NextAuth config — verified by snapshot test ofcreateAuthConfigoutput.
9.9 Frontend
NEXTAUTH_SECRETdocumented as required. Library will not start if missing (NextAuth itself enforces).NEXTAUTH_EXCHANGE_SECRETis never sent to the browser. Used server-side only insignIncallback. Validated by reading the bundled output of@platesoft/auth/config.- Proxy strips hop-by-hop headers (per RFC 7230 + custom
Authorizationoverride). - Proxy never echoes the bearer token in error responses.
- Edge-runtime compatibility validated by running tests in
@edge-runtime/jest-environmentor 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
- Cut
v0.0.1from main after all workstreams W1–W7 complete + tests green (6 Flyway migrations applied, defaultPermissiveOrgValidatoremits WARN on every call). - In a throwaway repo, consume
de.platesoft:plate-auth-starter:0.0.1+@platesoft/auth@0.0.1. - Implement an
OrgValidatorthat returnstruefor any input. Boot the app. - Hit
/api/auth/config— should return Google provider info. - Sign in via Google. Verify exchange + JWT issued +
/api/auth/meworks. - Cleanup: revoke + redeploy. Mark
v0.0.1as "validated."
10.2 Step 1 — Sparkboard adoption
Sparkboard is the easier consumer because it has no auth code yet.
- Add the Maven dep + npm dep at the chosen
0.1.0version. - Implement Sparkboard's
OrgValidatoragainst theirworkspacestable. - 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. - Add
app/api/auth/[...nextauth]/route.tsperIntegration-Guide.md. - Boot, sign in, ship.
- 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.
- Follow
Migration-InspectFlow.mdend-to-end. - Critical path:
- Add
plate-auth-starterdep - Remove old
de.platesoft.inspectflow.{filter,service,controller}auth classes - Rename
inspectflow.*config props →plate.auth.* - Insert baseline rows into
flyway_schema_history_authfor V1..V5 (data already exists from V26..V31) - Add
OrgValidatorimpl wrappingCompanyRepository.existsById(...) - Add
OnboardingHookimpl that calls existingOnboardingService - Frontend: replace
lib/auth-config.tswith a thin wrapper overcreateAuthConfig - Run full E2E suite — must pass
- Add
- Deploy to staging.
- Smoke test: sign-in for at least one user of each provider (Google, Microsoft if used, Email if used, password if registration was enabled).
- 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:
- Frontend rollback: revert the
package.jsondep + re-vendor the oldauth-config.ts. No DB change required. - Backend rollback: revert the
pom.xmldep, 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. - 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
LoginEventSinkSPI 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-backedInvitationMailer - Better Edge-runtime test coverage
- TypeScript export of
Membership,Invitation,AccessRequesttypes - 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
- Assessment:
Sprint-0-Assessment.md - Test plan:
Sprint-0-Testplan.md - Architecture reference:
Architecture.md - Open questions blocking final plan:
Open-Questions.md - Consumer integration:
Integration-Guide.md - InspectFlow migration recipe:
Migration-InspectFlow.md
End of plan.
Status: Submitted for Plan Reviewer (architect mode) review. Patrick GO required before code starts.