Stack
| Laag | Technologie | Versie |
|---|---|---|
| Backend | FastAPI (Python) | 0.115+ |
| Database driver | asyncpg | — |
| Database | PostgreSQL | 15+ |
| Frontend | React 18 + TypeScript + Vite | 18.x / 5.x |
| UI library | Tailwind CSS + shadcn/ui | 3.x |
| Grafieken | Recharts | 2.15.0 |
| State management | Zustand (persist) | — |
| Auth | PyJWT + JWT-claims | — |
| Reverse proxy | Traefik v3 | 3.x |
| Containers | Docker Compose v2 | — |
Architectuurdiagram
Browser (React SPA — TypeScript + Vite + Tailwind)
│ HTTPS
▼
Traefik v3 (SSL wildcard *.uw-domein.nl)
│
├──/ → Frontend container (Nginx, React bundle)
└──/api/ → Backend container (FastAPI + asyncpg)
│
▼
PostgreSQL 15
├── public
│ ├── tenants
│ ├── superadmin_users
│ └── magic_link_tokens
├── tenant_001
│ ├── users
│ ├── campaigns
│ ├── training_templates
│ └── ... (alle tenant-tabellen)
└── tenant_002
└── ...
Plugins (gemount als volume):
/plugins/
├── phishing_awareness/
│ ├── manifest.json
│ ├── backend/router.py → geladen door FastAPI bij opstart
│ └── frontend/index.tsx → geladen door React router
└── <eigen-plugin>/
Database — Schema-per-tenant
Elke tenant krijgt een eigen PostgreSQL-schema. Het public schema bevat
alleen platformbrede gegevens.
Public schema (platformbreed)
| Tabel | Inhoud |
|---|---|
tenants | Tenant-registraties (naam, subdomein, branding, module-flags) |
superadmin_users | Superadmin accounts (cross-tenant beheer) |
magic_link_tokens | Eenmalige inlog-tokens (15 min geldig) |
Tenant-schema (per organisatie)
| Categorie | Tabellen |
|---|---|
| Gebruikers | users, user_profiles, invitation_tokens, audit_log |
| Organisatie | departments, department_managers, groups, delegations |
| Campagnes | campaigns, campaign_tags, campaign_notes, campaign_reminders, campaign_group_assignments, campaign_department_assignments |
| Trainingen | training_templates, training_template_versions, training_progress, training_progress_save |
| Gamification | user_streaks, user_levels, badges, user_badges, challenges, user_challenges |
| Risico | human_risk_scores, hrs_events |
| Notificaties | notifications |
| Rapportages | report_history, scheduled_report_configs |
| Integraties | webhooks, api_keys, plugin_settings |
| Instellingen | tenant_settings |
psql direct — Alembic wordt niet gebruikt
vanwege incompatibiliteit met asyncpg en raw SQL in de initialisatiescripts.
Migratiescripts staan genummerd in migrations/ en worden in volgorde uitgevoerd.
Plugin-systeem — technisch
Bij opstart scant de backend /plugins/ en laadt elke folder met een
geldige manifest.json. De FastAPI router en het React component worden
dynamisch geregistreerd.
{
"id": "phishing_awareness",
"name": "Phishing Awareness",
"version": "1.0.0",
"description": "Basistraining over phishing-herkenning",
"author": "secctl.io",
"min_platform_version": "0.3.0",
"required_roles": ["medewerker"],
"backend_router": "backend/router.py",
"frontend_entry": "frontend/index.tsx"
}
Backend: router.py exporteert een FastAPI APIRouter.
De backend monteert deze onder /api/plugins/<plugin-id>/.
Frontend: index.tsx exporteert een React component.
De router registreert het op /plugins/<plugin-id> in de SPA.
plugin_settings) — niet in eigen tabellen per plugin.
API-structuur
De backend is een FastAPI-applicatie met versioned routers onder /api/v1/.
De volledige API-documentatie is beschikbaar via /docs (Swagger UI) en
/redoc op uw omgeving.
| Router | Prefix | Functie |
|---|---|---|
auth.py | /api/v1/auth | Login, magic link, token refresh, wachtwoord |
users.py | /api/v1/users | Gebruikersbeheer, import, sync, bulk-acties |
departments.py | /api/v1/departments | Afdelingshiërarchie, stats, managers |
campaigns.py | /api/v1/campaigns | Campagnes, toewijzingen, analytics, reminders |
builder.py | /api/v1/builder | Training Builder CRUD, versioning, publiceren |
hrs.py | /api/v1/hrs | Human Risk Score, overzicht, herberekening |
gamification.py | /api/v1/gamification | Streaks, XP-levels, badges, uitdagingen, leaderboard |
reports.py | /api/v1/reports | Rapportgeneratie, geplande rapporten, geschiedenis |
plugins.py | /api/v1/plugins | Plugin-lijst, instellingen, statistieken |
settings.py | /api/v1/settings | Webhooks, API-keys, rol-permissies, delegaties |
notifications.py | /api/v1/notifications | In-app notificaties CRUD |
profile.py | /api/v1/profile | Gebruikersprofiel GET/PUT (inclusief user_profiles) |
admin_users.py | /api/v1/admin | Cross-tenant gebruikersbeheer (superadmin only) |
mfa.py | /api/v1/mfa | Multi-factor authenticatie en passkeys |
sso.py | /api/v1/sso | SSO/SAML-configuratie per tenant |
azure_ad.py | /api/v1/azure-ad | Microsoft Azure AD SSO-login + Graph user-import |
Frontend — React 18
frontend/src/
├── views/ # pagina-componenten per module
│ ├── dashboard/
│ ├── campaigns/
│ ├── users/
│ ├── departments/
│ ├── gamification/
│ ├── reports/
│ ├── builder/
│ ├── hrs/
│ ├── plugins/
│ ├── settings/
│ ├── profile/
│ ├── admin/
│ └── auth/
├── components/
│ └── layout/ # Sidebar, Topbar (dark mode toggle, taalwissel)
├── stores/
│ ├── langStore.ts # Zustand persist — taalvoorkeur (nl/en)
│ └── themeStore.ts # Zustand persist — dark mode (system/light/dark)
├── i18n/
│ ├── translations.ts # NL + EN woordenboeken
│ └── index.ts # translate(lang, key, vars?) functie
├── hooks/
│ └── useT.ts # useT() hook — retourneert {t, lang}
└── router/
└── index.tsx # React Router v6 configuratie
i18n: Eigen vertalingssysteem zonder externe packages.
useT() haalt taalvoorkeur op uit langStore en retourneert
de vertaalfunctie t(key). Taalvoorkeur wordt gesynchroniseerd met het
gebruikersprofiel in de database.
Authenticatie
- Signed met
HS256enSECRET_KEY - Claims:
sub(user_id),tenant_id,role - Tenant-isolatie op basis van JWT-claim — geen session-opslag op de server
- Korte levensduur; verlenging via refresh flow
- Opgeslagen in
public.magic_link_tokens - Eenmalig gebruik — token wordt direct na gebruik ongeldig
- Geldigheid: 15 minuten
- Volledig los van uitnodigingstokens (
invitation_tokens)
/api/v1/azure-ad/sso/authorize → /sso/callback). Na succesvolle
login geeft de backend een eigen JWT uit. Gebruikersimport verloopt via de
Microsoft Graph API.
/api/v1/sso) en het datamodel zijn aanwezig.
De SAML-loginflow zelf staat nog op de roadmap.