03 — System Architecture
Project: CannaManage — B2B SaaS for German Cannabis Social Clubs (Anbauvereinigungen) Phase: Sprint 14 — Marketing & Monetization Stack: Spring Boot 4.0.6 (Java 21) · JPA/Hibernate 7 · PostgreSQL 16 · Next.js 15 (React 19) Last updated: 2026-06-19
1. Architecture Overview
graph TD
Internet["🌍 Internet"]
subgraph TrueNAS ["🖧 TrueNAS Docker — Production"]
Nginx["🔒 Nginx\n(reverse proxy + TLS)"]
subgraph Frontend ["⚛️ Next.js 15 Application"]
Marketing["Marketing Pages\n(Landing, Pricing, Login)"]
AdminUI["Admin Dashboard\n(18 sections)"]
PortalUI["Member Portal\n(self-service)"]
end
subgraph Backend ["☕ Spring Boot 4.0.6 (Java 21)"]
REST["REST API Layer\n33 controllers · /api/v1/"]
Service["Service Layer\n40+ services"]
JPA["JPA / Hibernate 7\nRepositories"]
Security["Dual SecurityFilterChain\nJWT (admin/staff) + Session (portal)"]
Audit["Audit Log\n(immutable trail)"]
Cache["Caffeine Cache\n(token revocation)"]
end
PG[("🐘 PostgreSQL 16\nmulti-tenant via schema")]
end
Internet -->|"HTTPS :443"| Nginx
Nginx --> Frontend
Nginx -->|"proxy_pass :8080"| Backend
Frontend -->|"REST/JSON"| Backend
REST --> Service
Service --> JPA
Service --> Cache
Service --> Audit
Security --> REST
JPA -->|"JDBC"| PG
Backend -->|"Stripe SDK"| Stripe["💳 Stripe\n(SEPA, PayPal, Card)"]
Backend -->|"SMTP"| Mail["📧 Mail\n(notifications, invites)"]
Backend -->|"OpenPDF"| PDF["📄 PDF Reports"]
Component Responsibilities
| Component | Technology | Role |
|---|---|---|
| Marketing Pages | Next.js 15 (SSR) | Public landing, pricing, login |
| Admin Dashboard | Next.js 15 (CSR) | 18-section club management UI |
| Member Portal | Next.js 15 (CSR) | Member self-service: quota, history, events |
| REST API | Spring Boot 4.0.6 / Spring MVC | 33 controllers, 100+ endpoints |
| Auth (Admin/Staff) | Spring Security 7 + JJWT | Stateless JWT authentication |
| Auth (Portal) | Spring Security 7 + HttpSession | Session-based member authentication |
| Token Revocation | Caffeine cache + DB backing | In-memory revocation check with automatic cleanup |
| Staff Permissions | JSONB + annotation checker | 8 granular permissions, role templates |
| ORM | JPA / Hibernate 7 | Entity persistence, tenant filtering |
| Database | PostgreSQL 16 | Primary data store (schema-per-tenant) |
| Migrations | Flyway 10 (V1–V36) | Versioned schema management |
| Payments | Stripe Java SDK | SEPA, PayPal, Credit Card subscriptions |
| Bank Import | Custom parsers | MT940, CAMT053, CSV statement parsing + auto-matching |
| Spring Mail (SMTP) | Notifications, invites, alerts | |
| OpenPDF (iText fork, LGPL) | Compliance report generation | |
| CSV | Apache Commons CSV | Semicolon-delimited reports, ISO-8859-1 |
| Audit | Custom audit service | Immutable trail for compliance actions |
| Testing | Testcontainers + JaCoCo | Real-DB integration tests, 80% coverage gate |
| CI/CD | Gitea Actions | PostgreSQL service container, automated pipeline |
| Hosting | TrueNAS Docker + Nginx | Production at cannamanage.plate-software.de |
2. Multi-Tenancy Strategy
Decision: Schema-Per-Tenant
Each club gets its own PostgreSQL schema (e.g. tenant_abc123). A platform-level public schema holds only the tenants registry. Flyway runs per-schema migrations on onboarding.
Why schema-per-tenant, not shared schema?
| Concern | Shared Schema | Schema-Per-Tenant |
|---|---|---|
| Data isolation | Application-layer only — one missing filter = data leak | Enforced at DB level — schemas are hard boundaries |
| DSGVO compliance | Harder to prove isolation; one backup contains all clubs' data | Per-tenant pg_dump; each club's data is cleanly separable |
| Deletion / right to erasure | Must DELETE WHERE tenant_id = ? across every table |
DROP SCHEMA tenant_abc123 CASCADE — clean and auditable |
| Migrations | One migration path for all | Per-schema migration via Flyway schemas config |
| Query performance | Cross-tenant index bloat on large shared tables | Smaller per-tenant tables; no cross-tenant contention |
| Future per-club DB isolation | Requires full re-architecture | Trivial: move schema to dedicated DB server |
Conclusion: For a compliance SaaS handling personal health-adjacent data (cannabis consumption records), schema-per-tenant is the correct design from Day 1. The migration complexity is manageable; the data isolation benefit is permanent.
Tenant Provisioning
POST /api/v1/admin/bootstrap
→ TenantProvisioningService.provisionTenant(tenantId)
→ CREATE SCHEMA tenant_{tenantId}
→ Flyway.migrate(schema=tenant_{tenantId}) // applies all V1–V36
→ INSERT INTO public.tenants (id, schema_name, onboarded_at, status)
Tenant Resolution
HTTP Request
└─ Spring Security Filter: extract JWT → resolve tenant_id
└─ TenantContext.setCurrentTenant(tenantId) // ThreadLocal
└─ DataSource routes to schema: SET search_path = tenant_{tenantId}
└─ All queries execute in tenant's private schema
3. Authentication & Authorization
Dual SecurityFilterChain
graph LR
subgraph Chain1 ["JWT Chain (Order 1) — /api/v1/**"]
JF[JwtAuthFilter] --> JD[JWT Decode]
JD --> Role[Role Check]
Role --> Perm[Permission Check]
end
subgraph Chain2 ["Session Chain (Order 2) — /portal/**"]
SF[SessionAuthFilter] --> SC[Session Cookie]
SC --> MR[Member Role]
end
| Property | JWT Chain | Session Chain |
|---|---|---|
| Path | /api/v1/** |
/portal/** |
| Token type | Bearer JWT (Authorization header) | HttpSession cookie |
| Users | Admin, Staff | Members |
| CSRF | Disabled (stateless) | Enabled |
| Expiry | Access: 8h, Refresh: 30d | Session: 24h |
Roles
| Role | Description | Access |
|---|---|---|
ROLE_CLUB_ADMIN |
Club administrator | Full club management, all members, reports, distributions, staff, finance |
ROLE_STAFF |
Club staff member | Configurable subset of admin permissions |
ROLE_MEMBER |
Club member | Own quota, own distribution history, events, forum |
ROLE_PREVENTION_OFFICER |
Designated prevention officer | Member under-21 reports, prevention data |
Staff Permission Model
8 granular permissions stored as JSONB on staff_accounts:
public enum StaffPermission {
RECORD_DISTRIBUTION, // can record distributions
VIEW_MEMBER_LIST, // can view member roster
VIEW_MEMBER_QUOTA, // can view individual member quota
ADD_MEMBER, // can register new members
VIEW_STOCK, // can view batch/strain inventory
RECORD_STOCK_IN, // can add new batches
VIEW_COMPLIANCE_REPORT, // can generate/download reports
MANAGE_GROW_CALENDAR // can manage cultivation calendar entries
}
4. Data Model (57 Entities)
Entity Groups
graph TD
subgraph Core ["Core Domain"]
Member
Distribution
MonthlyQuota
Batch
Strain
StockMovement
end
subgraph Auth ["Authentication"]
User
StaffAccount
RevokedToken
InviteToken
Consent
DeviceRegistration
end
subgraph Club ["Club Management"]
Club
ClubSettings
BoardMember
end
subgraph Finance ["Finance"]
Transaction
Expense
MembershipFee
ImportSession
ImportedTransaction
PaymentMatch
Subscription
end
subgraph Communication ["Communication"]
InfoPost
Event
EventRsvp
ForumThread
ForumPost
Notification
NotificationPreference
end
subgraph Governance ["Governance"]
Assembly
AgendaItem
Vote
VoteBallot
Document
end
subgraph Grow ["Cultivation"]
GrowCycle
GrowEntry
SensorReading
PropagationSource
end
subgraph Compliance ["Compliance"]
ComplianceRecord
ComplianceDeadline
DestructionRecord
TransportRecord
PreventionActivity
Report
AuditLog
end
Key Entity Counts by Domain
| Domain | Entities | Key Tables |
|---|---|---|
| Core Operations | 6 | Member, Distribution, MonthlyQuota, Batch, Strain, StockMovement |
| Authentication | 6 | User, StaffAccount, RevokedToken, InviteToken, Consent, DeviceRegistration |
| Club Management | 3 | Club, ClubSettings, BoardMember |
| Finance | 6 | Transaction, Expense, MembershipFee, ImportSession, ImportedTransaction, PaymentMatch |
| Communication | 7 | InfoPost, Event, EventRsvp, ForumThread, ForumPost, Notification, NotificationPreference |
| Governance | 5 | Assembly, AgendaItem, Vote, VoteBallot, Document |
| Cultivation | 4 | GrowCycle, GrowEntry, SensorReading, PropagationSource |
| Compliance | 6 | ComplianceRecord, ComplianceDeadline, DestructionRecord, TransportRecord, PreventionActivity, Report |
| Audit & Billing | 4 | AuditLog, Subscription, StorageQuota, ... |
| Total | ~57 |
5. API Layer (33 Controllers)
Controller Inventory
| Group | Controllers | Endpoints |
|---|---|---|
| Auth | AuthController, ConsentController, DsgvoController | Login, refresh, register, consent management |
| Members | MemberController, PortalController | CRUD, quota, self-service |
| Operations | DistributionController, StockController, GrowCalendarController | Record distributions, manage inventory, cultivation |
| Communication | InfoBoardController, EventController, ForumController | Posts, events, threads |
| Notifications | NotificationController, NotificationPreferenceController, NotificationComposeController, DeviceRegistrationController | Push, email, in-app |
| Finance | FinanceController, BankImportController, SubscriptionController, StripeWebhookController | Treasury, bank import, billing |
| Governance | AssemblyController, BoardController, DocumentController | Assemblies, votes, documents |
| Compliance | ComplianceController, ComplianceDashboardController, ComplianceDeadlineController, ComplianceRecordsController, ReportController | Status, deadlines, records, reports |
| Admin | StaffController, ClubController, MailSettingsController, StorageController, AuditController | Staff, club config, mail, storage, audit |
| System | TestResetController | Test environment reset (non-prod only) |
6. Frontend Architecture
Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| Framework | Next.js 15 (App Router) | SSR for marketing, CSR for dashboard |
| UI Library | React 19 | Component rendering |
| Components | shadcn/ui + Radix | Accessible, composable UI primitives |
| Styling | Tailwind CSS 4 | Utility-first styling with dark mode |
| Data | @tanstack/react-query | Server state management, caching |
| Tables | TanStack Table v8 | Sortable, filterable data tables |
| Charts | Recharts | KPI dashboards, quota visualization |
| Forms | React Hook Form + Zod | Type-safe form validation |
| Auth | NextAuth v5 | Session management, JWT relay |
| i18n | next-intl | German / English localization |
| Testing | Vitest + MSW + Playwright | Unit, mock, E2E |
Route Groups
app/
├── (marketing)/ → Public: landing, pricing, login
├── (dashboard-layout)/ → Admin: 18 sections with sidebar
│ ├── dashboard/
│ ├── members/
│ ├── distributions/
│ ├── stock/
│ ├── grow/
│ ├── info-board/
│ ├── calendar/
│ ├── forum/
│ ├── finance/
│ │ └── import/
│ ├── assemblies/
│ ├── documents/
│ ├── board/
│ ├── settings/staff/
│ ├── compliance/
│ ├── reports-center/
│ ├── audit-log/
│ └── reports/
└── (portal)/ → Member: self-service with top nav
├── portal/dashboard/
├── portal/history/
├── portal/profile/
└── portal/events/
7. Database Migrations (Flyway V1–V36)
| Range | Sprint | Domain |
|---|---|---|
| V1–V5 | 1–3 | Core schema, members, distributions, stock, staff, club settings |
| V6–V10 | 6 | DSGVO consent, Stripe subscriptions, grow calendar, notifications, PWA |
| V11–V14 | 7 | Info board, events, forum |
| V15–V19 | 8 | Finance (treasury), assemblies, documents, board members |
| V20–V22 | 9 | Reports, compliance records, compliance deadlines |
| V23–V26 | 9 | Destruction records, transport records, propagation sources, prevention activities |
| V27–V29 | 9–10 | Compliance dashboard, bank import, distribution THC/CBD tracking |
| V30–V33 | 10–11 | Import sessions, payment matching, test coverage support |
| V34–V36 | 12–14 | Document integration, storage quotas, marketing/subscription tiers |
8. Integration Points
graph LR
CM[CannaManage Backend]
CM -->|"Stripe SDK"| Stripe["Stripe API\n(subscriptions, webhooks)"]
CM -->|"SMTP"| SMTP["Mail Server\n(notifications, invites)"]
CM -->|"OpenPDF"| PDF["PDF Generation\n(compliance reports)"]
CM -->|"MT940/CAMT053"| Bank["Bank Statement Files\n(uploaded by user)"]
CM -->|"Push API"| Push["Web Push\n(PWA notifications)"]
| Integration | Protocol | Direction | Purpose |
|---|---|---|---|
| Stripe | REST/SDK | Bidirectional | Subscription billing, webhooks for payment events |
| SMTP | SMTP/TLS | Outbound | Email notifications, staff invites, alerts |
| OpenPDF | Library | Internal | Generate PDF compliance reports |
| Bank Import | File upload | Inbound | MT940, CAMT053, CSV bank statement parsing |
| Web Push | Push API | Outbound | PWA push notifications to registered devices |
| Swagger UI | HTTP | Inbound | API documentation at /swagger-ui.html |
9. Deployment Architecture
graph TB
Dev["👨💻 Dev Workstation\n(macOS)"]
Gitea["🏠 Gitea\n(TrueNAS :30008)"]
Runner["⚙️ Gitea Actions Runner\n(TrueNAS Docker)"]
Dev -->|"git push"| Gitea
Gitea -->|"triggers"| Runner
Runner -->|"mvn + docker build"| Deploy
subgraph TrueNAS ["🖧 TrueNAS — Production Docker"]
Deploy["Docker Compose"]
Nginx["Nginx :443"]
App["cannamanage-app :8080"]
FE["cannamanage-frontend :3000"]
DB["PostgreSQL 16 :5432"]
Deploy --> Nginx
Deploy --> App
Deploy --> FE
Deploy --> DB
Nginx --> App
Nginx --> FE
App --> DB
end
Internet["🌍 Internet"] -->|"HTTPS"| Nginx
See Deployment Guide for full production setup details.
🌿 CannaManage
📋 Planning
🏗️ Architecture
🎨 Design
💻 Development
🌟 Product
📊 Sprint Status
| Sprint | Theme | Status |
|---|---|---|
| 1 | Domain Foundation | ✅ |
| 2 | REST API | ✅ |
| 3 | Staff & Portal | ✅ |
| 4 | Frontend MVP | ✅ |
| 5 | API Integration | ✅ |
| 6 | Production Readiness | ✅ |
| 7 | Communication | ✅ |
| 8 | Vereinsverwaltung | ✅ |
| 9 | Berichtszentrale | ✅ |
| 10 | Payment Import | ✅ |
| 11 | Test Coverage | ✅ |
| 12 | Golden Tests | ✅ |
| 13 | Prod Hardening | ✅ |
| 14 | Marketing | ✅ |
📈 Metrics
| Metric | Value |
|---|---|
| Entities | 57 |
| Controllers | 33 |
| Migrations | V1–V36 |
| Tests | 500+ |
| Coverage | 80% |