Table of Contents
- Integration Guide
- 1. What you get
- 2. Prerequisites
- 3. Five-minute setup (backend)
- 3.1 Add Maven dependency
- 3.2 Required configuration (application.yml)
- 3.3 What auto-config wires for you
- 3.4 Plug in your domain (SPI seams)
- 3.5 Protect your endpoints
- 4. Five-minute setup (frontend)
- 4.1 Add npm dependency
- 4.2 Required env vars
- 4.3 Wire NextAuth via factory
- 4.4 NextAuth route handler (boilerplate, required)
- 4.5 API proxy (forward authenticated requests to backend)
- 4.6 Optional React components
- 5. Database setup
- 6. Minimum viable consumer app — full example
- 7. Operational considerations
- 8. Verification checklist (after first deploy)
- 9. Common pitfalls
- 10. Next steps after first integration
- 11. Cross-references
Integration Guide
Status: Draft v1
Date: 2026-06-24
Owner: Patrick
Audience: Greenfield Spring Boot 4.1 + Next.js 15 App Router consumers (primary target: Sparkboard)
Library version: 0.1.0
If you are migrating an existing app that already has its own auth (InspectFlow), see Migration-InspectFlow.md instead.
1. What you get
By adding de.platesoft:plate-auth-starter:0.1.0 (backend) + @platesoft/auth:0.1.0 (frontend) to your project, you get:
- Backend: REST endpoints for login, signup (opt-in), password reset, OAuth (Google), token exchange (HMAC-signed envelope), JWT-protected filter, audit, memberships, invitations, access requests.
- Frontend: NextAuth v5 config factory, edge-runtime API proxy, HMAC envelope sign/verify helpers, optional React components (
<AuthProvider>,<LoginForm>,<SignupForm>). - Schema: Five Postgres tables created via Flyway in a private
flyway_schema_history_authtable. - Extension points (SPI):
OrgValidator,OrgDisplayNameResolver,InvitationMailer,AccessRequestMailer,OnboardingHook— implement to plug your domain in without forking.
2. Prerequisites
| Item | Required | Notes |
|---|---|---|
| Java | 25 LTS | Matches Spring Boot 4.1 minimum |
| Spring Boot | 4.1.0+ | 4.0.x not supported (see Open-Questions.md Q08) |
| Postgres | 14+ (16 recommended) | Or compatible (e.g. CockroachDB — untested) |
| Node | 20+ (22 LTS recommended) | NextAuth v5 + Edge runtime |
| Next.js | 15+ App Router | Pages Router not supported |
| Gitea Maven registry access | Yes | URL + token in ~/.m2/settings.xml |
| Gitea npm registry access | Yes | .npmrc with @platesoft:registry=... |
3. Five-minute setup (backend)
3.1 Add Maven dependency
<dependency>
<groupId>de.platesoft</groupId>
<artifactId>plate-auth-starter</artifactId>
<version>0.1.0</version>
</dependency>
Configure the Gitea registry in pom.xml:
<repositories>
<repository>
<id>gitea-platesoft</id>
<url>https://git.plate-software.de/api/packages/platesoft/maven</url>
</repository>
</repositories>
Add credentials to ~/.m2/settings.xml:
<server>
<id>gitea-platesoft</id>
<username>your-gitea-user</username>
<password>your-gitea-token</password>
</server>
3.2 Required configuration (application.yml)
plate:
auth:
jwt:
secret: ${PLATE_AUTH_JWT_SECRET} # 32+ chars, hex/base64
access-expiration: PT15M
refresh-expiration: P30D
issuer: my-app
exchange:
secret: ${PLATE_AUTH_EXCHANGE_SECRET} # 32+ chars, distinct from jwt secret
max-age: PT60S
nonce-ttl: PT5M
registration:
enabled: false # invite-only by default
cors:
allowed-origins:
- https://app.example.com
providers:
google:
enabled: true
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
spring:
datasource:
url: jdbc:postgresql://localhost:5432/myapp
username: ${DB_USER}
password: ${DB_PASSWORD}
That's it for the bare minimum. Run your app — the starter auto-configures.
3.3 What auto-config wires for you
| Bean | Purpose |
|---|---|
JwtService |
Mints + parses JWTs |
ExchangeService |
HMAC envelope mint/consume |
JwtAuthenticationFilter |
Reads Authorization: Bearer ..., populates SecurityContext |
SecurityFilterChain (named plateAuthSecurityChain) |
Whitelists /auth/**, requires JWT elsewhere |
OrgValidator (default = PermissiveOrgValidator) |
Accepts any (org_type, org_id) and logs a WARN on every call ("OrgValidator default permissive — override de.platesoft.auth.spi.OrgValidator bean before production") — replace via SPI before production |
InvitationMailer (default = LoggingMailer) |
Logs mails to console — replace via SPI |
AccessRequestMailer (default = LoggingMailer) |
Same |
OnboardingHook (default = NoopOnboardingHook) |
Does nothing — replace via SPI |
All defaults are @ConditionalOnMissingBean — define your own bean of the same type to override.
3.4 Plug in your domain (SPI seams)
@Configuration
public class PlateAuthSpiConfig {
@Bean
public OrgValidator orgValidator(StudioRepository repo) {
return (orgType, orgId) ->
"STUDIO".equals(orgType) && repo.existsById(orgId);
}
@Bean
public OrgDisplayNameResolver orgDisplayNameResolver(StudioRepository repo) {
return (orgType, orgId) -> repo.findById(orgId)
.map(Studio::getName)
.orElse("Unknown");
}
@Bean
public InvitationMailer invitationMailer(MailService mail) {
return (toEmail, inviter, orgName, token) ->
mail.send(toEmail, "Invite to " + orgName, "Open: https://app.example.com/invite/" + token);
}
@Bean
public OnboardingHook onboardingHook(StudioService studios) {
return (userId, email, signupContext) -> {
// e.g. create a personal default studio for new sign-ups
studios.createForUser(userId, email);
};
}
}
3.5 Protect your endpoints
By default, anything outside /auth/** requires a valid JWT. To customize:
@Bean
@Order(0) // before plateAuthSecurityChain
public SecurityFilterChain myCustomChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/public/**")
.authorizeHttpRequests(a -> a.anyRequest().permitAll())
.build();
}
To read the current user in a controller:
@GetMapping("/api/me")
public MeResponse me(@AuthenticationPrincipal Jwt principal) {
return new MeResponse(principal.getSubject(), principal.getClaim("email"));
}
4. Five-minute setup (frontend)
4.1 Add npm dependency
# .npmrc must contain:
# @platesoft:registry=https://git.plate-software.de/api/packages/platesoft/npm/
# //git.plate-software.de/api/packages/platesoft/npm/:_authToken=YOUR_TOKEN
pnpm add @platesoft/auth@0.1.0
4.2 Required env vars
# .env.local
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=... # NextAuth v5 session secret, 32+ chars
PLATE_AUTH_EXCHANGE_SECRET=... # MUST match backend plate.auth.exchange.secret
PLATE_AUTH_BACKEND_URL=http://localhost:8080
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
4.3 Wire NextAuth via factory
Create 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",
},
})
);
4.4 NextAuth route handler (boilerplate, required)
Create frontend/app/api/auth/[...nextauth]/route.ts:
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
Note: NextAuth v5 requires this exact filename for its OAuth callback URLs to work. Do not rename it. This is a NextAuth limitation, not ours — see Roadmap.md for "remove boilerplate" v0.2 ambition.
4.5 API proxy (forward authenticated requests to backend)
Create 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!,
});
4.6 Optional React components
// app/login/page.tsx
import { LoginForm } from "@platesoft/auth/react";
export default function LoginPage() {
return <LoginForm onSuccess={() => window.location.href = "/dashboard"} />;
}
// app/layout.tsx
import { AuthProvider } from "@platesoft/auth/react";
import { auth } from "@/auth";
export default async function RootLayout({ children }) {
const session = await auth();
return (
<html>
<body>
<AuthProvider session={session}>{children}</AuthProvider>
</body>
</html>
);
}
If you want to ship your own UI, skip the React import — only @platesoft/auth/server + @platesoft/auth/edge are required.
5. Database setup
The starter ships Flyway migrations under classpath:db/migration/plate-auth/. They run automatically on startup against a dedicated history table (flyway_schema_history_auth).
If your app also uses Flyway with its own migrations:
spring:
flyway:
enabled: true # your migrations, your history table
locations: classpath:db/migration
table: flyway_schema_history # YOUR table
The starter's Flyway bean (plateAuthFlyway) runs before the application's main Flyway:
| Order | Bean | Locations | History table |
|---|---|---|---|
| 1 | plateAuthFlyway |
db/migration/plate-auth |
flyway_schema_history_auth |
| 2 | (your default Flyway) | db/migration |
flyway_schema_history |
You don't need to configure the starter's Flyway — it's wired automatically.
5.1 What's created
| Table | Purpose |
|---|---|
users |
Identity record + password hash (nullable for OAuth-only) |
user_identities |
Provider linkage (google, password, ms-entra) |
memberships |
(user_id, org_type, org_id, role) — polymorphic FK |
invitations |
Token-hashed invitations |
access_requests |
Self-service access requests |
login_events |
Audit (success/failure, IP, UA) |
See Architecture.md § 6 ER diagram for the schema.
6. Minimum viable consumer app — full example
A working hello world consumer:
Backend (SparkboardApplication.java)
@SpringBootApplication
public class SparkboardApplication {
public static void main(String[] args) {
SpringApplication.run(SparkboardApplication.class, args);
}
}
@RestController
class HelloController {
@GetMapping("/api/hello")
public Map<String, String> hello(@AuthenticationPrincipal Jwt principal) {
return Map.of("hello", principal.getClaim("email"));
}
}
That's it. Add the dependency, set the 4 env vars, and /api/hello is JWT-protected.
Frontend (app/page.tsx)
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function Home() {
const session = await auth();
if (!session) redirect("/login");
const res = await fetch("http://localhost:3000/api/hello", {
headers: { Cookie: (await import("next/headers")).cookies().toString() },
});
const { hello } = await res.json();
return <h1>Hello, {hello}</h1>;
}
7. Operational considerations
7.1 Secrets
| Secret | Length | Rotation strategy (v0.1) |
|---|---|---|
plate.auth.jwt.secret |
≥32 chars | Rotate → all sessions invalidated. Plan downtime. |
plate.auth.exchange.secret |
≥32 chars | Rotate → all in-flight envelopes rejected. Coordinate rolling deploy with frontend. |
NEXTAUTH_SECRET |
≥32 chars | Rotate → all NextAuth sessions invalidated. |
v0.2 will add multi-key support (key-id header). For v0.1: rotate during planned maintenance.
7.2 Logging
Set logging.level.de.platesoft.auth=INFO in production. DEBUG reveals envelope details (without secrets) — useful for troubleshooting but verbose.
7.3 Replication / scale-out
v0.1 nonce store is in-memory (ConcurrentHashMap). Replicas do not share the nonce set. Consequence: if the frontend sends the same envelope to a different replica, the replay-detection misses. Mitigations:
- Sticky sessions (load balancer affinity by user)
- Single replica (acceptable for v0.1 consumers)
- Wait for v0.2
NonceStoreSPI with Redis backend
7.4 Migrations on existing DBs
For greenfield (Sparkboard): nothing special. For existing DBs with conflicting tables (e.g. you already have users): rename your tables or talk to Patrick — there's no tablePrefix option in v0.1 (see Roadmap.md v0.2).
8. Verification checklist (after first deploy)
Run these manually to verify the integration is healthy:
- App boots without errors. Logs show
Started ... in X seconds. curl http://localhost:8080/auth/healthreturns 200 OK.- DB has 6 new tables / index objects under your schema.
flyway_schema_history_authhas 6 rows (V1..V6). curl -d '{"email":"a","password":"b"}' http://localhost:8080/auth/loginreturns 401 with structured JSON error (not a stack trace).- Frontend
/loginpage renders. - Sign-in with Google works → redirect →
/api/helloreturns email. - WARN logs
"OrgValidator default permissive — override ..."are gone — meaning you registered a realOrgValidatorbean. Until you do, the default fires on every membership check. - No remaining
LoggingMailerwarnings (you should have replacedInvitationMailerandAccessRequestMailer).
9. Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
BindValidationException: jwt.secret at boot |
Secret missing or < 32 chars | Set env var |
401 on every /api/... call from frontend |
exchangeSecret mismatch between FE and BE |
Verify both sides read same secret |
OAuth: redirect_uri_mismatch from Google |
Google console not updated with your URL | Add https://app.example.com/api/auth/callback/google to Google OAuth client |
409 nonce already used on every login |
Sticky sessions disabled, multiple replicas | Use single replica or wait for v0.2 NonceStore |
| Flyway: "schema not empty" on a fresh DB | Your existing tables collide with users, etc. |
See Migration-InspectFlow.md for the existing-DB recipe |
@AuthenticationPrincipal returns null |
Filter chain order — your custom chain ran before plateAuthSecurityChain and short-circuited |
Set @Order on your custom chain explicitly |
10. Next steps after first integration
- Replace
PermissiveOrgValidatorwith a real one (your org table). - Replace
LoggingMailerwithJavaMailSender+ your SMTP config. - Add
OnboardingHookto create default resources for new users. - Subscribe to Roadmap.md v0.2 — refresh-token rotation + magic-link + multi-key secrets.
- Read Open-Questions.md to understand which decisions might shift in v0.2.
11. 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
- Migration-InspectFlow.md
End of Integration-Guide.md (v1).