Table of Contents
- Migration Guide: InspectFlow → plate-auth
- 1. What this migration does
- 2. Prerequisites
- 3. Migration overview
- 4. Step A — Add dependencies
- 5. Step B — Delete moved backend classes
- 5.1 Classes to delete
- 5.2 Classes to KEEP (T3 — InspectFlow-specific)
- 5.3 Find-and-fix imports
- 5.4 SecurityConfig
- 6. Step C — Rename config keys
- 6.1 Backend: application.yml
- 6.2 Frontend: .env.local / .env.production
- 6.3 Membership entity column changes
- 7. Step D — Add SPI implementations
- 8. Step E — Flyway baseline rows
- 9. Step F — Frontend swap
- 9.1 Delete files
- 9.2 Keep files
- 9.3 New frontend/auth.ts
- 9.4 New frontend/app/api/[...path]/route.ts
- 10. Step G — Run the E2E suite
- 11. Step H — Merge and clean up
- 12. Rollback procedure
- 13. What changes for end-users
- 14. Common pitfalls (migration-specific)
- 15. Sign-off checklist
- 16. Cross-references
Migration Guide: InspectFlow → plate-auth
Status: Draft v1
Date: 2026-06-24
Owner: Patrick
Audience: InspectFlow maintainer performing the dependency swap from in-tree auth code to plate-auth-starter
Target plate-auth version: 0.1.0 (after 0.0.1 validation tag)
This guide is InspectFlow-specific. For greenfield consumers, see Integration-Guide.md.
1. What this migration does
Replaces InspectFlow's hand-rolled auth code with a dependency on plate-auth-starter + @platesoft/auth. After this migration:
- ~25 backend classes under
de.platesoft.inspectflow.{security,filter,service,controller,entity,repository,dto,enums}are deleted and become a single Maven dependency. - ~5–8 frontend files under
frontend/{auth.ts, app/api/[...path]/route.ts, app/api/auth/[...nextauth]/route.ts, lib/hmac.ts}are deleted and become a single npm dependency + a 5-lineauth.ts. - Flyway migrations
V26..V31are kept but marked as baseline rows in the newflyway_schema_history_authtable so plate-auth'sV1..V5are not re-applied. - InspectFlow keeps
Companyentity +OrgValidatorSPI implementation + onboarding logic — T3 stays in-house.
Goal: zero behavioral change for end-users. All E2E tests stay green. (Sprint-0-Testplan.md § 6)
2. Prerequisites
Before starting:
- InspectFlow on Spring Boot 4.1.0 (bump from 4.0.7 if needed — Sprint 14.7 prerequisite, see Open-Questions.md Q08)
- plate-auth
0.0.1validation tag has been published and you can resolve it from Gitea - All Sprint 14.6 work merged to
main(clean baseline) - Database backup taken
- Sparkboard has already consumed plate-auth
0.0.1successfully (proves the library works against a real consumer) - Feature branch:
feature/plate-auth-migration
If any of these is false, stop and resolve first.
3. Migration overview
flowchart LR
S0[Sprint 14.6 baseline] --> A[Step A: Add deps]
A --> B[Step B: Delete moved backend classes]
B --> C[Step C: Rename config keys]
C --> D[Step D: Add SPI implementations]
D --> E[Step E: Flyway baseline rows]
E --> F[Step F: Frontend swap]
F --> G[Step G: Run E2E suite]
G -->|green| H[Step H: Merge + delete dead code]
G -->|red| R[Rollback]
Estimated effort: 1–2 days for an experienced maintainer assuming the library works as advertised. Most time is verification, not coding.
4. Step A — Add dependencies
4.1 Backend pom.xml
<dependency>
<groupId>de.platesoft</groupId>
<artifactId>plate-auth-starter</artifactId>
<version>0.1.0</version>
</dependency>
Add Gitea Maven repo + credentials if not already present (see Integration-Guide.md § 3.1).
4.2 Frontend package.json
cd frontend
pnpm add @platesoft/auth@0.1.0
.npmrc must contain the @platesoft: scope mapping.
4.3 Verify build still passes
cd backend && mvn clean compile
cd frontend && pnpm install
No code changes yet — both halves should still build cleanly with the new deps idle alongside the existing in-tree code.
5. Step B — Delete moved backend classes
These classes are now provided by plate-auth-starter. Delete the InspectFlow-local copy after verifying the package and class name match exactly.
5.1 Classes to delete
| InspectFlow path | Replaced by |
|---|---|
backend/src/main/java/de/platesoft/inspectflow/service/JwtService.java |
de.platesoft.auth.service.JwtService |
backend/src/main/java/de/platesoft/inspectflow/filter/JwtAuthenticationFilter.java |
de.platesoft.auth.filter.JwtAuthenticationFilter |
backend/src/main/java/de/platesoft/inspectflow/config/SecurityConfig.java |
Auto-config in PlateAuthAutoConfiguration (see § 5.4 below) |
…/inspectflow/service/ExchangeService.java |
de.platesoft.auth.service.ExchangeService |
…/inspectflow/service/HmacEnvelope.java |
de.platesoft.auth.crypto.HmacEnvelope |
…/inspectflow/service/MembershipService.java |
de.platesoft.auth.service.MembershipService |
…/inspectflow/service/InvitationService.java |
de.platesoft.auth.service.InvitationService |
…/inspectflow/service/AccessRequestService.java |
de.platesoft.auth.service.AccessRequestService |
…/inspectflow/service/AuthService.java (login/signup/reset) |
de.platesoft.auth.service.AuthService |
…/inspectflow/controller/AuthController.java |
de.platesoft.auth.controller.AuthController |
…/inspectflow/controller/MembershipController.java |
de.platesoft.auth.controller.MembershipController |
…/inspectflow/controller/InvitationController.java |
de.platesoft.auth.controller.InvitationController |
…/inspectflow/controller/AccessRequestController.java |
de.platesoft.auth.controller.AccessRequestController |
…/inspectflow/controller/AdminAuditController.java |
de.platesoft.auth.controller.AdminAuditController |
…/inspectflow/entity/User.java |
de.platesoft.auth.entity.User |
…/inspectflow/entity/UserIdentity.java |
de.platesoft.auth.entity.UserIdentity |
…/inspectflow/entity/Membership.java |
de.platesoft.auth.entity.Membership (with polymorphic FK — see § 6.3) |
…/inspectflow/entity/Invitation.java |
de.platesoft.auth.entity.Invitation |
…/inspectflow/entity/AccessRequest.java |
de.platesoft.auth.entity.AccessRequest |
…/inspectflow/entity/LoginEvent.java |
de.platesoft.auth.entity.LoginEvent |
…/inspectflow/repository/UserRepository.java |
de.platesoft.auth.repository.UserRepository |
…/inspectflow/repository/MembershipRepository.java |
de.platesoft.auth.repository.MembershipRepository |
…/inspectflow/repository/InvitationRepository.java |
de.platesoft.auth.repository.InvitationRepository |
…/inspectflow/repository/AccessRequestRepository.java |
de.platesoft.auth.repository.AccessRequestRepository |
…/inspectflow/repository/LoginEventRepository.java |
de.platesoft.auth.repository.LoginEventRepository |
…/inspectflow/dto/... (auth-related) |
de.platesoft.auth.dto.* |
5.2 Classes to KEEP (T3 — InspectFlow-specific)
These stay in InspectFlow because they encode the Company domain or onboarding logic:
| Path | Reason |
|---|---|
de.platesoft.inspectflow.entity.Company |
Concrete org table — InspectFlow's domain |
de.platesoft.inspectflow.repository.CompanyRepository |
Same |
de.platesoft.inspectflow.service.CompanyService |
Same |
de.platesoft.inspectflow.service.OrgContextResolver |
Adapter — refactor to implement plate-auth's OrgValidator SPI |
de.platesoft.inspectflow.service.OnboardingService |
App-specific onboarding (create default machines, etc.) |
5.3 Find-and-fix imports
After deleting the classes in § 5.1, the rest of InspectFlow that references them (controllers, services that aren't auth-related) will fail to compile. Rewrite imports:
# Find all consumers of the deleted packages
cd backend
grep -rln "de.platesoft.inspectflow.service.JwtService" src/main/java/
grep -rln "de.platesoft.inspectflow.entity.User" src/main/java/
# … repeat per deleted class
# Replace package prefix
find src/main/java -name "*.java" -exec sed -i '' \
-e 's|de.platesoft.inspectflow.service.JwtService|de.platesoft.auth.service.JwtService|g' \
-e 's|de.platesoft.inspectflow.entity.User|de.platesoft.auth.entity.User|g' \
-e 's|de.platesoft.inspectflow.entity.Membership|de.platesoft.auth.entity.Membership|g' \
{} +
Take small steps. Replace one package at a time and recompile. Big-bang sed will leave you with a sea of red.
5.4 SecurityConfig
The starter ships its own SecurityFilterChain bean named plateAuthSecurityChain. InspectFlow's current SecurityConfig.java declares its own beans for passwordEncoder, authenticationManager, and a filterChain. Three of these are auto-provided by the starter:
passwordEncoder→PlateAuthAutoConfigurationprovidesBCryptPasswordEncoderwith cost 10. Delete InspectFlow's.authenticationManager→ same. Delete InspectFlow's.filterChain→ starter'splateAuthSecurityChaincovers/auth/**+ JWT for the rest. Keep an InspectFlow-specific chain for paths that need custom rules (e.g./api/admin/**requireshasRole('ADMIN')).
// InspectFlow keeps a focused chain:
@Bean
@Order(0) // before plateAuthSecurityChain
public SecurityFilterChain inspectflowAdminChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/admin/**")
.authorizeHttpRequests(a -> a.anyRequest().hasRole("ADMIN"))
.csrf(c -> c.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}
6. Step C — Rename config keys
InspectFlow has historically scattered auth config across inspectflow.*, jwt.*, and nextauth.*. plate-auth consolidates everything under plate.auth.*.
6.1 Backend: application.yml
Before:
jwt:
secret: ${JWT_SECRET}
access-expiration-minutes: 15
refresh-expiration-days: 30
nextauth:
exchange-secret: ${EXCHANGE_SECRET}
exchange-max-age-seconds: 60
inspectflow:
registration-enabled: false
cors:
allowed-origins: [...]
After:
plate:
auth:
jwt:
secret: ${JWT_SECRET}
access-expiration: PT15M
refresh-expiration: P30D
issuer: inspectflow
exchange:
secret: ${EXCHANGE_SECRET}
max-age: PT60S
nonce-ttl: PT5M
registration:
enabled: false
cors:
allowed-origins: [...]
providers:
google:
enabled: true
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
Format changes:
- Numeric
*-minutes/*-seconds/*-days→ ISO-8601Durationformat (PT15M,PT60S,P30D). Spring Boot parses both15mandPT15M, but the starter validates the ISO form.
6.2 Frontend: .env.local / .env.production
| Old | New |
|---|---|
JWT_SECRET |
(unused — backend only now) |
EXCHANGE_SECRET |
PLATE_AUTH_EXCHANGE_SECRET |
BACKEND_URL |
PLATE_AUTH_BACKEND_URL |
NEXTAUTH_SECRET |
NEXTAUTH_SECRET (unchanged) |
NEXTAUTH_URL |
NEXTAUTH_URL (unchanged) |
GOOGLE_CLIENT_ID |
GOOGLE_CLIENT_ID (unchanged) |
GOOGLE_CLIENT_SECRET |
GOOGLE_CLIENT_SECRET (unchanged) |
Update docker-compose.yml, TrueNAS config, .env.example, and .env.prod.example.
6.3 Membership entity column changes
Old InspectFlow Membership:
@Entity
public class Membership {
@ManyToOne private User user;
@ManyToOne private Company company; // ← direct FK
private Role role;
}
New plate-auth Membership:
@Entity
public class Membership {
@ManyToOne private User user;
private String orgType; // "COMPANY"
private Long orgId; // company.id
private Role role;
}
Migration: the existing memberships.company_id column must be rewritten to (org_type='COMPANY', org_id=company_id). Handled by the Flyway data-migration step in § 7.3.
7. Step D — Add SPI implementations
InspectFlow plugs back into plate-auth via SPI beans. Add a new config class:
// backend/src/main/java/de/platesoft/inspectflow/auth/PlateAuthBindings.java
@Configuration
public class PlateAuthBindings {
@Bean
public OrgValidator orgValidator(CompanyRepository companies) {
return (orgType, orgId) ->
"COMPANY".equals(orgType) && companies.existsById(orgId);
}
@Bean
public OrgDisplayNameResolver orgDisplayNameResolver(CompanyRepository companies) {
return (orgType, orgId) -> companies.findById(orgId)
.map(Company::getName)
.orElse("Unbekannt");
}
@Bean
public InvitationMailer invitationMailer(InspectFlowMailService mail) {
return mail::sendInvitation; // delegate to existing mailer
}
@Bean
public AccessRequestMailer accessRequestMailer(InspectFlowMailService mail) {
return mail::sendAccessRequestNotification;
}
@Bean
public OnboardingHook onboardingHook(OnboardingService onboarding) {
return onboarding::onUserCreated; // delegate to existing onboarding
}
}
This replaces all five default SPI beans. The starter logs "User-provided OrgValidator detected, default disabled" at INFO on boot — verify this in your first run.
8. Step E — Flyway baseline rows
InspectFlow's existing flyway_schema_history contains rows for V26..V31 (your existing auth migrations). plate-auth ships its own migrations V1..V5 and runs them against flyway_schema_history_auth (a separate table). The schema already has the tables — running V1..V5 would fail.
8.1 Migration approach
Add a new InspectFlow-local migration V32__C_seed_plate_auth_history.sql:
-- Pre-populate flyway_schema_history_auth so plate-auth Flyway sees the tables as already migrated.
-- The schema was created by InspectFlow's V26..V31; plate-auth's V1..V5 would otherwise re-apply them.
CREATE TABLE IF NOT EXISTS flyway_schema_history_auth (
installed_rank int NOT NULL,
version varchar(50),
description varchar(200) NOT NULL,
type varchar(20) NOT NULL,
script varchar(1000) NOT NULL,
checksum int,
installed_by varchar(100) NOT NULL,
installed_on timestamp NOT NULL DEFAULT now(),
execution_time int NOT NULL,
success boolean NOT NULL,
PRIMARY KEY (installed_rank)
);
INSERT INTO flyway_schema_history_auth
(installed_rank, version, description, type, script, checksum, installed_by, execution_time, success)
VALUES
(1, '1', 'Create users', 'SQL', 'V1__C_create_users.sql', NULL, 'inspectflow-migration', 0, true),
(2, '2', 'Create user_identities','SQL', 'V2__C_create_user_identities.sql', NULL, 'inspectflow-migration', 0, true),
(3, '3', 'Create memberships', 'SQL', 'V3__C_create_memberships.sql', NULL, 'inspectflow-migration', 0, true),
(4, '4', 'Create invitations', 'SQL', 'V4__C_create_invitations.sql', NULL, 'inspectflow-migration', 0, true),
(5, '5', 'Create login_events', 'SQL', 'V5__C_create_login_events.sql', NULL, 'inspectflow-migration', 0, true)
ON CONFLICT DO NOTHING;
Checksum caveat. Setting
checksum = NULLmakes Flyway skip checksum validation. If you want strict mode, run plate-auth on a scratch DB once, copy the checksums from itsflyway_schema_history_auth, and paste them here.
8.2 Data migration: memberships.company_id → (org_type, org_id)
Same migration file or separate V33__C_rewrite_memberships_to_polymorphic.sql:
-- Add new columns
ALTER TABLE memberships ADD COLUMN IF NOT EXISTS org_type varchar(64);
ALTER TABLE memberships ADD COLUMN IF NOT EXISTS org_id bigint;
-- Backfill from existing company_id
UPDATE memberships SET org_type = 'COMPANY', org_id = company_id WHERE company_id IS NOT NULL;
-- Index + non-null constraint
CREATE INDEX IF NOT EXISTS idx_memberships_org ON memberships(org_type, org_id);
ALTER TABLE memberships ALTER COLUMN org_type SET NOT NULL;
ALTER TABLE memberships ALTER COLUMN org_id SET NOT NULL;
-- Drop the old FK column (after verification!)
-- ALTER TABLE memberships DROP CONSTRAINT memberships_company_id_fkey;
-- ALTER TABLE memberships DROP COLUMN company_id;
Caution. Do not drop
company_idin the same migration. Keep it as a shadow column until the next minor release and verify no code still reads it.
9. Step F — Frontend swap
9.1 Delete files
frontend/auth.ts→ rewrite (5 lines, see § 9.3)frontend/app/api/auth/[...nextauth]/route.ts→ keep, no change neededfrontend/app/api/[...path]/route.ts→ rewrite (3 lines, see § 9.4)frontend/lib/hmac.ts(if exists) → delete; replaced by@platesoft/auth/edgefrontend/lib/auth-config.ts(if exists) → delete; replaced bycreateAuthConfigfactory
9.2 Keep files
frontend/middleware.ts— InspectFlow keeps its own (custom redirect logic for unauth users)frontend/components/login-form.tsxetc. if you have custom UI — leave them, ignore@platesoft/auth/react
9.3 New frontend/auth.ts
import { createAuthConfig } from "@platesoft/auth/server";
import NextAuth from "next-auth";
export const { handlers, auth, signIn, signOut } = NextAuth(
createAuthConfig({
backendUrl: process.env.PLATE_AUTH_BACKEND_URL!,
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",
},
})
);
9.4 New frontend/app/api/[...path]/route.ts
import { createProxyHandlers } from "@platesoft/auth/edge";
export const runtime = "edge";
export const { GET, POST, PUT, PATCH, DELETE } = createProxyHandlers({
backendUrl: process.env.PLATE_AUTH_BACKEND_URL!,
exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!,
});
That's it. Anything else InspectFlow needs (custom error pages, custom callbacks) is wired into NextAuth options that createAuthConfig exposes as overridable.
10. Step G — Run the E2E suite
cd frontend
pnpm exec playwright install --with-deps # if not already
pnpm exec playwright test
All 30+ scenarios in frontend/e2e/ must pass green. The critical ones for this migration:
auth-flow.unauth.spec.ts— anonymous redirect, login formcompanies.spec.ts— membership reads workadmin-archive.spec.ts,admin-timeline.spec.ts,admin-migration.spec.ts— admin role gatecommand-palette.spec.ts— JWT-protected ops- Any "login as admin / login as user" setup script in
auth.setup.ts
If any test fails, stop. Diagnose via:
- Check backend logs for
OrgValidator/ SPI warnings. - Check
login_eventstable for the failing user. - Diff the failing request between old + new code paths (proxy headers, exchange envelope shape).
11. Step H — Merge and clean up
After all tests pass:
- Squash-merge
feature/plate-auth-migrationtomain. - Tag InspectFlow as
vX.Y+1.0(minor bump — internal refactor with config breaking change for ops). - Update
docs/— add a section indocs/SPRINT-14-OVERVIEW.mdnoting the carve-out + library version. - One sprint later, drop
memberships.company_idcolumn in a follow-up migration (give yourself time to revert if production reveals issues). - Subscribe to plate-auth
0.2.0for the v0.2 feature wave (refresh rotation, magic-link, multi-key secrets).
12. Rollback procedure
If the migration goes sideways:
Code rollback
git checkout main
git revert feature/plate-auth-migration # or just deploy the previous tag
Database rollback
The data migration in § 8.2 is additive (memberships.org_type, org_id are new columns). The old company_id column is preserved. Rolling back the code means the old Membership entity reads company_id again and ignores the new columns — safe.
The Flyway baseline rows in flyway_schema_history_auth are harmless if the code is reverted — the table simply exists with 5 rows and no consumer.
Secret rollback
If you have already rotated PLATE_AUTH_EXCHANGE_SECRET and EXCHANGE_SECRET was different in the old code, restore the old env vars when reverting the code.
Total downtime
The migration is designed for zero-downtime deploy:
- New columns added before code switch (additive).
- New code reads new columns; old code reads old.
- Stop the old replica, start the new — one request is in flight, at worst.
If you cannot do zero-downtime, plan a 5-minute maintenance window.
13. What changes for end-users
Nothing. Logins, sessions, sign-ups, invitations, access requests, admin views — all behave identically. The change is internal: same APIs, same UX, different code path.
If a user reports a regression, treat it as a bug — file an InspectFlow ticket and a plate-auth ticket (since the regression is likely in the library, not in InspectFlow's residual code).
14. Common pitfalls (migration-specific)
| Symptom | Likely cause | Fix |
|---|---|---|
Boot fails with Field 'orgType' of Membership is null |
Forgot the data migration in § 8.2 | Run V33 manually |
| Flyway: "checksum mismatch on V1" | Baseline rows in § 8.1 have wrong checksums | Set flyway.validate-on-migrate=false temporarily, or recompute checksums |
OrgValidator always returns false |
Wrong orgType literal — case sensitive ("COMPANY" not "company") |
Check § 7 SPI binding |
| Frontend cannot reach backend → 502 | BACKEND_URL rename to PLATE_AUTH_BACKEND_URL not propagated to all envs |
Grep BACKEND_URL across repo + deploy configs |
E2E companies.spec.ts fails |
OrgDisplayNameResolver SPI not registered → display name shows Unbekannt |
Add bean in § 7 |
Admin chain order wrong → users get 403 on /api/admin/... |
@Order(0) missing on inspectflowAdminChain |
Add explicit order; default starter chain is @Order(100) |
15. Sign-off checklist
Before declaring migration complete:
- All Playwright E2E tests pass
- Manual smoke test: log in via Google, password, accept invite, request access, approve as admin
/admin/auditshowslogin_eventsrows from both old and new code paths (timestamps span the migration)- No
WARN/ERRORlog entries fromde.platesoft.auth.*in first 24h post-deploy - Database row counts before/after migration match (users, memberships, invitations, access_requests, login_events)
memberships.org_typeis non-null on all rows (use SQL audit)- Deleted backend classes (§ 5.1) are actually deleted (no zombies)
- Deleted frontend files (§ 9.1) are actually deleted
16. Cross-references
- Home.md
- Vision.md
- Architecture.md
- Roadmap.md
- Sprint-0-Assessment.md
- Sprint-0-Plan.md
- Sprint-0-Testplan.md
- Open-Questions.md
- Integration-Guide.md
End of Migration-InspectFlow.md (v1).