feat: migrate clients module to patients + add docs

This commit is contained in:
colinislit
2025-11-23 10:13:00 +01:00
parent 8e3925ea09
commit 6fcb9a0e7b
277 changed files with 9131 additions and 458 deletions

View File

@@ -0,0 +1,206 @@
# Hoe werkt de Authenticatie Flow? 🔐
Een simpele uitleg van wat er gebeurt wanneer gebruikers zich aanmelden.
---
## 📧 Email Confirmatie Flow (nieuwe gebruikers)
### Stap 1: Gebruiker meldt zich aan
```
Gebruiker vult in op /login:
├─ Email: jan@example.com
└─ Wachtwoord: Geheim123!
```
### Stap 2: Supabase stuurt email
```
Supabase maakt account aan → Stuurt bevestigingsmail
De email bevat een link zoals:
https://aispeedrun.nl/auth/callback?token=xyz123&type=signup
└─────┬─────┘
Dit is de redirect URL!
```
### Stap 3: Gebruiker klikt op link in email
```
Browser gaat naar: /auth/callback?token=xyz123
De callback route doet:
1. ✅ Controleert de token
2. ✅ Activeert het account
3. ✅ Logt gebruiker in
4. → Stuurt door naar /epd/clients (omdat ze al wachtwoord hebben)
```
---
## 🔑 Wachtwoord Reset Flow
### Stap 1: Gebruiker klikt "Wachtwoord vergeten?"
```
Gaat naar: /reset-password
Vult in: jan@example.com
```
### Stap 2: Supabase stuurt reset email
```
Email bevat link:
https://aispeedrun.nl/auth/callback?token=abc789&type=recovery&next=/update-password
└─────┬─────┘ └────┬────┘
Callback route Waar naartoe daarna?
```
### Stap 3: Gebruiker klikt link
```
/auth/callback ontvangt de token
├─ Controleert token ✅
├─ Logt gebruiker tijdelijk in
└─ Redirect naar: /update-password (van de 'next' parameter)
```
### Stap 4: Nieuw wachtwoord instellen
```
Op /update-password:
├─ Gebruiker vult nieuw wachtwoord in
├─ Wachtwoord wordt opgeslagen
└─ Redirect naar /login → Gebruiker kan inloggen!
```
---
## ✉️ Magic Link Flow (oude methode)
### Stap 1: Gebruiker vraagt magic link aan
```
Vult alleen email in (geen wachtwoord)
```
### Stap 2: Email met magic link
```
Link: https://aispeedrun.nl/auth/callback?token=magic456
```
### Stap 3: Eerste keer inloggen
```
/auth/callback detecteert: "nieuwe magic link gebruiker"
└─ Redirect naar /set-password (optioneel wachtwoord instellen)
├─ Wachtwoord instellen → /epd/clients
└─ Overslaan → /epd/clients (blijf magic link gebruiken)
```
---
## 🌐 Waarom de Redirect URLs belangrijk zijn
Supabase moet weten welke URLs **veilig** zijn om naar terug te sturen.
### Zonder redirect URLs in Supabase:
```
❌ Link in email: https://aispeedrun.nl/auth/callback?token=xyz
Supabase zegt: "Deze URL ken ik niet, BLOCKED!"
Gebruiker ziet error 😞
```
### Met redirect URLs in Supabase:
```
✅ Link in email: https://aispeedrun.nl/auth/callback?token=xyz
Supabase zegt: "Deze URL staat in mijn lijst, OK!"
Gebruiker wordt ingelogd en doorgestuurd 🎉
```
---
## 🔧 De Site URL vs Redirect URLs
### Site URL (1 URL)
```
Dit is je "hoofd" URL waar Supabase denkt dat je app draait.
Supabase gebruikt dit voor:
├─ {{ .ConfirmationURL }} in emails (de basis)
└─ Default redirects
Development: http://localhost:3000
Production: https://aispeedrun.nl
```
### Redirect URLs (meerdere URLs mogelijk)
```
Dit is de "whitelist" van URLs waar Supabase naartoe MAG redirecten.
Je moet ALLE mogelijke auth callbacks toevoegen:
├─ /auth/callback → Email confirmaties, magic links
├─ /update-password → Na password reset
├─ /set-password → Nieuwe users (optioneel wachtwoord)
└─ /reset-password → Password reset pagina
Voor zowel localhost als productie!
```
---
## 🎯 Simpel Gezegd
1. **Site URL** = Waar draait je app?
- Tijdens development: `http://localhost:3000`
- Live op internet: `https://aispeedrun.nl`
2. **Redirect URLs** = Welke paginas mag Supabase bezoeken na login/reset?
- Voeg ALLE auth-gerelateerde URLs toe
- Voor zowel development als productie
3. **Email links** = Gebouwd met Site URL + token
- Als Site URL = localhost → emails gaan naar localhost ❌
- Als Site URL = aispeedrun.nl → emails gaan naar je website ✅
---
## 📝 Voorbeeld Flow in de Praktijk
```
[Gebruiker]
↓ Registreert op /login
[Jouw App]
↓ POST naar Supabase "maak account"
[Supabase]
↓ Stuurt email naar gebruiker
↓ Email link = [Site URL]/auth/callback?token=xyz
[Email Inbox]
↓ Gebruiker klikt link
[Browser]
↓ Gaat naar aispeedrun.nl/auth/callback?token=xyz
[Supabase]
↓ Checkt: staat "aispeedrun.nl/auth/callback" in Redirect URLs?
↓ JA ✅ → Verifieert token
[Jouw App - /auth/callback route]
↓ Token geldig? → Login gebruiker
↓ Nieuwe gebruiker met wachtwoord?
↓ JA → Redirect naar /epd/clients
[Gebruiker is ingelogd! 🎉]
```
---
## ❓ Veelgestelde Vragen
### Waarom krijg ik localhost links in productie emails?
→ Je Site URL staat nog op `http://localhost:3000` in Supabase. Wijzig naar `https://aispeedrun.nl`
### Waarom krijg ik "Invalid Redirect URL" errors?
→ De URL staat niet in je Redirect URLs lijst. Voeg hem toe in Supabase Dashboard.
### Kan ik zowel localhost als productie tegelijk gebruiken?
→ JA! Voeg beide toe aan Redirect URLs. Wissel alleen de Site URL afhankelijk van waar je test.
### Moet ik www. ook toevoegen?
→ Als je site bereikbaar is via `www.aispeedrun.nl`, voeg dan ook die URLs toe.
---
**Hopelijk is het nu duidelijk! 🚀**

View File

