3
CannaManage 03 Architecture
Patrick Plate edited this page 2026-06-19 16:43:56 +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.

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 (V1V36) Versioned schema management
Payments Stripe Java SDK SEPA, PayPal, Credit Card subscriptions
Bank Import Custom parsers MT940, CAMT053, CSV statement parsing + auto-matching
Email Spring Mail (SMTP) Notifications, invites, alerts
PDF 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 V1V36
       → 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 V1V36)

Range Sprint Domain
V1V5 13 Core schema, members, distributions, stock, staff, club settings
V6V10 6 DSGVO consent, Stripe subscriptions, grow calendar, notifications, PWA
V11V14 7 Info board, events, forum
V15V19 8 Finance (treasury), assemblies, documents, board members
V20V22 9 Reports, compliance records, compliance deadlines
V23V26 9 Destruction records, transport records, propagation sources, prevention activities
V27V29 910 Compliance dashboard, bank import, distribution THC/CBD tracking
V30V33 1011 Import sessions, payment matching, test coverage support
V34V36 1214 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.