1
Migration InspectFlow
Patrick Plate edited this page 2026-06-24 14:32:48 +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.

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.
  • ~58 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-line auth.ts.
  • Flyway migrations V26..V31 are kept but marked as baseline rows in the new flyway_schema_history_auth table so plate-auth's V1..V5 are not re-applied.
  • InspectFlow keeps Company entity + OrgValidator SPI 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.1 validation 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.1 successfully (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: 12 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:

  • passwordEncoderPlateAuthAutoConfiguration provides BCryptPasswordEncoder with cost 10. Delete InspectFlow's.
  • authenticationManager → same. Delete InspectFlow's.
  • filterChain → starter's plateAuthSecurityChain covers /auth/** + JWT for the rest. Keep an InspectFlow-specific chain for paths that need custom rules (e.g. /api/admin/** requires hasRole('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-8601 Duration format (PT15M, PT60S, P30D). Spring Boot parses both 15m and PT15M, 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 = NULL makes Flyway skip checksum validation. If you want strict mode, run plate-auth on a scratch DB once, copy the checksums from its flyway_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_id in 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 needed
  • frontend/app/api/[...path]/route.ts → rewrite (3 lines, see § 9.4)
  • frontend/lib/hmac.ts (if exists) → delete; replaced by @platesoft/auth/edge
  • frontend/lib/auth-config.ts (if exists) → delete; replaced by createAuthConfig factory

9.2 Keep files

  • frontend/middleware.ts — InspectFlow keeps its own (custom redirect logic for unauth users)
  • frontend/components/login-form.tsx etc. 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 form
  • companies.spec.ts — membership reads work
  • admin-archive.spec.ts, admin-timeline.spec.ts, admin-migration.spec.ts — admin role gate
  • command-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:

  1. Check backend logs for OrgValidator / SPI warnings.
  2. Check login_events table for the failing user.
  3. 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:

  1. Squash-merge feature/plate-auth-migration to main.
  2. Tag InspectFlow as vX.Y+1.0 (minor bump — internal refactor with config breaking change for ops).
  3. Update docs/ — add a section in docs/SPRINT-14-OVERVIEW.md noting the carve-out + library version.
  4. One sprint later, drop memberships.company_id column in a follow-up migration (give yourself time to revert if production reveals issues).
  5. Subscribe to plate-auth 0.2.0 for 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/audit shows login_events rows from both old and new code paths (timestamps span the migration)
  • No WARN/ERROR log entries from de.platesoft.auth.* in first 24h post-deploy
  • Database row counts before/after migration match (users, memberships, invitations, access_requests, login_events)
  • memberships.org_type is 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


End of Migration-InspectFlow.md (v1).