@@ -0,0 +1,183 @@
# Auth Hook Setup Guide
## Overzicht
Deze hook detecteert duplicate emails VOOR een user wordt aangemaakt,
waardoor gebruikers direct feedback krijgen als hun email al geregistreerd is.
**Voordelen:**
- ✅ Server-side validatie (kan niet omzeild worden)
- ✅ Duidelijke foutmeldingen voor gebruikers
- ✅ Betrouwbaar (werkt ongeacht password)
- ✅ Case-insensitive email matching
- ✅ Email normalisatie (lowercase + trim)
## Setup (Eerste Keer)
### Stap 1: Deploy Migration
**Optie A: Via Supabase Dashboard (Aanbevolen)**
1. Ga naar: [Supabase Dashboard → SQL Editor](https://supabase.com/dashboard/project/dqugbrpwtisgyxscpefg/sql)
2. Open het migration bestand: `supabase/migrations/20251119094908_auth_hook_duplicate_email.sql`
3. Kopieer de volledige inhoud
4. Plak in de SQL Editor
5. Klik "RUN" om de functie aan te maken
**Optie B: Via Supabase CLI (Als geconfigureerd)**
```bash
npx supabase db push
```
### Stap 2: Verificatie & Instructies
Run het setup script om te verifiëren dat de functie bestaat:
```bash
pnpm run setup:auth-hook
```
Dit script:
- ✅ Checkt of de functie bestaat
- 📋 Geeft instructies voor Dashboard configuratie
- 🔗 Biedt directe links naar relevante Dashboard pagina's
### Stap 3: Configureer Hook Link
**⚠️ Deze stap moet handmatig via Dashboard** (Supabase ondersteunt dit nog niet via API):
1. Ga naar: [Supabase Dashboard → Auth → Hooks](https://supabase.com/dashboard/project/dqugbrpwtisgyxscpefg/auth/hooks)
2. Klik **"Add a new hook"** of **"Enable Hooks"**
3. Vul in:
- **Hook Type:** "Send a hook on before a user is created" (`before-user-created`)
- **Select hook:** "Postgres Function"
- **Schema:** `public`
- **Function Name:** `hook_check_duplicate_email`
4. Klik **"Create hook"** of **"Save"**
### Stap 4: Test
Test de hook door:
1. Ga naar je signup pagina: `http://localhost:3000/login`
2. Probeer te registreren met een **bestaand** emailadres (bijv. `demo@mini-ecd.demo`)
3. Je zou een error moeten zien: _"Dit emailadres is al geregistreerd. Probeer in te loggen of gebruik 'Wachtwoord vergeten?'."_
4. Probeer te registreren met een **nieuw** emailadres
5. Dit zou normaal moeten werken (verificatie email verzonden)
## Test Cases
| Test Case | Scenario | Expected Result |
|-----------|----------|-----------------|
| TC1 | Signup met nieuw email | ✅ Account aangemaakt, email verzonden |
| TC2 | Signup met bestaand email | ❌ Error: "Dit emailadres is al geregistreerd..." |
| TC3 | Signup met bestaand email (case variant: `Email@Example.com`) | ❌ Error (case-insensitive match) |
| TC4 | Signup met lege/NULL email | ❌ Error: "Email adres is verplicht." |
| TC5 | Hook disabled → signup met bestaand email | ⚠️ Oude gedrag (geen error, maar ook geen email) |
## Herhaalbaarheid
-**Functie code** staat in migrations (version controlled)
- ⚠️ **Hook link** moet per omgeving handmatig worden geconfigureerd
-**Documentatie** staat in Git
-**Setup script** voor validatie en instructies
## Technische Details
### Wat Doet de Hook?
De `hook_check_duplicate_email` functie:
1. Ontvangt signup event van Supabase Auth
2. Haalt email adres uit event payload
3. Valideert email (niet NULL/empty)
4. Normaliseert email (lowercase + trim)
5. Checkt of email al bestaat in `auth.users` table (case-insensitive)
6. Als email bestaat → return error object
7. Als email nieuw is → return empty object (allow signup)
### Security
- **Security Definer:** Functie draait met elevated permissions
- **Search Path:** Expliciet ingesteld op `public, auth` voor veilige schema access
- **Permissions:** Alleen `supabase_auth_admin` kan de functie uitvoeren
- **Email Enumeration Protection:** Werkt samen met bestaande email confirmation
### Performance
- ⚡ Direct database check (geen extra HTTP calls)
- ⚡ Indexed lookup op `auth.users.email`
- ⚡ Minimale overhead (< 10ms typisch)
## Toekomstige Verbeteringen
Zodra Supabase Management API Auth Hooks ondersteunt, kunnen we:
- [ ] Hook link volledig automatiseren
- [ ] Setup script uitbreiden met API calls
- [ ] CI/CD pipeline voor hook configuratie
- [ ] Automated tests voor hook functionaliteit
## Flexibiliteit
De functie is geschreven in standaard PostgreSQL, waardoor:
- ✅ Werkt met elke auth provider die Postgres functies ondersteunt
- ✅ Makkelijk te migreren naar andere auth systemen
- ✅ Geen vendor lock-in voor de logica zelf
## Troubleshooting
### "Function does not exist" error
**Probleem:** De hook functie is niet aangemaakt in de database.
**Oplossing:**
1. Controleer of migration is uitgevoerd via Dashboard of CLI
2. Run `pnpm run setup:auth-hook` voor verificatie
3. Check Supabase logs voor SQL errors
### Hook lijkt niet te werken
**Probleem:** Signup met bestaand email geeft geen error.
**Mogelijke oorzaken:**
1. Hook link niet geconfigureerd in Dashboard → Ga naar Auth → Hooks
2. Hook is disabled → Check hook status in Dashboard
3. Email confirmation staat uit → Check Auth → Email Templates
**Verificatie:**
```sql
-- Check of functie bestaat
SELECT routine_name
FROM information_schema.routines
WHERE routine_schema = 'public'
AND routine_name = 'hook_check_duplicate_email';
-- Test functie handmatig
SELECT hook_check_duplicate_email('{"user": {"email": "demo@mini-ecd.demo"}}'::jsonb);
```
### Wrong error message
**Probleem:** Error message klopt niet of is in het Engels.
**Oplossing:**
1. Check of je de laatste versie van de migration hebt gebruikt
2. Update functie via SQL Editor met correcte error messages
3. Rebuild client error handling (`app/login/page.tsx`)
## Related Documentation
- [Supabase Auth Hooks Documentation](https://supabase.com/docs/guides/auth/auth-hooks)
- [Bouwplan: Auth Hook Implementation](./bouwplan-auth-hook-duplicate-email-v1.0.md)
- [Main README](../README.md)
## Support
Voor vragen of problemen:
1. Check deze documentatie
2. Check Supabase logs in Dashboard
3. Run `pnpm run setup:auth-hook` voor diagnostics
4. Review `supabase/migrations/20251119094908_auth_hook_duplicate_email.sql`

416
docs/design/AUTH_SETUP.md Normal file
View File

@@ -0,0 +1,416 @@
# 🔐 Authentication Setup Guide
**Project:** AI Speedrun - Mini-ECD Prototype
**Epic:** E2 - Database & Auth
**Story:** E2.S3 - Demo auth flow
**Last Updated:** 2024-11-15
---
## Overview
This document describes the authentication implementation for the EPD prototype, including magic link login and demo user accounts.
---
## Authentication Methods
### 1. Magic Link (Primary Method)
Users can sign in using email-only authentication:
1. User enters email on `/login`
2. Supabase sends magic link to email
3. User clicks link → auto-logged in
4. **New users:** Account is automatically created on first magic link request
**Benefits:**
- No password to remember
- More secure than traditional passwords
- Better UX for demo environment
- Auto-creates accounts (no separate signup flow needed)
---
### 2. Demo Accounts (For Presentations)
Pre-configured demo accounts for public demos and presentations:
| Email | Password | Access Level | Purpose |
|-------|----------|--------------|---------|
| demo@mini-ecd.demo | Demo2024! | interactive | Main demo account - full CRUD |
| readonly@mini-ecd.demo | Demo2024! | read_only | View-only for public demos |
| presenter@mini-ecd.demo | Demo2024! | presenter | Live presentations |
**Access Levels:**
- `read_only`: Can view all data, cannot create/edit/delete
- `interactive`: Full CRUD access to all features
- `presenter`: Full access + special presenter features (future)
---
## Setup Instructions
### 1. Environment Variables
Ensure these are set in your `.env.local`:
```bash
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://dqugbrpwtisgyxscpefg.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# Service role key (for admin operations)
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
```
### 2. Create Demo Users
Run the seed script to create demo user accounts:
```bash
# Make sure you have tsx installed
pnpm add -D tsx
# Run the seed script
tsx scripts/seed-demo-users.ts
```
**Expected output:**
```
🌱 Starting demo user seed...
Creating user: demo@mini-ecd.demo...
✅ Created auth user: xxx-xxx-xxx
✅ Created demo_users entry
✨ demo@mini-ecd.demo ready!
Creating user: readonly@mini-ecd.demo...
✅ Created auth user: xxx-xxx-xxx
✅ Created demo_users entry
✨ readonly@mini-ecd.demo ready!
Creating user: presenter@mini-ecd.demo...
✅ Created auth user: xxx-xxx-xxx
✅ Created demo_users entry
✨ presenter@mini-ecd.demo ready!
✅ Demo user seed complete!
```
### 3. Configure Supabase Auth Settings
Go to Supabase Dashboard → Authentication → Settings:
#### Email Templates
Customize the magic link email template:
**Subject:** "Login to Mini-ECD"
**Body:**
```html
<h2>Je magic link is klaar!</h2>
<p>Klik op de knop hieronder om in te loggen bij Mini-ECD:</p>
<p><a href="{{ .ConfirmationURL }}">Login naar EPD</a></p>
<p>Of kopieer deze link naar je browser:</p>
<p>{{ .ConfirmationURL }}</p>
<p><small>Deze link is 1 uur geldig.</small></p>
```
#### Redirect URLs
Add these redirect URLs under "Redirect URLs":
```
http://localhost:3000/auth/callback
https://yourdomain.com/auth/callback
```
#### Email Auth Settings
- ✅ Enable Email provider
- ✅ Confirm email: OFF (for demo convenience)
- ✅ Secure email change: ON
- ⏱️ Rate limits: Default (4 emails per hour)
---
## File Structure
```
app/
login/
page.tsx # Login UI (magic link + demo login)
auth/
callback/
route.ts # Handles magic link callback
logout/
route.ts # Logout endpoint
lib/
auth/
client.ts # Client-side auth helpers
server.ts # Server-side auth helpers
database.types.ts # Generated Supabase types
middleware.ts # Route protection
scripts/
seed-demo-users.ts # Demo user creation script
```
---
## Usage Examples
### Client-Side (React Components)
```typescript
import {
loginWithMagicLink,
loginWithPassword,
logout,
getUser,
isDemoUser
} from '@/lib/auth/client'
// Magic link login
async function handleMagicLink(email: string) {
const result = await loginWithMagicLink(email)
console.log(result.message) // "Check je email voor de magic link!"
}
// Demo account login
async function handleDemoLogin() {
await loginWithPassword('demo@mini-ecd.demo', 'Demo2024!')
router.push('/clients')
}
// Check current user
const user = await getUser()
const isDemo = await isDemoUser()
// Logout
await logout() // Redirects to /login
```
### Server-Side (API Routes, Server Components)
```typescript
import {
requireAuth,
getUser,
canWrite,
getDemoUserInfo
} from '@/lib/auth/server'
// Require authentication in API route
export async function GET() {
const session = await requireAuth() // Throws if not authenticated
// ... handle request
}
// Check write permissions
export async function POST() {
const hasWriteAccess = await canWrite()
if (!hasWriteAccess) {
return NextResponse.json(
{ error: 'Read-only demo account cannot create data' },
{ status: 403 }
)
}
// ... create resource
}
// Get demo user info
const demoInfo = await getDemoUserInfo()
if (demoInfo) {
console.log(`Access level: ${demoInfo.access_level}`)
console.log(`Usage count: ${demoInfo.usage_count}`)
}
```
---
## Route Protection
Routes are protected via `middleware.ts`:
### Public Routes (No Auth Required)
- `/` - Landing page
- `/login` - Login page
- `/epd` - EPD demo info
- `/contact` - Contact form
- `/auth/callback` - Auth callback
### Protected Routes (Auth Required)
- `/clients` - Client list
- `/clients/*` - Client details, intake, etc.
- Any other route not in public list
**Behavior:**
- ✅ Unauthenticated → Redirect to `/login?redirect=/original-path`
- ✅ Authenticated on `/login` → Redirect to `/clients`
- ✅ Session auto-refreshed in middleware
---
## Security Features
### ✅ Implemented
1. **RLS Policies**: All database queries filtered by `auth.uid()`
2. **Session Management**: Auto-refresh tokens via middleware
3. **Secure Cookies**: HTTP-only, secure flags set
4. **CSRF Protection**: Built-in Next.js CSRF protection
5. **Rate Limiting**: Supabase default (4 emails/hour)
6. **Demo User Tracking**: Usage count and last login tracked
### 🔒 Production Enhancements
For production deployment:
1. **Email Confirmation**: Enable email confirmation
2. **Password Requirements**: Enforce strong passwords
3. **MFA**: Add multi-factor authentication
4. **Session Timeout**: Implement auto-logout after inactivity
5. **IP Whitelisting**: Restrict demo accounts to specific IPs
6. **Audit Logging**: Enhanced tracking of all auth events
---
## Demo User Management
### Checking Demo Status
```typescript
// Check if user is demo user
const isDemo = await isDemoUser()
// Get access level
const accessLevel = await getDemoAccessLevel()
// Returns: 'read_only' | 'interactive' | 'presenter' | null
```
### Restricting Actions
```typescript
// In API route
const demoInfo = await getDemoUserInfo()
if (demoInfo?.access_level === 'read_only') {
return NextResponse.json(
{ error: 'This demo account is read-only' },
{ status: 403 }
)
}
```
### Resetting Demo Accounts
To reset a demo account (clear data, reset usage):
```sql
-- Reset usage count
UPDATE demo_users
SET usage_count = 0, last_login_at = NULL
WHERE access_level = 'interactive';
-- Or via Supabase Dashboard: Authentication → Users → Delete user data
```
---
## Troubleshooting
### Issue: Magic link not arriving
**Causes:**
- Email in spam folder
- Rate limit exceeded (4 emails/hour)
- Email provider blocking Supabase emails
**Solutions:**
1. Check spam folder
2. Wait 1 hour and try again
3. Use demo account instead
4. Configure custom SMTP in Supabase
### Issue: "Invalid login credentials"
**Causes:**
- Wrong email/password for demo account
- Demo user not created yet
**Solutions:**
1. Check credentials match exactly (case-sensitive)
2. Run seed script: `tsx scripts/seed-demo-users.ts`
3. Verify in Supabase Dashboard → Authentication → Users
### Issue: Redirect loop on /login
**Causes:**
- Middleware configuration error
- Session cookie issues
**Solutions:**
1. Clear browser cookies
2. Check middleware.ts public routes config
3. Verify `NEXT_PUBLIC_SUPABASE_URL` is correct
### Issue: "Row violates RLS policy" errors
**Causes:**
- User not properly authenticated
- Session expired
- RLS policies misconfigured
**Solutions:**
1. Logout and login again
2. Check `auth.uid()` returns valid UUID
3. Verify RLS policies allow user access
---
## Testing Checklist
### Magic Link Flow
- [ ] Can enter email on /login
- [ ] Magic link email received
- [ ] Clicking link redirects to /clients
- [ ] Session persists after page refresh
- [ ] New users auto-created on first login
### Demo Account Flow
- [ ] Can login with demo@mini-ecd.demo
- [ ] Can login with readonly@mini-ecd.demo
- [ ] Interactive account can create/edit data
- [ ] Read-only account blocked from editing
- [ ] Demo usage tracked in demo_users table
### Route Protection
- [ ] /clients redirects to /login when not authenticated
- [ ] /login redirects to /clients when authenticated
- [ ] Public routes accessible without auth
- [ ] Session auto-refreshes
### Logout
- [ ] Logout clears session
- [ ] Redirects to /login
- [ ] Cannot access protected routes after logout
---
## References
- [Supabase Auth Documentation](https://supabase.com/docs/guides/auth)
- [Next.js Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware)
- Build Plan: `docs/specs/bouwplan-ai-speedrun-marketing-first-v1.1.md` Epic 2
- Technical Design: `docs/specs/to-mini-ecd-v1_2.md` § 5.7
---
**Status:** ✅ Implemented and Ready for Testing
**Next Steps:** E2.S4 - Seed data script (clients + dossiers)

304
docs/design/RLS_SECURITY.md Normal file
View File

@@ -0,0 +1,304 @@
# 🔒 Row Level Security (RLS) Documentation
**Project:** AI Speedrun - Mini-ECD Prototype
**Epic:** E2 - Database & Auth
**Story:** E2.S2 - RLS policies implementeren
**Last Updated:** 2024-11-15
---
## Overview
This document describes the Row Level Security (RLS) implementation for the EPD core database tables. RLS is PostgreSQL's security feature that restricts which rows users can access in database queries.
### Security Model
- **Authentication Required:** All data access requires a valid Supabase authentication session
- **Authorization:** Checked via `auth.uid()` function which returns the authenticated user's UUID
- **MVP Level:** All authenticated users can access all data (suitable for demo/single-org)
- **Production Path:** Ready to extend with `org_id` filtering for multi-tenancy
---
## Tables & Policies
### 1. Clients Table
**Purpose:** Basic client information
**RLS Enabled:** ✅ Yes
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view clients | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create clients | INSERT | `auth.uid() IS NOT NULL` |
| Authenticated users can update clients | UPDATE | `auth.uid() IS NOT NULL` |
| Authenticated users can delete clients | DELETE | `auth.uid() IS NOT NULL` |
**Production Enhancement:**
```sql
-- Add organization filtering
CREATE POLICY "Users can view own org clients"
ON clients FOR SELECT
USING (
auth.uid() IS NOT NULL AND
org_id = (SELECT org_id FROM users WHERE id = auth.uid())
);
```
---
### 2. Intake Notes Table
**Purpose:** TipTap/ProseMirror JSON content storage
**RLS Enabled:** ✅ Yes
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view intake notes | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create intake notes | INSERT | `auth.uid() IS NOT NULL` |
| Authenticated users can update intake notes | UPDATE | `auth.uid() IS NOT NULL` |
| Authenticated users can delete intake notes | DELETE | `auth.uid() IS NOT NULL` |
**Security Features:**
- Full-text search index with Dutch language support
- Cascade delete when parent client is deleted
- Automatic `updated_at` trigger
---
### 3. Problem Profiles Table
**Purpose:** DSM-light categorization with severity scoring
**RLS Enabled:** ✅ Yes
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view problem profiles | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create problem profiles | INSERT | `auth.uid() IS NOT NULL` |
| Authenticated users can update problem profiles | UPDATE | `auth.uid() IS NOT NULL` |
| Authenticated users can delete problem profiles | DELETE | `auth.uid() IS NOT NULL` |
**Data Constraints:**
- Category: Must be one of 6 DSM-light categories
- Severity: Must be 'laag', 'middel', or 'hoog'
- Cascade delete with parent client
- SET NULL on source note deletion
---
### 4. Treatment Plans Table
**Purpose:** Treatment plans with JSONB structure and versioning
**RLS Enabled:** ✅ Yes
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view treatment plans | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create treatment plans | INSERT | `auth.uid() IS NOT NULL` |
| Authenticated users can update treatment plans | UPDATE | `auth.uid() IS NOT NULL` |
| Authenticated users can delete treatment plans | DELETE | `auth.uid() IS NOT NULL` |
**Versioning:**
- Each client can have multiple versions (v1, v2, etc.)
- Status: 'concept' (editable) or 'gepubliceerd' (locked)
- UNIQUE constraint on (client_id, version)
---
### 5. AI Events Table
**Purpose:** Telemetry and debugging for AI API calls
**RLS Enabled:** ✅ Yes
**Special:** Append-only (no UPDATE/DELETE for regular users)
#### Policies:
| Policy Name | Operation | Rule |
|------------|-----------|------|
| Authenticated users can view AI events | SELECT | `auth.uid() IS NOT NULL` |
| Authenticated users can create AI events | INSERT | `auth.uid() IS NOT NULL` |
| ~~UPDATE~~ | ❌ | Not allowed (audit trail) |
| ~~DELETE~~ | ❌ | Not allowed (audit trail) |
**Immutability:**
- Regular users cannot modify or delete AI events
- Ensures audit trail integrity
- Service role can bypass RLS for admin cleanup
---
## Testing RLS
### Test 1: Verify RLS is Enabled
```sql
SELECT tablename, rowsecurity as rls_enabled
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
```
**Expected Result:**
All 5 tables should show `rls_enabled: true`
### Test 2: Check Policy Count
```sql
SELECT
tablename,
COUNT(*) as policy_count,
STRING_AGG(cmd, ', ' ORDER BY cmd) as commands
FROM pg_policies
WHERE schemaname = 'public'
GROUP BY tablename;
```
**Expected Result:**
- `ai_events`: 2 policies (INSERT, SELECT)
- Other tables: 4 policies each (DELETE, INSERT, SELECT, UPDATE)
### Test 3: Verify Authentication Check
```sql
SELECT tablename, policyname, cmd, qual
FROM pg_policies
WHERE schemaname = 'public'
AND qual NOT LIKE '%auth.uid()%';
```
**Expected Result:**
Empty (all policies use `auth.uid()` checks)
---
## TypeScript Integration
TypeScript types are auto-generated and available at `lib/database.types.ts`:
```typescript
import type { Database } from '@/lib/database.types'
// Usage with Supabase client
const supabase = createClient<Database>(url, key)
// Type-safe queries
const { data: clients } = await supabase
.from('clients')
.select('*')
// Insert with type checking
const { data: newClient } = await supabase
.from('clients')
.insert({
first_name: 'John',
last_name: 'Doe',
birth_date: '1990-01-01'
})
```
---
## Security Best Practices
### ✅ Current Implementation
1. **Secure by Default:** RLS enabled on all tables
2. **Authentication Required:** All policies check `auth.uid() IS NOT NULL`
3. **Separation of Concerns:** Separate policies for each operation (SELECT, INSERT, UPDATE, DELETE)
4. **Audit Trail:** AI events are append-only
5. **Foreign Key Constraints:** Automatic cleanup with CASCADE/SET NULL
6. **Type Safety:** Generated TypeScript types prevent runtime errors
### 🔄 Production Enhancements
When moving to production with multiple organizations:
1. **Add Organization Column:**
```sql
ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
```
2. **Update Policies with Org Filtering:**
```sql
CREATE POLICY "Users can view own org data"
ON clients FOR SELECT
USING (
auth.uid() IS NOT NULL AND
org_id = (SELECT org_id FROM users WHERE id = auth.uid())
);
```
3. **Add Role-Based Access:**
```sql
CREATE POLICY "Admins can view all"
ON clients FOR SELECT
USING (
auth.uid() IS NOT NULL AND
EXISTS (
SELECT 1 FROM users
WHERE id = auth.uid() AND role IN ('admin', 'superadmin')
)
);
```
4. **Implement Row-Level Ownership:**
```sql
CREATE POLICY "Users can update own records"
ON intake_notes FOR UPDATE
USING (author = auth.uid());
```
---
## Troubleshooting
### Issue: "new row violates row-level security policy"
**Cause:** Trying to insert/update data that doesn't satisfy RLS WITH CHECK
**Solution:** Ensure user is authenticated and data meets policy requirements
### Issue: No data returned despite existing rows
**Cause:** User not authenticated or RLS USING clause filters out all rows
**Solution:** Verify `auth.uid()` returns a valid UUID
### Issue: Service role queries still restricted
**Cause:** Using anon key instead of service role key
**Solution:** Use `SUPABASE_SERVICE_ROLE_KEY` for admin operations
```typescript
// Service role bypasses RLS
const supabase = createClient(url, serviceRoleKey)
```
---
## Migration History
| Migration | Date | Changes |
|-----------|------|---------|
| `20241115000002_create_epd_core_tables.sql` | 2024-11-15 | Initial RLS policies (demo-level) |
| `20241115000003_enhance_rls_policies.sql` | 2024-11-15 | Granular policies per operation + ai_events immutability |
---
## References
- [Supabase RLS Documentation](https://supabase.com/docs/guides/auth/row-level-security)
- [PostgreSQL RLS Documentation](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
- Technical Design: `docs/specs/to-mini-ecd-v1_2.md` § 2.4
- Build Plan: `docs/specs/bouwplan-ai-speedrun-marketing-first-v1.1.md` Epic 2
---
**Status:** ✅ Implemented and Tested
**Next Steps:** E2.S3 - Demo auth flow

View File

@@ -0,0 +1,494 @@
# Datamodel Mini-ECD: FHIR-compliant GGZ Dossier
**Versie:** 1.1
**Datum:** 21 november 2024
**Status:** In ontwikkeling
---
## Overzicht
Het Mini-ECD gebruikt een datamodel gebaseerd op **FHIR (Fast Healthcare Interoperability Resources)**, de internationale standaard voor uitwisseling van zorggegevens. Dit maakt toekomstige integratie met MedMIJ (patiëntportalen) en Koppeltaal (eHealth apps) mogelijk zonder grote aanpassingen.
Het datamodel bestaat uit **13 kernonderdelen** die samen het complete GGZ-traject ondersteunen: van aanmelding tot behandelplan, inclusief doelen, toestemmingen en belangrijke waarschuwingen.
---
## De 13 bouwstenen van het dossier
### 1. **Behandelaren** (`practitioners`)
**Wat is het?**
Alle zorgprofessionals die in het systeem werken: psychologen, psychiaters, gz-psychologen, verpleegkundigen, etc.
**Belangrijkste gegevens:**
- BIG-nummer (indien geregistreerd)
- AGB-code
- Naam en voorletters
- Kwalificaties (bijv. "GZ-psycholoog", "Psychotherapeut")
- Contactgegevens
**Waarom FHIR?**
In FHIR heet dit een **Practitioner** resource. Dit maakt het mogelijk om behandelaren later uit te wisselen met andere systemen (bijvoorbeeld voor verwijzingen).
---
### 2. **Instellingen** (`organizations`)
**Wat is het?**
De GGZ-organisaties zelf: jouw instelling, maar ook externe organisaties waarmee je samenwerkt.
**Belangrijkste gegevens:**
- AGB-code instelling
- KVK-nummer
- Naam en eventuele nevenvestigingen
- Contactgegevens en adres
**Waarom FHIR?**
In FHIR heet dit een **Organization** resource. Nodig voor facturatie, verwijzingen en juridische verantwoordelijkheid.
---
### 3. **Cliënten** (`patients`)
**Wat is het?**
De patiënten/cliënten die behandeling krijgen.
**Belangrijkste gegevens:**
- BSN (verplicht)
- Naam, geboortedatum, geslacht
- Adres en contactgegevens
- Verzekeringsgegevens
- Huisarts (naam + AGB-code)
- Noodcontactpersoon
**Waarom FHIR?**
In FHIR heet dit een **Patient** resource. Dit is de basis voor alle andere gegevens in het dossier. Het correspondeert met de Nederlandse **ZIB Patient** (ZorgInformatieBouwsteen).
**Privacy:**
BSN wordt versleuteld opgeslagen en is alleen toegankelijk voor geautoriseerde behandelaren.
---
### 4. **Contactmomenten** (`encounters`)
**Wat is het?**
Elk contact tussen cliënt en behandelaar: intakegesprek, behandelsessie, telefonisch consult, etc.
**Belangrijkste gegevens:**
- Type contact (intake, diagnostiek, behandeling, crisis)
- Status (gepland, bezig, afgerond)
- Wanneer (start- en eindtijd)
- Wie (behandelaar + cliënt)
- Waar (polikliniek, online, kliniek)
- Waarom (aanmeldingsreden, klachten)
**Waarom FHIR?**
In FHIR heet dit een **Encounter** resource. Dit is cruciaal omdat alle andere gegevens (diagnoses, observaties, behandelplannen) gekoppeld worden aan een specifiek contactmoment. Hierdoor kun je later zien: "Deze diagnose is gesteld tijdens de intake van 15 maart 2024".
Dit correspondeert met de Nederlandse **ZIB Contact**.
---
### 5. **Diagnoses** (`conditions`)
**Wat is het?**
De vastgestelde diagnoses volgens DSM-5 of ICD-10. Dit kunnen zowel definitieve diagnoses zijn als voorlopige diagnoses.
**Belangrijkste gegevens:**
- DSM-5 code (bijv. "F32.2")
- Omschrijving (bijv. "Depressieve episode, ernstig")
- Status (actief, in remissie, opgelost)
- Ernst (mild, matig, ernstig)
- Zekerheid (voorlopig, bevestigd, uitgesloten)
- Wanneer ontstaan / wanneer opgelost
- Wie stelde de diagnose vast
- Bij welk contactmoment
**Waarom FHIR?**
In FHIR heet dit een **Condition** resource. Dit onderscheidt tussen "encounter diagnosis" (gesteld tijdens een specifiek contact) en "problem list item" (langlopend probleem op de problemlijst).
Dit correspondeert met de Nederlandse **ZIB Problem**.
**Voorbeeld:**
Een cliënt meldt zich aan met depressieve klachten. Na de intake wordt voorlopig "F32.2 - Depressieve episode, ernstig" vastgesteld. Na behandeling verandert de status naar "in remissie".
---
### 6. **Observaties & Metingen** (`observations`)
**Wat is het?**
Alle metingen, scores, risico-inschattingen en observaties tijdens de behandeling.
**Belangrijkste gegevens:**
- Wat werd geobserveerd (bijv. "Suïcidaliteit", "PHQ-9 score", "Bloeddruk")
- Uitkomst (bijv. "Hoog risico", "Score: 18 punten", "120/80")
- Interpretatie (normaal, afwijkend hoog, afwijkend laag)
- Wanneer gemeten
- Door wie
- Bij welk contactmoment
**Categorieën:**
- **ROM-metingen**: PHQ-9, GAD-7, OQ-45, etc.
- **Risico-inschattingen**: Suïcidaliteit, agressie, verwaarlozing
- **Middelengebruik**: Alcohol, drugs, medicatie
- **Vitale functies**: Bloeddruk, hartslag (indien relevant)
- **Sociale anamnese**: Werk, relatie, financiën
**Waarom FHIR?**
In FHIR heet dit een **Observation** resource. Dit is een zeer flexibele resource die allerlei soorten metingen kan bevatten. Door standaard codes te gebruiken (SNOMED, LOINC) kunnen deze later gedeeld worden met andere systemen.
Dit correspondeert met de Nederlandse **ZIB Alert** en **ZIB LaboratoryTestResult**.
**Voorbeeld:**
- ROM-vragenlijst PHQ-9 ingevuld: score 18 (matig-ernstige depressie)
- Risico-inschatting: "Suïcidale gedachten aanwezig, geen concrete plannen" → interpretatie: matig risico
---
### 7. **Medicatie** (`medication_statements`)
**Wat is het?**
De medicatie die de cliënt gebruikt of heeft gebruikt. Dit kan voorgeschreven zijn door de psychiater, maar ook medicatie van de huisarts.
**Belangrijkste gegevens:**
- Medicijnnaam (bijv. "Sertraline 50mg tablet")
- ATC-code (internationale medicijncode)
- Status (actief, gestopt, gepland)
- Dosering (bijv. "1 tablet 's ochtends")
- Toedieningsweg (oraal, intraveneus, etc.)
- Startdatum / stopdatum
- Reden van gebruik (bijv. "Depressie")
**Waarom FHIR?**
In FHIR heet dit een **MedicationStatement** resource. Dit registreert wat de patiënt daadwerkelijk gebruikt (niet wat voorgeschreven is - dat zou een MedicationRequest zijn).
Dit correspondeert met de Nederlandse **ZIB MedicationUse** en is onderdeel van het **MedicatieProces 9.0**.
**Let op:**
Voor volledige medicatiegeschiedenis moet later gekoppeld worden met het Landelijk Schakelpunt (LSP) of andere medicatieservices.
---
### 8. **Behandelplannen** (`care_plans`)
**Wat is het?**
Het overzicht van de geplande behandeling: wat gaan we doen, waarom, en met welk doel?
**Belangrijkste gegevens:**
- Titel (bijv. "Behandelplan depressie")
- Beschrijving van de aanpak
- Status (concept, actief, afgerond, gestopt)
- Looptijd (startdatum - einddatum)
- Behandeldoelen (bijv. "PHQ-9 score < 10", "Herstel dagelijks functioneren")
- Welke diagnoses worden behandeld
- Wie is de regiebehandelaar
- Welk zorgteam is betrokken
**Waarom FHIR?**
In FHIR heet dit een **CarePlan** resource. Dit is de container voor alle behandelactiviteiten en koppelt diagnoses aan interventies.
Dit correspondeert met de Nederlandse **ZIB TreatmentDirective**.
**Koppeltaal-integratie:**
Dit is ook de resource die Koppeltaal gebruikt om eHealth-apps te koppelen aan de behandeling. Bijvoorbeeld: "Opdracht: 3x per week mindfulness oefening via app X".
---
### 9. **Behandelactiviteiten** (`care_plan_activities`)
**Wat is het?**
De concrete activiteiten binnen een behandelplan: gesprekken, medicatie, huiswerkopdrachten, ROM-metingen, etc.
**Belangrijkste gegevens:**
- Omschrijving (bijv. "Individuele CGT sessies", "ROM-meting PHQ-9")
- Status (nog niet gestart, gepland, bezig, afgerond)
- Planning (bijv. "1x per week, 12 sessies")
- Uitvoerende behandelaar
- Locatie (polikliniek, online, kliniek)
- Voortgang (vrije tekst updates)
**Waarom FHIR?**
In FHIR heet dit **CarePlan.activity**. Dit is onderdeel van de CarePlan resource en beschrijft de "wat en wanneer" van de behandeling.
**Voorbeeld activiteiten:**
- Individuele CGT: 1x/week, 12 sessies
- Medicatie: Sertraline 50mg dagelijks
- ROM-meting: Elke 4 weken PHQ-9 invullen
- Huiswerk: Dagboek bijhouden
---
### 10. **Toestemmingen & Wilsverklaringen** (`consents`)
**Wat is het?**
Alle toestemmingen van de cliënt: voor behandeling, voor gegevensuitwisseling (AVG), wilsverklaringen (niet-reanimeren, euthanasie-verklaring, etc.).
**Belangrijkste gegevens:**
- Type toestemming (behandeling, privacy/AVG, wilsverklaring, onderzoek)
- Status (actief, ingetrokken, afgewezen)
- Categorie (niet-reanimeren, advance directive, noodgevallen-only)
- Datum en wie gaf toestemming
- Geldigheid (startdatum - einddatum)
- Wat mag wel/niet (toegang, delen, correctie)
- Met wie mag gedeeld worden (specifieke behandelaren, organisaties)
- Documenten (ondertekende verklaring als PDF)
**Waarom FHIR?**
In FHIR heet dit een **Consent** resource. Dit correspondeert met de Nederlandse **ZIB AdvanceDirective**.
**AVG-compliance:**
Dit is cruciaal voor AVG-naleving. Hiermee registreer je:
- Toestemming voor behandeling (informed consent)
- Toestemming voor delen met huisarts/andere zorgverleners
- Intrekking van toestemming
- Wilsverklaringen die juridisch bindend zijn
**Voorbeelden:**
- "Toestemming behandeling depressie" (informed consent)
- "Geen toestemming delen met huisarts" (privacy)
- "Niet-reanimeren verklaring" (wilsverklaring)
- "Toestemming opname behandelgegevens in landelijke uitwisseling" (MedMIJ)
---
### 11. **Waarschuwingen & Alerts** (`flags`)
**Wat is het?**
Belangrijke waarschuwingen die behandelaren **direct** moeten zien bij het openen van een dossier. Denk aan veiligheidsrisico's, allergieën, of gedragswaarschuwingen.
**Belangrijkste gegevens:**
- Type waarschuwing (veiligheid, klinisch, gedrag, infectie, allergie)
- Alert inhoud (bijv. "Suïciderisico", "Agressie naar hulpverleners")
- Prioriteit (hoog, middel, laag)
- Status (actief, inactief)
- Geldigheid (startdatum - einddatum)
- Wie maakte de alert
- Gerelateerde diagnoses of observaties
**Waarom FHIR?**
In FHIR heet dit een **Flag** resource. Dit correspondeert met de Nederlandse **ZIB Alert**.
**Verschil met Observations:**
Observations zijn metingen/bevindingen. Flags zijn **actieve waarschuwingen** die aandacht vragen.
**Categorieën:**
- **Safety (veiligheid)**: Suïciderisico, zelfverwaarlozing, valrisico
- **Clinical (klinisch)**: Ernstige allergie voor medicatie, infectiegevaar
- **Behavioral (gedrag)**: Agressie naar hulpverleners, grensoverschrijdend gedrag
- **Administrative**: Geen-toon status (privacy), wanbetaler
**Voorbeeld flags:**
- 🔴 "HOOG SUÏCIDERISICO - Concrete plannen, middelen aanwezig"
- 🟠 "Agressie naar vrouwelijke hulpverleners - Alleen mannelijke behandelaar"
- 🟡 "Allergie: Penicilline - anafylactische shock"
- ⚪ "Geen toestemming contact familie - Privacy verzoek"
**In de UI:**
Flags worden prominent weergegeven (rood banner bovenaan dossier) zodat ze niet gemist kunnen worden.
---
### 12. **Documenten** (`document_references`)
**Wat is het?**
Alle documenten in het dossier: intakeverslagen, behandelplannen, brieven aan huisarts, ROM-rapporten, etc.
**Belangrijkste gegevens:**
- Type document (intakeverslag, behandelplan, brief, rapport)
- Status (concept, definitief, vervangen)
- Datum
- Auteur (behandelaar)
- Gekoppeld aan welk contactmoment
- Content (Markdown tekst, PDF, of link naar bestand)
**Waarom FHIR?**
In FHIR heet dit een **DocumentReference** resource. Dit zorgt ervoor dat documenten doorzoekbaar zijn en gekoppeld kunnen worden aan specifieke momenten in de behandeling.
**MedMIJ-integratie:**
Via MedMIJ kunnen cliënten later hun eigen documenten ophalen in een persoonlijke gezondheidsomgeving (PGO-app).
---
## Hoe hangen deze onderdelen samen?
```
Cliënt (Patient)
├─── heeft Toestemmingen (Consents) ⚠️ AVG-compliant
├─── heeft Waarschuwingen (Flags) 🚨 Altijd zichtbaar
└─── heeft Contactmomenten (Encounters)
├─── leidt tot Diagnoses (Conditions)
│ └─── ondersteund door Observaties (Observations)
├─── gebruikt Medicatie (MedicationStatements)
├─── krijgt Behandelplan (CarePlan)
│ ├─── met Doelen (Goals) 🎯 Meetbaar
│ └─── met Activiteiten (CarePlanActivities)
└─── resulteert in Documenten (DocumentReferences)
Uitgevoerd door Behandelaar (Practitioner)
Binnen Instelling (Organization)
```
**Nieuwe verbindingen:**
- **Goals** zijn gekoppeld aan **CarePlan** en **Conditions**
- **Goals** worden gemeten via **Observations** (ROM-scores)
- **Flags** zijn gekoppeld aan **Conditions** en **Observations** (wat veroorzaakt de alert)
- **Consents** bepalen wie **DocumentReferences** mag inzien
---
## Waarom FHIR gebruiken?
### **1. Toekomstbestendig**
FHIR is de internationale standaard voor zorggegevens. Alle moderne zorgsystemen ondersteunen dit. Door vanaf dag 1 FHIR-compliant te bouwen, kunnen we later makkelijk integreren met:
- MedMIJ (patiëntportalen)
- Koppeltaal (eHealth apps)
- Landelijk Schakelpunt (LSP)
- Andere GGZ-instellingen
- Huisartseninformatiesystemen
### **2. Herbruikbaarheid**
Elk onderdeel ("resource") kan apart uitgewisseld worden. Bijvoorbeeld:
- Huisarts vraagt diagnoses op via FHIR API
- Cliënt haalt eigen medicatielijst op via MedMIJ
- eHealth app ontvangt behandelplan via Koppeltaal
### **3. Geen vendor lock-in**
Omdat we een open standaard gebruiken, zijn we niet afhankelijk van één leverancier. Data kan altijd geëxporteerd en geïmporteerd worden in FHIR-formaat.
### **4. Bewezen technologie**
FHIR wordt wereldwijd gebruikt door duizenden ziekenhuizen, klinieken en zorginstellingen. Alle grote EPD-leveranciers ondersteunen het.
---
## MedMIJ & Koppeltaal: Wat betekent dit?
### **MedMIJ - Patiëntportalen**
MedMIJ is het Nederlandse afsprakenstelsel waarmee patiënten hun medische gegevens kunnen ophalen in een PGO-app (Persoonlijke Gezondheidsomgeving).
**Voor GGZ is de "Basisgegevens GGZ 2.0" specificatie relevant:**
- 24 zorginformatiebouwstenen (ZIBs)
- Inclusief: diagnoses, medicatie, behandelplan, contactmomenten
**Ons datamodel ondersteunt dit omdat:**
- Alle velden volgen de MedMIJ FHIR profielen
- DSM-5 codes zijn opgenomen
- Juridische status kan vastgelegd worden
- Medicatie volgens MedicatieProces 9.0
**In de toekomst kunnen we:**
- Een FHIR API bouwen die MedMIJ-compliant is
- Cliënten toegang geven tot hun eigen dossier via een PGO-app
- Automatisch gegevens uitwisselen met andere zorgaanbieders
### **Koppeltaal - eHealth Apps**
Koppeltaal is de standaard waarmee GGZ-instellingen eHealth apps kunnen koppelen aan hun EPD.
**Voorbeeld:**
Behandelaar schrijft voor: "Doe dagelijks de mindfulness oefening in app MindDistrict"
→ Koppeltaal zorgt dat dit automatisch in het EPD en in de app komt te staan
→ Voortgang komt automatisch terug in het EPD
**Ons datamodel ondersteunt dit omdat:**
- CarePlan resource volgt Koppeltaal specificaties
- Activities kunnen gekoppeld worden aan externe apps
- Status updates worden automatisch verwerkt
---
## Privacy & Beveiliging
### **Encryptie**
- BSN wordt versleuteld opgeslagen
- Communicatie via HTTPS/TLS
### **Toegangscontrole (RLS)**
- Behandelaren zien alleen hun eigen cliënten
- Cliënten kunnen later hun eigen data inzien (via patiëntenportaal)
- Auditlog houdt bij wie wat wanneer heeft bekeken
### **AVG-compliance**
- Recht op inzage: cliënt kan eigen data opvragen
- Recht op vergetelheid: data kan verwijderd worden
- Logging: alle acties worden gelogd
- Bewaartermijnen: automatische archivering na X jaar
---
## Technische implementatie
### **Database: PostgreSQL (Supabase)**
- Type-safe met ENUMs voor statussen
- Automatische timestamps (created_at, updated_at)
- Foreign keys voor relaties
- Indexes voor performance
### **Veldnamen volgen FHIR**
Bijvoorbeeld:
- `name_family` → Patient.name.family
- `code_code` → Condition.code.coding.code
- `clinical_status` → Condition.clinicalStatus
Dit maakt het later makkelijk om FHIR JSON te genereren.
### **Later: FHIR API endpoints**
```
GET /fhir/Patient/{id}
GET /fhir/Encounter?patient={id}
GET /fhir/Condition?patient={id}
GET /fhir/CarePlan?patient={id}
```
---
## Wat betekent dit voor gebruikers?
### **Voor behandelaren:**
- Alle data is logisch gestructureerd
- Diagnoses zijn gekoppeld aan intake-moment
- Behandelplan volgt automatisch uit diagnose
- ROM-scores zijn zichtbaar in tijdlijn
### **Voor cliënten (in toekomst):**
- Eigen dossier inzien via app
- Behandelplan en afspraken zien
- ROM-vragenlijsten invullen via app
- Resultaten direct naar behandelaar
### **Voor beheerders:**
- Export naar andere systemen is mogelijk
- Backups bevatten FHIR-compliant data
- Audits en rapportages zijn eenvoudig
- Geen vendor lock-in
---
## Roadmap
### **Fase 1: MVP (nu)**
✅ Database schema met alle FHIR resources
✅ Intake → Diagnose → Behandelplan workflow
✅ Basis toegangscontrole
### **Fase 2: Basis functionaliteit**
🔲 UI voor alle resources
🔲 AI-assistentie voor intake
🔲 ROM-metingen integratie
### **Fase 3: Integraties**
🔲 FHIR API endpoints
🔲 MedMIJ aansluiting (patiëntportaal)
🔲 Koppeltaal aansluiting (eHealth apps)
🔲 LSP medicatie-uitwisseling
---
## Referenties
- **FHIR Specificatie:** https://hl7.org/fhir/
- **MedMIJ GGZ:** https://informatiestandaarden.nictiz.nl/wiki/MedMij:V2020.01/OntwerpGGZ
- **Koppeltaal:** https://www.koppeltaal.nl/
- **ZIBs (ZorgInformatieBouwstenen):** https://zibs.nl/
- **DSM-5 Codes:** American Psychiatric Association
- **MedicatieProces 9.0:** https://informatiestandaarden.nictiz.nl/wiki/mp:V9
---
**Laatst bijgewerkt:** 21 november 2024
**Auteur:** Colin Lit (ikbenlit.nl)
**Project:** AI Speedrun - Mini-ECD