feat: add Participant and MagicToken models with automatic migrations

Implements Phase 2 infrastructure for participant registration and authentication:

Database Models:
- Add Participant model with exchange scoping and soft deletes
- Add MagicToken model for passwordless authentication
- Add participants relationship to Exchange model
- Include proper indexes and foreign key constraints

Migration Infrastructure:
- Generate Alembic migration for new models
- Create entrypoint.sh script for automatic migrations on container startup
- Update Containerfile to use entrypoint script and include uv binary
- Remove db.create_all() in favor of migration-based schema management

This establishes the foundation for implementing stories 4.1-4.3, 5.1-5.3, and 10.1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-22 16:23:47 -07:00
parent 5201b2f036
commit eaafa78cf3
22 changed files with 10459 additions and 7 deletions

View File

@@ -0,0 +1,777 @@
# Data Model - v0.2.0
**Version**: 0.2.0
**Date**: 2025-12-22
**Status**: Phase 2 Design
## Introduction
This document defines the complete database schema for Sneaky Klaus. The schema is designed for SQLite with SQLAlchemy ORM, optimized for read-heavy workloads with occasional writes, and structured to support all user stories in the product backlog.
**Phase 2 Additions**: This version fully implements the Participant and MagicToken tables that were designed in v0.1.0 but not yet utilized. These tables enable participant registration, magic link authentication, and participant sessions.
**Note**: Session storage is managed by Flask-Session, which creates and manages its own session table. The custom Session table previously defined has been removed in favor of Flask-Session's implementation.
## Entity Relationship Diagram
```mermaid
erDiagram
Admin ||--o{ Exchange : creates
Exchange ||--o{ Participant : contains
Exchange ||--o{ ExclusionRule : defines
Exchange ||--o{ NotificationPreference : configures
Participant ||--o{ Match : "gives to"
Participant ||--o{ Match : "receives from"
Participant ||--o{ MagicToken : authenticates_with
Participant }o--o{ ExclusionRule : excluded_from
Exchange {
int id PK
string slug UK
string name
text description
string budget
int max_participants
datetime registration_close_date
datetime exchange_date
string timezone
string state
datetime created_at
datetime updated_at
datetime completed_at
}
Admin {
int id PK
string email UK
string password_hash
datetime created_at
datetime updated_at
}
Participant {
int id PK
int exchange_id FK
string name
string email
text gift_ideas
boolean reminder_enabled
datetime created_at
datetime updated_at
datetime withdrawn_at
}
Match {
int id PK
int exchange_id FK
int giver_id FK
int receiver_id FK
datetime created_at
}
ExclusionRule {
int id PK
int exchange_id FK
int participant_a_id FK
int participant_b_id FK
datetime created_at
}
MagicToken {
int id PK
string token_hash UK
string token_type
string email
int participant_id FK
int exchange_id FK
datetime created_at
datetime expires_at
datetime used_at
}
PasswordResetToken {
int id PK
string token_hash UK
string email
datetime created_at
datetime expires_at
datetime used_at
}
RateLimit {
int id PK
string key UK
int attempts
datetime window_start
datetime expires_at
}
NotificationPreference {
int id PK
int exchange_id FK
boolean new_registration
boolean participant_withdrawal
boolean matching_complete
datetime created_at
datetime updated_at
}
```
## Entity Definitions
### Admin
The administrator account for the entire installation. Only one admin exists per deployment.
**Table**: `admin`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY | Auto-increment ID |
| `email` | VARCHAR(255) | UNIQUE, NOT NULL | Admin email address |
| `password_hash` | VARCHAR(255) | NOT NULL | bcrypt password hash |
| `created_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Account creation timestamp |
| `updated_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Last update timestamp |
**Indexes**:
- `idx_admin_email` on `email` (unique)
**Constraints**:
- Email format validation at application level
- Only one admin record should exist (enforced at application level)
**Notes**:
- Password hash uses bcrypt with cost factor 12
- `updated_at` automatically updated on any modification
---
### Exchange
Represents a single Secret Santa exchange event.
**Table**: `exchange`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY | Auto-increment ID |
| `slug` | VARCHAR(12) | UNIQUE, NOT NULL | URL-safe identifier for exchange |
| `name` | VARCHAR(255) | NOT NULL | Exchange name/title |
| `description` | TEXT | NULLABLE | Optional description |
| `budget` | VARCHAR(100) | NOT NULL | Gift budget (e.g., "$20-30") |
| `max_participants` | INTEGER | NOT NULL, CHECK >= 3 | Maximum participant limit |
| `registration_close_date` | TIMESTAMP | NOT NULL | When registration ends |
| `exchange_date` | TIMESTAMP | NOT NULL | When gifts are exchanged |
| `timezone` | VARCHAR(50) | NOT NULL | Timezone for dates (e.g., "America/New_York") |
| `state` | VARCHAR(20) | NOT NULL | Current state (see states below) |
| `created_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Exchange creation timestamp |
| `updated_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Last update timestamp |
| `completed_at` | TIMESTAMP | NULLABLE | When exchange was marked complete |
**Indexes**:
- `idx_exchange_slug` on `slug` (unique)
- `idx_exchange_state` on `state`
- `idx_exchange_exchange_date` on `exchange_date`
- `idx_exchange_completed_at` on `completed_at`
**States** (enum enforced at application level):
- `draft`: Exchange created but not accepting registrations
- `registration_open`: Participants can register
- `registration_closed`: Registration ended, ready for matching
- `matched`: Participants have been assigned recipients
- `completed`: Exchange date has passed
**State Transitions**:
- `draft``registration_open`
- `registration_open``registration_closed`
- `registration_closed``registration_open` (reopen)
- `registration_closed``matched` (after matching)
- `matched``registration_open` (reopen, clears matches)
- `matched``completed`
**Constraints**:
- `registration_close_date` must be before `exchange_date` (validated at application level)
- `max_participants` minimum value: 3
- Timezone must be valid IANA timezone (validated at application level)
- `slug` must be unique across all exchanges
**Slug Generation**:
- Generated on exchange creation using `secrets.choice()` from Python's secrets module
- 12 URL-safe alphanumeric characters (a-z, A-Z, 0-9)
- Immutable once generated (never changes)
- Used in public URLs for exchange registration and participant access
**Cascade Behavior**:
- Deleting exchange cascades to: participants, matches, exclusion rules, notification preferences
---
### Participant
A person registered in a specific exchange.
**Table**: `participant`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY | Auto-increment ID |
| `exchange_id` | INTEGER | FOREIGN KEY → exchange.id, NOT NULL | Associated exchange |
| `name` | VARCHAR(255) | NOT NULL | Display name |
| `email` | VARCHAR(255) | NOT NULL | Email address |
| `gift_ideas` | TEXT | NULLABLE | Wishlist/gift preferences |
| `reminder_enabled` | BOOLEAN | NOT NULL, DEFAULT TRUE | Opt-in for reminder emails |
| `created_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Registration timestamp |
| `updated_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Last update timestamp |
| `withdrawn_at` | TIMESTAMP | NULLABLE | Withdrawal timestamp (soft delete) |
**Indexes**:
- `idx_participant_exchange_id` on `exchange_id`
- `idx_participant_email` on `email`
- `idx_participant_exchange_email` on `(exchange_id, email)` (composite unique)
**Constraints**:
- Email must be unique within an exchange (composite unique index)
- Email format validation at application level
- Cannot modify after matching except gift_ideas and reminder_enabled (enforced at application level)
**Cascade Behavior**:
- Deleting exchange cascades to delete participants
- Deleting participant cascades to: matches (as giver or receiver), exclusion rules, magic tokens
**Soft Delete**:
- Withdrawal sets `withdrawn_at` instead of hard delete
- Withdrawn participants excluded from matching and participant lists
- Withdrawn participants cannot be re-activated (must re-register)
---
### Match
Represents a giver-receiver assignment in an exchange.
**Table**: `match`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY | Auto-increment ID |
| `exchange_id` | INTEGER | FOREIGN KEY → exchange.id, NOT NULL | Associated exchange |
| `giver_id` | INTEGER | FOREIGN KEY → participant.id, NOT NULL | Participant giving gift |
| `receiver_id` | INTEGER | FOREIGN KEY → participant.id, NOT NULL | Participant receiving gift |
| `created_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Match creation timestamp |
**Indexes**:
- `idx_match_exchange_id` on `exchange_id`
- `idx_match_giver_id` on `giver_id`
- `idx_match_receiver_id` on `receiver_id`
- `idx_match_exchange_giver` on `(exchange_id, giver_id)` (composite unique)
**Constraints**:
- Each participant can be a giver exactly once per exchange (composite unique)
- Each participant can be a receiver exactly once per exchange (enforced at application level)
- `giver_id` cannot equal `receiver_id` (no self-matching, enforced at application level)
- Both giver and receiver must belong to same exchange (enforced at application level)
**Cascade Behavior**:
- Deleting exchange cascades to delete matches
- Deleting participant cascades to delete matches (triggers re-match requirement)
**Validation**:
- All participants in exchange must have exactly one match as giver and one as receiver
- No exclusion rules violated (enforced during matching)
---
### ExclusionRule
Defines pairs of participants who should not be matched together.
**Table**: `exclusion_rule`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY | Auto-increment ID |
| `exchange_id` | INTEGER | FOREIGN KEY → exchange.id, NOT NULL | Associated exchange |
| `participant_a_id` | INTEGER | FOREIGN KEY → participant.id, NOT NULL | First participant |
| `participant_b_id` | INTEGER | FOREIGN KEY → participant.id, NOT NULL | Second participant |
| `created_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Rule creation timestamp |
**Indexes**:
- `idx_exclusion_exchange_id` on `exchange_id`
- `idx_exclusion_participants` on `(exchange_id, participant_a_id, participant_b_id)` (composite unique)
**Constraints**:
- `participant_a_id` and `participant_b_id` must be different (enforced at application level)
- Both participants must belong to same exchange (enforced at application level)
- Exclusion is bidirectional: A→B exclusion also means B→A (handled at application level)
- Prevent duplicate rules with swapped participant IDs (enforced by ordering IDs: `participant_a_id < participant_b_id`)
**Cascade Behavior**:
- Deleting exchange cascades to delete exclusion rules
- Deleting participant cascades to delete related exclusion rules
**Application Logic**:
- When adding exclusion, always store with lower ID as participant_a, higher ID as participant_b
- Matching algorithm treats exclusion as bidirectional
---
### MagicToken
Time-limited tokens for participant passwordless authentication and admin password reset.
**Table**: `magic_token`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY | Auto-increment ID |
| `token_hash` | VARCHAR(255) | UNIQUE, NOT NULL | SHA-256 hash of token |
| `token_type` | VARCHAR(20) | NOT NULL | 'magic_link' or 'password_reset' |
| `email` | VARCHAR(255) | NOT NULL | Email address token sent to |
| `participant_id` | INTEGER | FOREIGN KEY → participant.id, NULLABLE | For magic links only |
| `exchange_id` | INTEGER | FOREIGN KEY → exchange.id, NULLABLE | For magic links only |
| `created_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Token creation timestamp |
| `expires_at` | TIMESTAMP | NOT NULL | Token expiration (1 hour from creation) |
| `used_at` | TIMESTAMP | NULLABLE | When token was consumed |
**Indexes**:
- `idx_magic_token_hash` on `token_hash` (unique)
- `idx_magic_token_type_email` on `(token_type, email)`
- `idx_magic_token_expires_at` on `expires_at`
**Constraints**:
- `token_type` must be 'magic_link' or 'password_reset' (enforced at application level)
- For magic_link: `participant_id` and `exchange_id` must be NOT NULL
- For password_reset: `participant_id` and `exchange_id` must be NULL
- Tokens expire 1 hour after creation
- Tokens are single-use (validated via `used_at`)
**Token Generation**:
- Token: 32-byte random value from `secrets` module, base64url encoded
- Hash: SHA-256 hash of token (stored in database)
- Original token sent in email, never stored
**Validation**:
- Token valid if: hash matches, not expired, not used
- On successful validation: set `used_at` timestamp, create session
**Cleanup**:
- Expired or used tokens purged hourly via background job
**Cascade Behavior**:
- Deleting participant cascades to delete magic tokens
- Deleting exchange cascades to delete related magic tokens
---
### RateLimit
Tracks authentication attempt rate limits per email/key.
**Table**: `rate_limit`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY | Auto-increment ID |
| `key` | VARCHAR(255) | UNIQUE, NOT NULL | Rate limit key (e.g., "login:email@example.com") |
| `attempts` | INTEGER | NOT NULL, DEFAULT 0 | Number of attempts in current window |
| `window_start` | TIMESTAMP | NOT NULL, DEFAULT NOW | Start of current rate limit window |
| `expires_at` | TIMESTAMP | NOT NULL | When rate limit resets |
**Indexes**:
- `idx_rate_limit_key` on `key` (unique)
- `idx_rate_limit_expires_at` on `expires_at`
**Rate Limit Policies**:
- Admin login: 5 attempts per 15 minutes per email
- Participant magic link: 3 requests per hour per email
- Password reset: 3 requests per hour per email
**Key Format**:
- Admin login: `login:admin:{email}`
- Magic link: `magic_link:{email}`
- Password reset: `password_reset:{email}`
**Workflow**:
1. Check if key exists and within window
2. If attempts exceeded: reject request
3. If within limits: increment attempts
4. If window expired: reset attempts and window
**Cleanup**:
- Expired rate limit entries purged daily via background job
---
### NotificationPreference
Admin's email notification preferences per exchange or globally.
**Table**: `notification_preference`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| `id` | INTEGER | PRIMARY KEY | Auto-increment ID |
| `exchange_id` | INTEGER | FOREIGN KEY → exchange.id, NULLABLE | Specific exchange (NULL = global default) |
| `new_registration` | BOOLEAN | NOT NULL, DEFAULT TRUE | Notify on new participant registration |
| `participant_withdrawal` | BOOLEAN | NOT NULL, DEFAULT TRUE | Notify on participant withdrawal |
| `matching_complete` | BOOLEAN | NOT NULL, DEFAULT TRUE | Notify on successful matching |
| `created_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Preference creation timestamp |
| `updated_at` | TIMESTAMP | NOT NULL, DEFAULT NOW | Last update timestamp |
**Indexes**:
- `idx_notification_exchange_id` on `exchange_id` (unique)
**Constraints**:
- Only one notification preference per exchange (unique index)
- Only one global preference (exchange_id = NULL)
**Application Logic**:
- If exchange-specific preference exists, use it
- Otherwise, fall back to global preference
- If no preferences exist, default all to TRUE
**Cascade Behavior**:
- Deleting exchange cascades to delete notification preferences
---
## Data Types & Conventions
### Timestamps
- **Type**: `TIMESTAMP` (SQLite stores as ISO 8601 string)
- **Timezone**: All timestamps stored in UTC
- **Application Layer**: Convert to exchange timezone for display
- **Automatic Fields**: `created_at`, `updated_at` managed by SQLAlchemy
### Email Addresses
- **Type**: `VARCHAR(255)`
- **Validation**: RFC 5322 format validation at application level
- **Storage**: Lowercase normalized
- **Indexing**: Indexed for lookup performance
### Enumerations
SQLite doesn't support native enums. Enforced at application level:
**ExchangeState**:
- `draft`
- `registration_open`
- `registration_closed`
- `matched`
- `completed`
**UserType** (Session):
- `admin`
- `participant`
**TokenType** (MagicToken):
- `magic_link`
- `password_reset`
### JSON Fields
- **Type**: `TEXT` (SQLite)
- **Serialization**: JSON string
- **Access**: SQLAlchemy JSON type provides automatic serialization/deserialization
- **Usage**: Session data, future extensibility
### Boolean Fields
- **Type**: `BOOLEAN` (SQLite stores as INTEGER: 0 or 1)
- **Default**: Explicit defaults defined per field
- **Access**: SQLAlchemy Boolean type handles conversion
---
## Performance Optimization
### Indexes Summary
| Table | Index Name | Columns | Type | Purpose |
|-------|------------|---------|------|---------|
| admin | idx_admin_email | email | UNIQUE | Login lookup |
| exchange | idx_exchange_slug | slug | UNIQUE | Exchange lookup by slug |
| exchange | idx_exchange_state | state | INDEX | Filter by state |
| exchange | idx_exchange_exchange_date | exchange_date | INDEX | Auto-completion job |
| exchange | idx_exchange_completed_at | completed_at | INDEX | Purge job |
| participant | idx_participant_exchange_id | exchange_id | INDEX | List participants |
| participant | idx_participant_email | email | INDEX | Lookup by email |
| participant | idx_participant_exchange_email | exchange_id, email | UNIQUE | Email uniqueness per exchange |
| match | idx_match_exchange_id | exchange_id | INDEX | List matches |
| match | idx_match_giver_id | giver_id | INDEX | Lookup giver's match |
| match | idx_match_receiver_id | receiver_id | INDEX | Lookup receivers |
| match | idx_match_exchange_giver | exchange_id, giver_id | UNIQUE | Prevent duplicate givers |
| exclusion_rule | idx_exclusion_exchange_id | exchange_id | INDEX | List exclusions |
| exclusion_rule | idx_exclusion_participants | exchange_id, participant_a_id, participant_b_id | UNIQUE | Prevent duplicates |
| magic_token | idx_magic_token_hash | token_hash | UNIQUE | Token validation |
| magic_token | idx_magic_token_type_email | token_type, email | INDEX | Rate limit checks |
| magic_token | idx_magic_token_expires_at | expires_at | INDEX | Cleanup job |
| rate_limit | idx_rate_limit_key | key | UNIQUE | Rate limit lookup |
| rate_limit | idx_rate_limit_expires_at | expires_at | INDEX | Cleanup job |
| notification_preference | idx_notification_exchange_id | exchange_id | UNIQUE | Preference lookup |
### Query Optimization Strategies
1. **Eager Loading**: Use SQLAlchemy `joinedload()` for relationships to prevent N+1 queries
2. **Batch Operations**: Use bulk insert/update for matching operations
3. **Filtered Queries**: Always filter by exchange_id first (most selective)
4. **Connection Pooling**: Disabled for SQLite (single file, no benefit)
5. **WAL Mode**: Enabled for better read concurrency
### SQLite Configuration
```python
# Connection string
DATABASE_URL = 'sqlite:///data/sneaky-klaus.db'
# Pragmas (set on connection)
PRAGMA journal_mode = WAL; # Write-Ahead Logging
PRAGMA foreign_keys = ON; # Enforce foreign keys
PRAGMA synchronous = NORMAL; # Balance safety and performance
PRAGMA temp_store = MEMORY; # Temporary tables in memory
PRAGMA cache_size = -64000; # 64MB cache
```
---
## Data Validation Rules
### Exchange
- `name`: 1-255 characters, required
- `slug`: Exactly 12 URL-safe alphanumeric characters, auto-generated, unique
- `budget`: 1-100 characters, required (freeform text)
- `max_participants`: ≥ 3, required
- `registration_close_date` < `exchange_date`: validated at application level
- `timezone`: Valid IANA timezone name
### Participant
- `name`: 1-255 characters, required
- `email`: Valid email format, unique per exchange
- `gift_ideas`: 0-10,000 characters (optional)
### Match
- Validation at matching time:
- All participants have exactly one match (as giver and receiver)
- No self-matches
- No exclusion rules violated
- Single cycle preferred
### ExclusionRule
- `participant_a_id` < `participant_b_id`: enforced when creating
- Both participants must be in same exchange
- Cannot exclude participant from themselves
### MagicToken
- Token expiration: 1 hour from creation
- Single use only
- Token hash must be unique
---
## Audit Trail
### Change Tracking
- All tables have `created_at` timestamp
- Most tables have `updated_at` timestamp (auto-updated)
- Soft deletes used where appropriate (`withdrawn_at` for participants)
### Future Audit Enhancements
If detailed audit trail is needed:
- Create `audit_log` table
- Track: entity type, entity ID, action, user, timestamp, changes
- Trigger-based or application-level logging
---
## Migration Strategy
### Initial Schema Creation
Use Alembic for database migrations:
```bash
# Create initial migration
alembic revision --autogenerate -m "Initial schema"
# Apply migration
alembic upgrade head
```
### Schema Versioning
- Alembic tracks migration version in `alembic_version` table
- Each schema change creates new migration file
- Migrations can be rolled back if needed
### Future Schema Changes
When adding fields or tables:
1. Create Alembic migration
2. Test migration on copy of production database
3. Run migration during deployment
4. Update SQLAlchemy models
---
## Sample Data Relationships
### Example: Complete Exchange Flow
1. **Exchange Created** (state: draft)
- 1 exchange record created
2. **Registration Opens** (state: registration_open)
- Exchange state updated
- Participants register (3-100 participant records)
3. **Registration Closes** (state: registration_closed)
- Exchange state updated
- Admin adds exclusion rules (0-N exclusion_rule records)
4. **Matching Occurs** (state: matched)
- Exchange state updated
- Match records created (N matches for N participants)
- Magic tokens created for notifications
5. **Exchange Completes** (state: completed)
- Exchange state updated
- `completed_at` timestamp set
- 30-day retention countdown begins
6. **Data Purge** (30 days after completion)
- Exchange deleted (cascades to participants, matches, exclusions)
---
## Database Size Estimates
### Assumptions
- 50 exchanges per installation
- Average 20 participants per exchange
- 3 exclusion rules per exchange
- 30-day retention = 5 active exchanges + 5 completed
### Storage Calculation
| Entity | Records | Avg Size | Total |
|--------|---------|----------|-------|
| Admin | 1 | 500 B | 500 B |
| Exchange | 50 | 1 KB | 50 KB |
| Participant | 1,000 | 2 KB | 2 MB |
| Match | 1,000 | 200 B | 200 KB |
| ExclusionRule | 150 | 200 B | 30 KB |
| Flask-Session (managed) | 50 | 500 B | 25 KB |
| MagicToken | 200 | 500 B | 100 KB |
| RateLimit | 100 | 300 B | 30 KB |
| NotificationPreference | 50 | 300 B | 15 KB |
**Total Estimated Size**: ~3 MB (excluding indexes and overhead)
**With Indexes & Overhead**: ~10 MB typical, ~50 MB maximum
SQLite database file size well within acceptable limits for self-hosted deployment.
---
## Backup & Recovery
### Backup Strategy
**Database File Location**: `/app/data/sneaky-klaus.db`
**Backup Methods**:
1. **Volume Snapshot**: Recommended for Docker deployments
```bash
docker run --rm -v sneaky-klaus-data:/data -v $(pwd):/backup \
alpine tar czf /backup/sneaky-klaus-backup-$(date +%Y%m%d).tar.gz /data
```
2. **SQLite Backup Command**: Online backup without downtime
```bash
sqlite3 sneaky-klaus.db ".backup sneaky-klaus-backup.db"
```
3. **File Copy**: Requires application shutdown
```bash
docker stop sneaky-klaus
cp /app/data/sneaky-klaus.db /backups/
docker start sneaky-klaus
```
**Backup Frequency**: Daily (automated via cron or external backup tool)
**Retention**: 30 days of backups
### Recovery Procedure
1. Stop application container
2. Replace database file with backup
3. Restart application
4. Verify health endpoint
5. Test basic functionality (login, view exchanges)
---
## Constraints Summary
### Foreign Key Relationships
| Child Table | Column | Parent Table | Parent Column | On Delete |
|-------------|--------|--------------|---------------|-----------|
| participant | exchange_id | exchange | id | CASCADE |
| match | exchange_id | exchange | id | CASCADE |
| match | giver_id | participant | id | CASCADE |
| match | receiver_id | participant | id | CASCADE |
| exclusion_rule | exchange_id | exchange | id | CASCADE |
| exclusion_rule | participant_a_id | participant | id | CASCADE |
| exclusion_rule | participant_b_id | participant | id | CASCADE |
| magic_token | participant_id | participant | id | CASCADE |
| magic_token | exchange_id | exchange | id | CASCADE |
| notification_preference | exchange_id | exchange | id | CASCADE |
**Flask-Session**: Managed by Flask-Session extension (no foreign keys in application schema)
**RateLimit**: No foreign keys (key-based, not entity-based)
### Unique Constraints
- `admin.email`: UNIQUE
- `exchange.slug`: UNIQUE
- `participant.(exchange_id, email)`: UNIQUE (composite)
- `match.(exchange_id, giver_id)`: UNIQUE (composite)
- `exclusion_rule.(exchange_id, participant_a_id, participant_b_id)`: UNIQUE (composite)
- `magic_token.token_hash`: UNIQUE
- `rate_limit.key`: UNIQUE
- `notification_preference.exchange_id`: UNIQUE (one preference per exchange)
### Check Constraints
- `exchange.max_participants >= 3`
- Application-level validation for additional constraints (state transitions, date ordering, etc.)
---
## Future Schema Enhancements
Potential additions for future versions:
1. **Audit Log Table**: Track all changes for compliance/debugging
2. **Email Queue Table**: Decouple email sending from transactional operations
3. **Participant Groups**: Support for couple/family groupings (exclude from each other automatically)
4. **Multiple Admins**: Add admin roles and permissions
5. **Exchange Templates**: Reusable exchange configurations
6. **Custom Fields**: User-defined participant attributes
These enhancements would require schema changes but are out of scope for v0.1.0.
---
## References
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)
- [SQLite Documentation](https://www.sqlite.org/docs.html)
- [Alembic Documentation](https://alembic.sqlalchemy.org/)
- [ADR-0001: Core Technology Stack](../../decisions/0001-core-technology-stack.md)
- [ADR-0002: Authentication Strategy](../../decisions/0002-authentication-strategy.md)