docs(architecture): plate-auth consumer model — single-org SPARK_ORG, OnboardingHook, polymorphic FK
+540
@@ -0,0 +1,540 @@
|
||||
# Architecture
|
||||
|
||||
Sparkboard is a **greenfield consumer of [plate-auth](https://git.plate-software.de/pplate/plate-auth/wiki)**. Everything in this document is about how Sparkboard wires itself into that library, what it adds on top, and what it deliberately leaves out.
|
||||
|
||||
If you have not already read [plate-auth — Architecture](https://git.plate-software.de/pplate/plate-auth/wiki/Architecture), read that first. This document assumes you know the tier model (T1 auth core, T2 multi-tenancy, T3 domain) and the SPI seams.
|
||||
|
||||
---
|
||||
|
||||
## 1. System shape
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ sparkboard.plate-software.de │
|
||||
│ (IONOS Apache + Let's Encrypt) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│ TLS
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ frps tunnel (TrueNAS) — port 30011 — frpc client │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────┴───────────────────────────────┐
|
||||
│ TrueNAS Docker network │
|
||||
│ │
|
||||
│ ┌────────────────┐ / ┌─────────────────┐ │
|
||||
│ │ frontend │ /api/* │ backend │ │
|
||||
│ │ Next.js 15 │───────────────▶│ Spring Boot 4.1 │ │
|
||||
│ │ + NextAuth v5 │ Bearer JWT │ + plate-auth │ │
|
||||
│ │ + @platesoft/ │ (issued by │ + Sparkboard │ │
|
||||
│ │ auth/proxy │ plate-auth) │ domain │ │
|
||||
│ └────────────────┘ └──────────┬──────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ Postgres 16 │ │
|
||||
│ │ (single DB) │ │
|
||||
│ └──────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Google OAuth 2.0
|
||||
(Sprint 1: Google only)
|
||||
```
|
||||
|
||||
The Spring Boot backend hosts **both** plate-auth's endpoints (`/api/auth/**`, `/api/me`, `/api/memberships/**`) and Sparkboard's domain endpoints (`/api/ideas/**`). Both sets of endpoints live in the same `JAR`, mounted by the same `DispatcherServlet`, signed by the same backend JWT.
|
||||
|
||||
There is no separate "auth service". Plate-auth is a library, not a microservice. Sparkboard is one process.
|
||||
|
||||
---
|
||||
|
||||
## 2. Two artifacts that Sparkboard depends on
|
||||
|
||||
| Artifact | What | Pulled from |
|
||||
|----------|------|------------|
|
||||
| `de.platesoft:plate-auth-starter:0.1.0` | Spring Boot 4.1 starter — auto-configures everything in T1 + T2 | Gitea Maven registry at `git.plate-software.de` |
|
||||
| `@platesoft/auth@0.1.0` | npm package — NextAuth v5 factory + proxy handler + React hooks | Gitea npm registry at `git.plate-software.de` |
|
||||
|
||||
Both ship lockstep at v0.1.0. See [plate-auth Roadmap](https://git.plate-software.de/pplate/plate-auth/wiki/Roadmap) for versioning policy.
|
||||
|
||||
**Sparkboard does not vendor, copy, or fork any code from plate-auth.** It depends on the published artifacts. Period.
|
||||
|
||||
---
|
||||
|
||||
## 3. Tier model — what Sparkboard owns
|
||||
|
||||
Plate-auth's tier model maps onto Sparkboard like this:
|
||||
|
||||
| Tier | Owner | Sparkboard's involvement |
|
||||
|------|-------|--------------------------|
|
||||
| **T1 — Auth core** (sessions, JWT, exchange, providers, allowlist) | plate-auth | Configure via `plate.auth.*` properties only. Zero code. |
|
||||
| **T2 — Multi-tenancy** (`memberships`, invitations, access-requests) | plate-auth | Configure for **single-org mode**: hide invitation UI, disable access-request flow. Membership table is real and populated. |
|
||||
| **T3 — Consumer onboarding + domain** | **Sparkboard** | Implement [`SparkboardOnboardingHook`](#single-org-mode-and-onboardinghook). Implement the [`Idea` domain](#5-sparkboard-domain). |
|
||||
|
||||
Everything else is "left as default" — Sparkboard does **not** implement `OrgValidator`, `OrgDisplayNameResolver`, `InvitationMailer`, or `AccessRequestMailer` SPIs in Sprint 1. They are not needed when there's exactly one org and no invitations.
|
||||
|
||||
---
|
||||
|
||||
## 4. Single-org mode and OnboardingHook
|
||||
|
||||
Sparkboard is the canonical "single-org consumer" pattern for plate-auth. It is what the [plate-auth Integration Guide](https://git.plate-software.de/pplate/plate-auth/wiki/Integration-Guide) describes as the minimum-viable case.
|
||||
|
||||
### 4.1 The single org
|
||||
|
||||
One row, seeded by Flyway, in the `memberships` table — actually in a tiny `spark_org` table that Sparkboard owns:
|
||||
|
||||
```sql
|
||||
-- backend/src/main/resources/db/migration/V2__seed_family_spark_org.sql
|
||||
CREATE TABLE spark_org (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(80) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO spark_org (id, name) VALUES
|
||||
('00000000-0000-0000-0000-000000000001', 'Family Spark');
|
||||
```
|
||||
|
||||
The org_id `00000000-0000-0000-0000-000000000001` is a magic constant. There is exactly one. See [Open Question Q01](Open-Questions.md#q01-single-org-name-and-id-strategy) for the discussion about whether it should be configurable.
|
||||
|
||||
### 4.2 The polymorphic FK contract
|
||||
|
||||
Plate-auth's `memberships` table has the polymorphic shape:
|
||||
|
||||
```
|
||||
memberships:
|
||||
user_id UUID NOT NULL → auth_identities.user_id
|
||||
org_type VARCHAR NOT NULL -- discriminator, set by consumer
|
||||
org_id UUID NOT NULL -- validated by OrgValidator SPI
|
||||
role VARCHAR NOT NULL -- 'ADMIN' | 'MEMBER'
|
||||
PRIMARY KEY (user_id, org_type, org_id)
|
||||
```
|
||||
|
||||
For Sparkboard, every row uses `org_type = 'SPARK_ORG'` and `org_id = 00000000-0000-0000-0000-000000000001`.
|
||||
|
||||
Because Sparkboard has exactly one org and trusts itself, it accepts plate-auth's **default no-op `OrgValidator`** in Sprint 1. (A future sprint may implement a strict `OrgValidator` that checks `spark_org.id` exists — but it's a 5-line implementation; not worth doing in Sprint 1.)
|
||||
|
||||
### 4.3 SparkboardOnboardingHook
|
||||
|
||||
This is the **one SPI bean** Sparkboard implements. It's how a newly-signed-in Google user gets a membership row.
|
||||
|
||||
```java
|
||||
// backend/src/main/java/de/plate/sparkboard/onboarding/SparkboardOnboardingHook.java
|
||||
package de.plate.sparkboard.onboarding;
|
||||
|
||||
import de.platesoft.auth.spi.OnboardingHook;
|
||||
import de.platesoft.auth.spi.OnboardingContext;
|
||||
import de.platesoft.auth.membership.MembershipService;
|
||||
import de.platesoft.auth.membership.Role;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class SparkboardOnboardingHook implements OnboardingHook {
|
||||
|
||||
public static final String ORG_TYPE = "SPARK_ORG";
|
||||
public static final UUID FAMILY_SPARK_ID =
|
||||
UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
|
||||
private final MembershipService memberships;
|
||||
private final SparkboardAdminProperties admins;
|
||||
|
||||
public SparkboardOnboardingHook(MembershipService memberships,
|
||||
SparkboardAdminProperties admins) {
|
||||
this.memberships = memberships;
|
||||
this.admins = admins;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterFirstLogin(OnboardingContext ctx) {
|
||||
Role role = admins.isAdminEmail(ctx.email()) ? Role.ADMIN : Role.MEMBER;
|
||||
memberships.upsert(ctx.userId(), ORG_TYPE, FAMILY_SPARK_ID, role);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`SparkboardAdminProperties` reads `sparkboard.admins[]` from `application.yml`:
|
||||
|
||||
```yaml
|
||||
sparkboard:
|
||||
admins:
|
||||
- patrick@plate-software.de
|
||||
- <friend's email>
|
||||
```
|
||||
|
||||
Everyone else who passes plate-auth's allowlist becomes a `MEMBER`.
|
||||
|
||||
### 4.4 What the hook does NOT do
|
||||
|
||||
- It does **not** create the org. The org is Flyway-seeded once.
|
||||
- It does **not** decide allowlisting. That is plate-auth's `plate.auth.allowlist` config.
|
||||
- It does **not** send a welcome email. (Sprint 4+ candidate.)
|
||||
- It does **not** create any Sparkboard-domain data. (No "default idea" on first login.)
|
||||
|
||||
It is **idempotent**: `memberships.upsert` is a `INSERT … ON CONFLICT DO NOTHING`. Re-running it on every login is fine and is the simplest implementation. Plate-auth promises to call it on first login only, but Sparkboard does not rely on that promise being bug-free.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sparkboard domain
|
||||
|
||||
In Sprint 1, Sparkboard owns **exactly one** domain table: `ideas`.
|
||||
|
||||
### 5.1 ER diagram (Sprint 1)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
auth_identities ||--o{ memberships : "has"
|
||||
spark_org ||--o{ memberships : "has (polymorphic, org_type='SPARK_ORG')"
|
||||
auth_identities ||--o{ ideas : "authored"
|
||||
spark_org ||--o{ ideas : "scoped to (always Family Spark in v1)"
|
||||
|
||||
auth_identities {
|
||||
UUID user_id PK
|
||||
VARCHAR email
|
||||
VARCHAR display_name
|
||||
TIMESTAMPTZ created_at
|
||||
}
|
||||
|
||||
memberships {
|
||||
UUID user_id PK,FK
|
||||
VARCHAR org_type PK
|
||||
UUID org_id PK
|
||||
VARCHAR role
|
||||
TIMESTAMPTZ created_at
|
||||
}
|
||||
|
||||
spark_org {
|
||||
UUID id PK
|
||||
VARCHAR name
|
||||
TIMESTAMPTZ created_at
|
||||
}
|
||||
|
||||
ideas {
|
||||
UUID id PK
|
||||
UUID org_id FK
|
||||
UUID author_id FK
|
||||
VARCHAR title
|
||||
TEXT description
|
||||
VARCHAR status
|
||||
TIMESTAMPTZ created_at
|
||||
TIMESTAMPTZ updated_at
|
||||
TIMESTAMPTZ archived_at
|
||||
}
|
||||
```
|
||||
|
||||
> The `auth_identities`, `memberships`, `invitations`, and `access_requests` tables are **owned and migrated by plate-auth**. Sparkboard does not touch them. Plate-auth tracks its own Flyway state in [`flyway_schema_history_auth`](https://git.plate-software.de/pplate/plate-auth/wiki/Architecture#9-distribution); Sparkboard tracks its own in `flyway_schema_history`.
|
||||
|
||||
### 5.2 Idea entity (Java)
|
||||
|
||||
```java
|
||||
// backend/src/main/java/de/plate/sparkboard/idea/Idea.java
|
||||
@Entity
|
||||
@Table(name = "ideas")
|
||||
public class Idea {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "org_id", nullable = false)
|
||||
private UUID orgId; // always FAMILY_SPARK_ID in v1
|
||||
|
||||
@Column(name = "author_id", nullable = false)
|
||||
private UUID authorId; // plate-auth user_id
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private IdeaStatus status = IdeaStatus.RAW;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt;
|
||||
|
||||
@Column(name = "archived_at")
|
||||
private Instant archivedAt;
|
||||
|
||||
// getters, setters, lifecycle callbacks omitted
|
||||
}
|
||||
|
||||
public enum IdeaStatus { RAW, EXPLORING, BUILDING, SHIPPED, DEAD }
|
||||
```
|
||||
|
||||
In Sprint 1, only `RAW` is ever set. Status transitions ship in Sprint 2.
|
||||
|
||||
### 5.3 Why `org_id` is on every idea row
|
||||
|
||||
Even though there is only one org in v1, every `Idea` carries an `org_id`. This is **deliberate forward-compatibility**:
|
||||
|
||||
- If a future Sparkboard ever supports multiple boards (e.g., "Family Spark" + "Work Spark"), the data model already handles it.
|
||||
- Read queries always filter by `WHERE org_id = ?` — the constant is read from the authenticated user's membership row.
|
||||
- It costs one column and one index. It is cheap insurance.
|
||||
|
||||
See [Open Question Q01](Open-Questions.md#q01-single-org-name-and-id-strategy).
|
||||
|
||||
---
|
||||
|
||||
## 6. End-to-end sign-in flow
|
||||
|
||||
This is the same flow plate-auth describes — Sparkboard adds zero steps. It is included here so a Sparkboard reader doesn't need to context-switch wikis.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as User (browser)
|
||||
participant FE as Next.js / NextAuth
|
||||
participant G as Google OAuth
|
||||
participant BE as Spring Boot + plate-auth + Sparkboard
|
||||
participant DB as Postgres
|
||||
|
||||
U->>FE: GET /login
|
||||
FE->>G: redirect to Google consent
|
||||
G-->>FE: ?code=...
|
||||
FE->>FE: NextAuth signIn callback
|
||||
Note over FE: @platesoft/auth/next-auth factory
|
||||
FE->>BE: POST /api/auth/exchange (HMAC-signed envelope: provider, providerId, email, name)
|
||||
BE->>BE: plate-auth verifies HMAC + provider
|
||||
BE->>BE: plate-auth checks allowlist
|
||||
BE->>DB: SELECT / INSERT auth_identities
|
||||
Note over BE,DB: First login? → fire OnboardingHook
|
||||
BE->>BE: SparkboardOnboardingHook.afterFirstLogin
|
||||
BE->>DB: INSERT memberships (user_id, 'SPARK_ORG', FAMILY_SPARK_ID, role)
|
||||
BE-->>FE: { userId, accessToken (15m JWT), refreshToken }
|
||||
FE->>FE: NextAuth session populated
|
||||
FE-->>U: redirect to /ideas
|
||||
```
|
||||
|
||||
Sparkboard contributes exactly one step: step 8 (the hook). Everything else is plate-auth.
|
||||
|
||||
---
|
||||
|
||||
## 7. Request → API flow (authenticated)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant FE as Next.js (App Router)
|
||||
participant Proxy as /api/backend/[...path] (Sparkboard)
|
||||
participant BE as Spring Boot
|
||||
|
||||
U->>FE: GET /ideas (server component)
|
||||
FE->>FE: const session = await auth()
|
||||
FE->>Proxy: fetch('/api/backend/api/ideas', { cache: 'no-store' })
|
||||
Note over Proxy: createProxyHandlers from @platesoft/auth/proxy
|
||||
Proxy->>Proxy: Inject Authorization: Bearer <session.accessToken>
|
||||
Proxy->>BE: GET /api/ideas
|
||||
BE->>BE: plate-auth JwtAuthFilter validates token
|
||||
BE->>BE: SecurityContext = { userId, email, memberships }
|
||||
BE->>BE: IdeaController.list(authenticatedUser)
|
||||
BE->>BE: ideaRepo.findByOrgIdOrderByCreatedAtDesc(FAMILY_SPARK_ID)
|
||||
BE-->>Proxy: 200 OK + JSON
|
||||
Proxy-->>FE: 200 OK + JSON
|
||||
FE-->>U: HTML with idea list
|
||||
```
|
||||
|
||||
Sparkboard's proxy route is **a single line** thanks to `@platesoft/auth/proxy`:
|
||||
|
||||
```typescript
|
||||
// frontend/app/api/backend/[...path]/route.ts
|
||||
export const { GET, POST, PUT, PATCH, DELETE } = createProxyHandlers({
|
||||
backendUrl: process.env.PLATE_AUTH_BACKEND_URL!,
|
||||
exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!,
|
||||
});
|
||||
```
|
||||
|
||||
This replaces the ~50 lines of hand-rolled proxy code that InspectFlow had (and CannaManage copy-pasted).
|
||||
|
||||
---
|
||||
|
||||
## 8. Package layout (backend)
|
||||
|
||||
```
|
||||
backend/
|
||||
├── pom.xml
|
||||
└── src/main/java/de/plate/sparkboard/
|
||||
├── SparkboardApplication.java
|
||||
├── onboarding/
|
||||
│ ├── SparkboardOnboardingHook.java
|
||||
│ └── SparkboardAdminProperties.java
|
||||
├── idea/
|
||||
│ ├── Idea.java
|
||||
│ ├── IdeaStatus.java
|
||||
│ ├── IdeaRepository.java
|
||||
│ ├── IdeaService.java
|
||||
│ ├── IdeaController.java
|
||||
│ ├── IdeaDto.java
|
||||
│ └── CreateIdeaRequest.java
|
||||
└── config/
|
||||
└── SparkboardSecurityConfig.java // optional — plate-auth's default may suffice
|
||||
```
|
||||
|
||||
Notably absent (because plate-auth owns them):
|
||||
|
||||
- No `User` / `UserRepository` / `UserService`
|
||||
- No `JwtAuthFilter` / `JwtUtil` / `JwtAuthenticationToken`
|
||||
- No `SecurityConfig` for auth wiring (plate-auth ships one; Sparkboard may extend it for `/api/ideas/**` if needed but Spring's default URL-pattern matching usually suffices)
|
||||
- No `AllowlistProperties` / allowlist endpoint
|
||||
- No `AuthExchangeController`
|
||||
- No membership / invitation / access-request controllers
|
||||
|
||||
That deletion list **is the value proposition of plate-auth**, made visible.
|
||||
|
||||
---
|
||||
|
||||
## 9. Package layout (frontend)
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── package.json (depends on @platesoft/auth: 0.1.0)
|
||||
├── next.config.ts
|
||||
├── auth.ts // NextAuth v5 factory call
|
||||
├── middleware.ts // re-export from @platesoft/auth/middleware (optional)
|
||||
└── app/
|
||||
├── api/
|
||||
│ ├── auth/[...nextauth]/route.ts // export { handlers as GET, handlers as POST } = handlers
|
||||
│ └── backend/[...path]/route.ts // createProxyHandlers(...)
|
||||
├── (auth)/login/page.tsx
|
||||
└── (app)/
|
||||
├── layout.tsx
|
||||
├── ideas/
|
||||
│ ├── page.tsx // list (server component)
|
||||
│ ├── new/page.tsx // create form
|
||||
│ └── [id]/page.tsx // detail (Sprint 2)
|
||||
└── components/
|
||||
├── idea-form.tsx
|
||||
└── idea-list.tsx
|
||||
```
|
||||
|
||||
There is **no** hand-rolled `lib/auth.ts` doing NextAuth config. The factory pattern from `@platesoft/auth/next-auth` collapses it to ~10 lines. See [Integration Guide](Integration-Guide.md#21-nextauthts-factory-wire-up).
|
||||
|
||||
---
|
||||
|
||||
## 10. Configuration
|
||||
|
||||
### 10.1 Backend `application.yml`
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
application:
|
||||
name: sparkboard
|
||||
datasource:
|
||||
url: jdbc:postgresql://${DB_HOST:postgres}:${DB_PORT:5432}/${DB_NAME:sparkboard}
|
||||
username: ${DB_USER:sparkboard}
|
||||
password: ${DB_PASSWORD}
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
|
||||
plate:
|
||||
auth:
|
||||
jwt:
|
||||
secret: ${PLATE_AUTH_JWT_SECRET}
|
||||
access-expiration: PT15M
|
||||
refresh-expiration: P30D
|
||||
exchange:
|
||||
secret: ${PLATE_AUTH_EXCHANGE_SECRET}
|
||||
registration:
|
||||
enabled: false # disabled — single-org allowlist mode
|
||||
allowlist:
|
||||
enabled: true
|
||||
emails:
|
||||
- patrick@plate-software.de
|
||||
- <friend@example.com>
|
||||
- <son1@example.com>
|
||||
- <son2@example.com>
|
||||
providers:
|
||||
google:
|
||||
enabled: true
|
||||
client-id: ${GOOGLE_CLIENT_ID}
|
||||
client-secret: ${GOOGLE_CLIENT_SECRET}
|
||||
|
||||
sparkboard:
|
||||
admins:
|
||||
- patrick@plate-software.de
|
||||
- <friend@example.com>
|
||||
```
|
||||
|
||||
All seven plate-auth properties above are documented in [plate-auth Architecture §3.3](https://git.plate-software.de/pplate/plate-auth/wiki/Architecture#33-configuration-properties-plateauth).
|
||||
|
||||
### 10.2 Frontend `.env.local`
|
||||
|
||||
```bash
|
||||
NEXTAUTH_SECRET=...
|
||||
NEXTAUTH_URL=https://sparkboard.plate-software.de
|
||||
|
||||
PLATE_AUTH_BACKEND_URL=http://backend:8080
|
||||
PLATE_AUTH_EXCHANGE_SECRET=... # same secret as backend's plate.auth.exchange.secret
|
||||
|
||||
GOOGLE_CLIENT_ID=...
|
||||
GOOGLE_CLIENT_SECRET=...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Database
|
||||
|
||||
- **Engine:** Postgres 16, single instance, single database called `sparkboard`.
|
||||
- **Migrations:** two Flyway histories side-by-side:
|
||||
- `flyway_schema_history` — Sparkboard's own (table `ideas`, `spark_org`)
|
||||
- `flyway_schema_history_auth` — plate-auth's (tables `auth_identities`, `memberships`, `invitations`, `access_requests`)
|
||||
- **No H2.** Not even in tests. Tests use Testcontainers Postgres.
|
||||
|
||||
This is a hard departure from InspectFlow's earlier H2-first stance. Plate-auth ships Postgres-only schemas; mixing H2 in is not worth the maintenance cost for a 4-user app.
|
||||
|
||||
---
|
||||
|
||||
## 12. Deployment
|
||||
|
||||
Same shape as InspectFlow and CannaManage. See [Sprint-1-Plan §W6](Sprint-1-Plan.md) for the actual files.
|
||||
|
||||
| Component | Where | How |
|
||||
|-----------|-------|-----|
|
||||
| TLS termination | IONOS Apache (`sparkboard.plate-software.de`) | Existing wildcard cert |
|
||||
| Tunnel | `frps` on TrueNAS, `frpc` on the IONOS box, port **30011** | New port per plate-software app |
|
||||
| Frontend | TrueNAS Docker, Next.js standalone build | `pnpm build && node server.js` |
|
||||
| Backend | TrueNAS Docker, Spring Boot fat JAR | `java -jar app.jar` |
|
||||
| Database | TrueNAS Docker, Postgres 16 image | Persistent volume on TrueNAS pool |
|
||||
| CI | Gitea Actions in `sparkboard` repo | Build → push images to Gitea registry → SSH-deploy script |
|
||||
|
||||
Port allocations across plate-software:
|
||||
|
||||
| App | frpc port |
|
||||
|-----|-----------|
|
||||
| InspectFlow | 30009 |
|
||||
| CannaManage | 30010 |
|
||||
| **Sparkboard** | **30011** |
|
||||
| plate-auth (if ever self-hosted as a demo) | 30012 |
|
||||
|
||||
---
|
||||
|
||||
## 13. Threat model summary
|
||||
|
||||
Inherited from plate-auth wholesale — see [plate-auth Architecture §10](https://git.plate-software.de/pplate/plate-auth/wiki/Architecture#10-threat-model-summary).
|
||||
|
||||
Sparkboard-specific additions:
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Idea is visible to a user not in the family org | All `Idea` reads filter on `org_id`; the authenticated user's `memberships` row pins the org. |
|
||||
| 5th Google account leaks in | `plate.auth.allowlist.emails` enforced server-side by plate-auth; allowlist is a hardcoded list of four addresses in v1 (see [Open Question Q02](Open-Questions.md#q02-allowlist-management)). |
|
||||
| Token reuse after admin removes a user | Out of scope for v1 (no admin UI). Mitigated operationally by short access-token expiration (15 min) and the fact that there is no remove-user flow yet. |
|
||||
|
||||
---
|
||||
|
||||
## 14. What this document is not
|
||||
|
||||
- Not a re-explanation of plate-auth internals — read [plate-auth Architecture](https://git.plate-software.de/pplate/plate-auth/wiki/Architecture) for that.
|
||||
- Not a step-by-step setup guide — see [Integration Guide](Integration-Guide.md) for the Sparkboard-flavoured walkthrough.
|
||||
- Not a description of Sprint 2+ design (reactions, comments, tags) — see [Roadmap](Roadmap.md).
|
||||
Reference in New Issue
Block a user