mitgliederverwaltung/docs/database_schema.dbml
Moritz 3852655597 docs: add comprehensive project documentation and reduce redundancy
Add CODE_GUIDELINES.md, database schema docs, and development-progress-log.md.
Refactor README.md to eliminate redundant information by linking to detailed docs.
Establish clear documentation hierarchy for better maintainability.
2025-11-13 11:15:57 +01:00

329 lines
12 KiB
Text

// Mila - Membership Management System
// Database Schema Documentation
//
// This file can be used with:
// - https://dbdiagram.io
// - https://dbdocs.io
// - VS Code Extensions: "DBML Language" or "dbdiagram.io"
//
// Version: 1.0
// Last Updated: 2025-11-10
Project mila_membership_management {
database_type: 'PostgreSQL'
Note: '''
# Mila Membership Management System
A membership management application for small to mid-sized clubs.
## Key Features:
- User authentication (OIDC + Password)
- Member management with flexible custom properties
- Bidirectional email synchronization between users and members
- Full-text search capabilities
- GDPR-compliant data management
## Domains:
- **Accounts**: User authentication and session management
- **Membership**: Club member data and custom properties
'''
}
// ============================================
// ACCOUNTS DOMAIN
// ============================================
Table users {
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
email citext [not null, unique, note: 'Email address (case-insensitive) - source of truth when linked to member']
hashed_password text [null, note: 'Bcrypt-hashed password (null for OIDC-only users)']
oidc_id text [null, unique, note: 'External OIDC identifier from authentication provider (e.g., Rauthy)']
member_id uuid [null, unique, note: 'Optional 1:1 link to member record']
indexes {
email [unique, name: 'users_unique_email_index']
oidc_id [unique, name: 'users_unique_oidc_id_index']
member_id [unique, name: 'users_unique_member_index']
}
Note: '''
**User Authentication Table**
Handles user login accounts with two authentication strategies:
1. Password-based authentication (email + hashed_password)
2. OIDC/SSO authentication (email + oidc_id)
**Relationship with Members:**
- Optional 1:1 relationship with members table (0..1 ↔ 0..1)
- A user can have 0 or 1 member (user.member_id can be NULL)
- A member can have 0 or 1 user (optional has_one relationship)
- Both entities can exist independently
- When linked, user.email is the source of truth
- Email changes sync bidirectionally between user ↔ member
**Constraints:**
- At least one auth method required (password OR oidc_id)
- Email must be unique across all users
- OIDC ID must be unique if present
- Member can only be linked to one user (enforced by unique index)
**Deletion Behavior:**
- When member is deleted → user.member_id set to NULL (user preserved)
- When user is deleted → member.user relationship cleared (member preserved)
'''
}
Table tokens {
jti text [pk, not null, note: 'JWT ID - unique token identifier']
subject text [not null, note: 'Token subject (usually user ID)']
purpose text [not null, note: 'Token purpose (e.g., "access", "refresh", "password_reset")']
expires_at timestamp [not null, note: 'Token expiration timestamp (UTC)']
extra_data jsonb [null, note: 'Additional token metadata']
created_at timestamp [not null, default: `now() AT TIME ZONE 'utc'`, note: 'Creation timestamp (UTC)']
updated_at timestamp [not null, default: `now() AT TIME ZONE 'utc'`, note: 'Last update timestamp (UTC)']
indexes {
subject [name: 'tokens_subject_idx', note: 'For user token lookups']
expires_at [name: 'tokens_expires_at_idx', note: 'For token cleanup queries']
purpose [name: 'tokens_purpose_idx', note: 'For purpose-based queries']
}
Note: '''
**AshAuthentication Token Management**
Stores JWT tokens for authentication and authorization.
**Token Purposes:**
- `access`: Short-lived access tokens for API requests
- `refresh`: Long-lived tokens for obtaining new access tokens
- `password_reset`: Temporary tokens for password reset flow
- `email_confirmation`: Temporary tokens for email verification
**Token Lifecycle:**
- Tokens are created during login/registration
- Can be revoked by deleting the record
- Expired tokens should be cleaned up periodically
- `store_all_tokens? true` enables token tracking
'''
}
// ============================================
// MEMBERSHIP DOMAIN
// ============================================
Table members {
id uuid [pk, not null, default: `uuid_generate_v7()`, note: 'UUIDv7 primary key (sortable by creation time)']
first_name text [not null, note: 'Member first name (min length: 1)']
last_name text [not null, note: 'Member last name (min length: 1)']
email text [not null, unique, note: 'Member email address (5-254 chars, validated)']
birth_date date [null, note: 'Date of birth (cannot be in future)']
paid boolean [null, note: 'Payment status flag']
phone_number text [null, note: 'Contact phone number (format: +?[0-9\- ]{6,20})']
join_date date [null, note: 'Date when member joined club (cannot be in future)']
exit_date date [null, note: 'Date when member left club (must be after join_date)']
notes text [null, note: 'Additional notes about member']
city text [null, note: 'City of residence']
street text [null, note: 'Street name']
house_number text [null, note: 'House number']
postal_code text [null, note: '5-digit German postal code']
search_vector tsvector [null, note: 'Full-text search index (auto-generated)']
indexes {
email [unique, name: 'members_unique_email_index']
search_vector [type: gin, name: 'members_search_vector_idx', note: 'GIN index for full-text search']
email [name: 'members_email_idx']
last_name [name: 'members_last_name_idx', note: 'For name sorting']
join_date [name: 'members_join_date_idx', note: 'For date filters']
(paid) [name: 'members_paid_idx', type: btree, note: 'Partial index WHERE paid IS NOT NULL']
}
Note: '''
**Club Member Master Data**
Core entity for membership management containing:
- Personal information (name, birth date, email)
- Contact details (phone, address)
- Membership status (join/exit dates, payment status)
- Additional notes
**Email Synchronization:**
When a member is linked to a user:
- User.email is the source of truth (overwrites member.email on link)
- Subsequent changes to either email sync bidirectionally
- Validates that email is not already used by another unlinked user
**Full-Text Search:**
- `search_vector` is auto-updated via trigger
- Weighted fields: first_name (A), last_name (A), email (B), notes (B)
- Supports flexible member search across multiple fields
**Relationships:**
- Optional 1:1 with users (0..1 ↔ 0..1) - authentication account
- 1:N with properties (custom dynamic fields)
**Validation Rules:**
- first_name, last_name: min 1 character
- email: 5-254 characters, valid email format
- birth_date: cannot be in future
- join_date: cannot be in future
- exit_date: must be after join_date (if both present)
- phone_number: matches pattern ^\+?[0-9\- ]{6,20}$
- postal_code: exactly 5 digits
'''
}
Table properties {
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
value jsonb [null, note: 'Union type value storage (format: {type: "string", value: "example"})']
member_id uuid [not null, note: 'Link to member']
property_type_id uuid [not null, note: 'Link to property type definition']
indexes {
(member_id, property_type_id) [unique, name: 'properties_unique_property_per_member_index', note: 'One property per type per member']
member_id [name: 'properties_member_id_idx']
property_type_id [name: 'properties_property_type_id_idx']
}
Note: '''
**Dynamic Custom Member Properties**
Provides flexible, extensible attributes for members beyond the fixed schema.
**Value Storage:**
- Stored as JSONB map with type discrimination
- Format: `{type: "string|integer|boolean|date|email", value: <actual_value>}`
- Allows multiple data types in single column
**Supported Types:**
- `string`: Text data
- `integer`: Numeric data
- `boolean`: True/False flags
- `date`: Date values
- `email`: Validated email addresses
**Constraints:**
- Each member can have only ONE property per property_type
- Properties are deleted when member is deleted (CASCADE)
- Property type cannot be deleted if properties exist (RESTRICT)
**Use Cases:**
- Custom membership numbers
- Additional contact methods
- Club-specific attributes
- Flexible data model without schema migrations
'''
}
Table property_types {
id uuid [pk, not null, default: `gen_random_uuid()`, note: 'Primary identifier']
name text [not null, unique, note: 'Property name/identifier (e.g., "membership_number")']
value_type text [not null, note: 'Data type: string | integer | boolean | date | email']
description text [null, note: 'Human-readable description']
immutable boolean [not null, default: false, note: 'If true, value cannot be changed after creation']
required boolean [not null, default: false, note: 'If true, all members must have this property']
indexes {
name [unique, name: 'property_types_unique_name_index']
}
Note: '''
**Property Type Definitions**
Defines the schema and behavior for custom member properties.
**Attributes:**
- `name`: Unique identifier for the property type
- `value_type`: Enforces data type consistency
- `description`: Documentation for users/admins
- `immutable`: Prevents changes after initial creation (e.g., membership numbers)
- `required`: Enforces that all members must have this property
**Constraints:**
- `value_type` must be one of: string, integer, boolean, date, email
- `name` must be unique across all property types
- Cannot be deleted if properties reference it (ON DELETE RESTRICT)
**Examples:**
- Membership Number (string, immutable, required)
- Emergency Contact (string, mutable, optional)
- Certified Trainer (boolean, mutable, optional)
- Certification Date (date, immutable, optional)
'''
}
// ============================================
// RELATIONSHIPS
// ============================================
// Optional 1:1 User ↔ Member Link
// - A user can have 0 or 1 linked member (optional)
// - A member can have 0 or 1 linked user (optional)
// - Both can exist independently
// - ON DELETE SET NULL: User preserved when member deleted
// - Email Synchronization: When linking occurs, user.email becomes source of truth
Ref: users.member_id - members.id [delete: set null]
// Member → Properties (1:N)
// - One member can have multiple properties
// - Each property belongs to exactly one member
// - ON DELETE CASCADE: Properties deleted when member deleted
// - UNIQUE constraint: One property per type per member
Ref: properties.member_id > members.id [delete: cascade]
// Property → PropertyType (N:1)
// - Many properties can reference one property type
// - Property type defines the schema/behavior
// - ON DELETE RESTRICT: Cannot delete type if properties exist
Ref: properties.property_type_id > property_types.id [delete: restrict]
// ============================================
// ENUMS
// ============================================
// Valid data types for property values
// Determines how Property.value is interpreted
Enum property_value_type {
string [note: 'Text data']
integer [note: 'Numeric data']
boolean [note: 'True/False flags']
date [note: 'Date values']
email [note: 'Validated email addresses']
}
// Token purposes for different authentication flows
Enum token_purpose {
access [note: 'Short-lived access tokens']
refresh [note: 'Long-lived refresh tokens']
password_reset [note: 'Password reset tokens']
email_confirmation [note: 'Email verification tokens']
}
// ============================================
// TABLE GROUPS
// ============================================
TableGroup accounts_domain {
users
tokens
Note: '''
**Accounts Domain**
Handles user authentication and session management using AshAuthentication.
Supports multiple authentication strategies (Password, OIDC).
'''
}
TableGroup membership_domain {
members
properties
property_types
Note: '''
**Membership Domain**
Core business logic for club membership management.
Supports flexible, extensible member data model.
'''
}