1039 lines
46 KiB
Markdown

# Complete Schema Reference
This page provides a comprehensive listing of all 33 models across both Prisma and Drizzle ORMs.
## Models Summary
| Group | Model | Table Name | Description | ORM |
|-------|-------|-----------|-------------|-----|
| **Auth & Users** | User | `users` | User accounts with role-based access control | Prisma |
| | RefreshToken | `refresh_tokens` | JWT refresh token storage | Prisma |
| **Influence** | Campaign | `campaigns` | Advocacy campaigns with feature flags | Prisma |
| | Representative | `representatives` | Cached representative data from Represent API | Prisma |
| | CampaignEmail | `campaign_emails` | Email tracking and delivery logs | Prisma |
| | RepresentativeResponse | `representative_responses` | Response wall submissions with moderation | Prisma |
| | ResponseUpvote | `response_upvotes` | Upvote tracking with deduplication | Prisma |
| | CustomRecipient | `custom_recipients` | Custom email targets for campaigns | Prisma |
| | PostalCodeCache | `postal_code_cache` | Postal code geocoding cache | Prisma |
| | EmailLog | `email_logs` | Global email audit trail | Prisma |
| | EmailVerification | `email_verifications` | Email verification tokens | Prisma |
| | Call | `calls` | Phone call tracking | Prisma |
| **Map — Locations** | Location | `locations` | Building-level address data with geocoding | Prisma |
| | Address | `addresses` | Unit-level data with support levels | Prisma |
| | LocationHistory | `location_history` | Audit trail for location changes | Prisma |
| **Map — Shifts & Cuts** | Shift | `shifts` | Volunteer shifts with scheduling | Prisma |
| | ShiftSignup | `shift_signups` | Shift signup tracking | Prisma |
| | Cut | `cuts` | GeoJSON polygon overlays for map filtering | Prisma |
| | MapSettings | `map_settings` | Singleton for map configuration | Prisma |
| **Canvassing** | CanvassSession | `canvass_sessions` | Canvassing session lifecycle | Prisma |
| | CanvassVisit | `canvass_visits` | Visit recording with outcomes | Prisma |
| | TrackingSession | `tracking_sessions` | GPS tracking sessions | Prisma |
| | TrackPoint | `track_points` | GPS breadcrumb trail | Prisma |
| **Email Templates** | EmailTemplate | `email_templates` | Email template master records | Prisma |
| | EmailTemplateVariable | `email_template_variables` | Template variable definitions | Prisma |
| | EmailTemplateVersion | `email_template_versions` | Template version history | Prisma |
| | EmailTemplateTestLog | `email_template_test_logs` | Test email audit logs | Prisma |
| **Landing Pages** | LandingPage | `landing_pages` | GrapesJS editor output with MkDocs export | Prisma |
| | PageBlock | `page_blocks` | Reusable block library | Prisma |
| **Site Settings** | SiteSettings | `site_settings` | Global site configuration singleton | Prisma |
| **Media** | videos | `videos` | Video library with metadata | Drizzle |
| | compilations | `compilations` | Video compilation tracking | Drizzle |
| | jobs | `jobs` | Job queue with resource management | Drizzle |
**Total:** 33 models (30 Prisma + 3 Drizzle)
---
## Auth & Users
### User
**Table:** `users`
**Description:** User accounts with role-based access control, temporary user support, and audit tracking.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| email | String | ✓ | — | Unique email address |
| password | String | ✓ | — | bcrypt hashed password (12+ chars policy) |
| name | String | ✗ | `null` | User display name |
| phone | String | ✗ | `null` | Phone number |
| role | UserRole | ✓ | `USER` | Role: SUPER_ADMIN, INFLUENCE_ADMIN, MAP_ADMIN, USER, TEMP |
| status | UserStatus | ✓ | `ACTIVE` | Status: ACTIVE, INACTIVE, SUSPENDED, EXPIRED |
| permissions | Json | ✗ | `null` | Granular per-app permissions object |
| createdVia | UserCreatedVia | ✓ | `STANDARD` | Creation source: ADMIN, PUBLIC_SHIFT_SIGNUP, STANDARD |
| expiresAt | DateTime | ✗ | `null` | Expiration date for TEMP users |
| expireDays | Int | ✗ | `null` | Days until expiration for TEMP users |
| lastLoginAt | DateTime | ✗ | `null` | Last login timestamp |
| emailVerified | Boolean | ✓ | `false` | Email verification status |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Unique: `email`
**Relations (33 total):**
- refreshTokens → RefreshToken[]
- campaignsCreated → Campaign[]
- campaignEmails → CampaignEmail[]
- responses → RepresentativeResponse[]
- responseUpvotes → ResponseUpvote[]
- shiftSignups → ShiftSignup[]
- locationsCreated → Location[]
- locationsUpdated → Location[]
- addressesCreated → Address[]
- addressesUpdated → Address[]
- locationEdits → LocationHistory[]
- cutsCreated → Cut[]
- canvassVisits → CanvassVisit[]
- canvassSessions → CanvassSession[]
- trackingSessions → TrackingSession[]
- templatesCreated → EmailTemplate[]
- templatesUpdated → EmailTemplate[]
- templateVersionsCreated → EmailTemplateVersion[]
- templateTestsSent → EmailTemplateTestLog[]
### RefreshToken
**Table:** `refresh_tokens`
**Description:** JWT refresh token storage with expiration tracking.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| token | String | ✓ | — | JWT refresh token (unique) |
| userId | String | ✓ | — | Foreign key to User |
| expiresAt | DateTime | ✓ | — | Token expiration timestamp |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
**Indexes:**
- Unique: `token`
- Foreign key: `userId`
**Relations:**
- user → User (onDelete: Cascade)
---
## Influence
### Campaign
**Table:** `campaigns`
**Description:** Advocacy campaigns with 12 feature flags and government-level targeting.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| slug | String | ✓ | — | URL-friendly slug (unique) |
| title | String | ✓ | — | Campaign title |
| description | String | ✗ | `null` | Campaign description (long text) |
| emailSubject | String | ✓ | — | Default email subject line |
| emailBody | String | ✓ | — | Default email body (long text) |
| callToAction | String | ✗ | `null` | Call-to-action text (long text) |
| coverPhoto | String | ✗ | `null` | Cover photo URL |
| status | CampaignStatus | ✓ | `DRAFT` | Status: DRAFT, ACTIVE, PAUSED, ARCHIVED |
| allowSmtpEmail | Boolean | ✓ | `true` | Allow SMTP email sending |
| allowMailtoLink | Boolean | ✓ | `true` | Allow mailto: links |
| collectUserInfo | Boolean | ✓ | `true` | Collect user information |
| showEmailCount | Boolean | ✓ | `true` | Show email sent count |
| showCallCount | Boolean | ✓ | `true` | Show call made count |
| allowEmailEditing | Boolean | ✓ | `false` | Allow users to edit email content |
| allowCustomRecipients | Boolean | ✓ | `false` | Allow custom email recipients |
| showResponseWall | Boolean | ✓ | `false` | Show public response wall |
| highlightCampaign | Boolean | ✓ | `false` | Highlight on campaign list page |
| targetGovernmentLevels | GovernmentLevel[] | ✓ | `[]` | Target levels: FEDERAL, PROVINCIAL, MUNICIPAL, SCHOOL_BOARD |
| createdByUserId | String | ✗ | `null` | Foreign key to User (creator) |
| createdByUserEmail | String | ✗ | `null` | Creator email (denormalized) |
| createdByUserName | String | ✗ | `null` | Creator name (denormalized) |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Unique: `slug`
**Relations:**
- createdByUser → User (onDelete: SetNull)
- emails → CampaignEmail[]
- responses → RepresentativeResponse[]
- customRecipients → CustomRecipient[]
- calls → Call[]
### Representative
**Table:** `representatives`
**Description:** Cached representative data from Represent API.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| postalCode | String | ✓ | — | Canadian postal code (indexed) |
| name | String | ✗ | `null` | Representative name |
| email | String | ✗ | `null` | Representative email |
| districtName | String | ✗ | `null` | Electoral district name |
| electedOffice | String | ✗ | `null` | Office title |
| partyName | String | ✗ | `null` | Political party |
| representativeSetName | String | ✗ | `null` | Representative set from Represent API |
| url | String | ✗ | `null` | Official website URL |
| photoUrl | String | ✗ | `null` | Photo URL |
| offices | Json | ✗ | `null` | Array of office contact info objects |
| cachedAt | DateTime | ✓ | `now()` | Cache timestamp |
**Indexes:**
- Non-unique: `postalCode`
**Relations:** None (standalone cache)
### CampaignEmail
**Table:** `campaign_emails`
**Description:** Email tracking and delivery logs for campaign emails.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| campaignId | String | ✓ | — | Foreign key to Campaign |
| campaignSlug | String | ✓ | — | Denormalized campaign slug |
| userId | String | ✗ | `null` | Foreign key to User (sender) |
| userEmail | String | ✗ | `null` | Sender email |
| userName | String | ✗ | `null` | Sender name |
| userPostalCode | String | ✗ | `null` | Sender postal code |
| recipientEmail | String | ✓ | — | Recipient email address |
| recipientName | String | ✗ | `null` | Recipient name |
| recipientTitle | String | ✗ | `null` | Recipient title |
| recipientLevel | GovernmentLevel | ✗ | `null` | Government level |
| emailMethod | EmailMethod | ✓ | — | Method: SMTP, MAILTO |
| subject | String | ✓ | — | Email subject line |
| message | String | ✓ | — | Email message body (long text) |
| status | CampaignEmailStatus | ✓ | `SENT` | Status: QUEUED, SENT, FAILED, CLICKED, USER_INFO_CAPTURED |
| senderIp | String | ✗ | `null` | Sender IP address |
| sentAt | DateTime | ✓ | `now()` | Send timestamp |
**Indexes:**
- Foreign key: `campaignId`
- Non-unique: `campaignSlug`
**Relations:**
- campaign → Campaign (onDelete: Cascade)
- user → User (onDelete: SetNull)
### RepresentativeResponse
**Table:** `representative_responses`
**Description:** Response wall submissions with moderation workflow.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| campaignId | String | ✓ | — | Foreign key to Campaign |
| campaignSlug | String | ✓ | — | Denormalized campaign slug |
| representativeName | String | ✓ | — | Representative name |
| representativeTitle | String | ✗ | `null` | Representative title |
| representativeLevel | GovernmentLevel | ✓ | — | Government level |
| representativeEmail | String | ✗ | `null` | Representative email |
| responseType | ResponseType | ✓ | — | Type: EMAIL, LETTER, PHONE_CALL, MEETING, SOCIAL_MEDIA, OTHER |
| responseText | String | ✓ | — | Response text (long text) |
| userComment | String | ✗ | `null` | User comment (long text) |
| screenshotUrl | String | ✗ | `null` | Screenshot URL |
| submittedByUserId | String | ✗ | `null` | Foreign key to User |
| submittedByName | String | ✗ | `null` | Submitter name |
| submittedByEmail | String | ✗ | `null` | Submitter email |
| isAnonymous | Boolean | ✓ | `false` | Anonymous submission flag |
| status | ResponseStatus | ✓ | `PENDING` | Status: PENDING, APPROVED, REJECTED |
| isVerified | Boolean | ✓ | `false` | Email verification status |
| verificationToken | String | ✗ | `null` | Verification token |
| verificationSentAt | DateTime | ✗ | `null` | Verification email timestamp |
| verifiedAt | DateTime | ✗ | `null` | Verification timestamp |
| verifiedBy | String | ✗ | `null` | Email address that verified |
| upvoteCount | Int | ✓ | `0` | Upvote count (denormalized) |
| submittedIp | String | ✗ | `null` | Submitter IP address |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Foreign key: `campaignId`
- Non-unique: `campaignSlug`
**Relations:**
- campaign → Campaign (onDelete: Cascade)
- submittedByUser → User (onDelete: SetNull)
- upvotes → ResponseUpvote[]
### ResponseUpvote
**Table:** `response_upvotes`
**Description:** Upvote tracking with deduplication by user ID and IP address.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| responseId | String | ✓ | — | Foreign key to RepresentativeResponse |
| userId | String | ✗ | `null` | Foreign key to User |
| userEmail | String | ✗ | `null` | User email (for guest upvotes) |
| upvotedIp | String | ✗ | `null` | Upvoter IP address |
**Indexes:**
- Unique: `[responseId, userId]` (prevent duplicate upvotes from logged-in users)
- Unique: `[responseId, upvotedIp]` (prevent duplicate upvotes from same IP)
**Relations:**
- response → RepresentativeResponse (onDelete: Cascade)
- user → User (onDelete: SetNull)
### CustomRecipient
**Table:** `custom_recipients`
**Description:** Custom email targets for campaigns (when allowCustomRecipients enabled).
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| campaignId | String | ✓ | — | Foreign key to Campaign |
| campaignSlug | String | ✓ | — | Denormalized campaign slug |
| recipientName | String | ✓ | — | Recipient name |
| recipientEmail | String | ✓ | — | Recipient email address |
| recipientTitle | String | ✗ | `null` | Recipient title |
| recipientOrganization | String | ✗ | `null` | Recipient organization |
| notes | String | ✗ | `null` | Admin notes (long text) |
| isActive | Boolean | ✓ | `true` | Active status |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Foreign key: `campaignId`
**Relations:**
- campaign → Campaign (onDelete: Cascade)
### PostalCodeCache
**Table:** `postal_code_cache`
**Description:** Postal code geocoding cache for centroid lookups.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| postalCode | String | ✓ | — | Canadian postal code (unique) |
| city | String | ✗ | `null` | City name |
| province | String | ✗ | `null` | Province code (e.g., "AB") |
| centroidLat | Decimal(10,8) | ✗ | `null` | Centroid latitude |
| centroidLng | Decimal(11,8) | ✗ | `null` | Centroid longitude |
| lastUpdated | DateTime | ✓ | `now()` | Last cache update |
**Indexes:**
- Unique: `postalCode`
**Relations:** None (standalone cache)
### EmailLog
**Table:** `email_logs`
**Description:** Global email audit trail (all email types).
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| recipientEmail | String | ✓ | — | Recipient email address |
| senderName | String | ✓ | — | Sender name |
| senderEmail | String | ✓ | — | Sender email address |
| subject | String | ✗ | `null` | Email subject line |
| message | String | ✗ | `null` | Email message body (long text) |
| postalCode | String | ✗ | `null` | Sender postal code |
| status | String | ✓ | `"sent"` | Status: sent, failed, previewed |
| senderIp | String | ✗ | `null` | Sender IP address |
| sentAt | DateTime | ✓ | `now()` | Send timestamp |
**Indexes:** None
**Relations:** None (audit log only)
### EmailVerification
**Table:** `email_verifications`
**Description:** Email verification tokens for response wall submissions.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| token | String | ✓ | — | Verification token (unique) |
| email | String | ✓ | — | Email address to verify |
| tempCampaignData | String | ✗ | `null` | Temporary campaign data JSON (long text) |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| expiresAt | DateTime | ✓ | — | Token expiration timestamp |
| used | Boolean | ✓ | `false` | Token used flag |
**Indexes:**
- Unique: `token`
**Relations:** None (standalone)
### Call
**Table:** `calls`
**Description:** Phone call tracking for advocacy campaigns.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| representativeName | String | ✓ | — | Representative name |
| representativeTitle | String | ✗ | `null` | Representative title |
| phoneNumber | String | ✓ | — | Phone number called |
| officeType | String | ✗ | `null` | Office type (constituency, legislative, etc.) |
| callerName | String | ✗ | `null` | Caller name |
| callerEmail | String | ✗ | `null` | Caller email |
| postalCode | String | ✗ | `null` | Caller postal code |
| campaignId | String | ✗ | `null` | Foreign key to Campaign |
| campaignSlug | String | ✗ | `null` | Denormalized campaign slug |
| callerIp | String | ✗ | `null` | Caller IP address |
| calledAt | DateTime | ✓ | `now()` | Call timestamp |
**Indexes:**
- Foreign key: `campaignId`
**Relations:**
- campaign → Campaign (onDelete: SetNull)
---
## Map — Locations
### Location
**Table:** `locations`
**Description:** Building-level address data with geocoding and NAR integration.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| latitude | Decimal(10,8) | ✓ | — | Latitude coordinate (required) |
| longitude | Decimal(11,8) | ✓ | — | Longitude coordinate (required) |
| address | String | ✓ | — | Base street address (no unit number) |
| postalCode | String | ✗ | `null` | Canadian postal code |
| province | String | ✗ | `null` | Province code (e.g., "AB") |
| federalDistrict | String | ✗ | `null` | Federal electoral district name |
| buildingUse | Int | ✗ | `null` | NAR BU_USE: 1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown |
| locGuid | String | ✗ | `null` | NAR LOC_GUID (unique) |
| buildingType | BuildingType | ✓ | `SINGLE_FAMILY` | Type: SINGLE_FAMILY, MULTI_UNIT, MIXED_USE, COMMERCIAL |
| totalUnits | Int | ✓ | `1` | Total units in building |
| buildingNotes | String | ✗ | `null` | Access codes, manager contact (long text) |
| geocodeConfidence | Int | ✗ | `null` | Geocoding confidence (0-100) |
| geocodeProvider | GeocodeProvider | ✗ | `null` | Provider: GOOGLE, MAPBOX, NOMINATIM, PHOTON, LOCATIONIQ, ARCGIS, UNKNOWN |
| createdByUserId | String | ✗ | `null` | Foreign key to User (creator) |
| updatedByUserId | String | ✗ | `null` | Foreign key to User (last updater) |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Unique: `locGuid`
- Composite: `[latitude, longitude]` (spatial queries)
- Non-unique: `postalCode`
**Relations:**
- createdByUser → User (onDelete: SetNull)
- updatedByUser → User (onDelete: SetNull)
- addresses → Address[]
- history → LocationHistory[]
### Address
**Table:** `addresses`
**Description:** Unit-level data with support levels and canvassing information.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| locationId | String | ✓ | — | Foreign key to Location |
| unitNumber | String | ✗ | `null` | Unit/apartment number |
| addrGuid | String | ✗ | `null` | NAR ADDR_GUID (unique) |
| firstName | String | ✗ | `null` | Occupant first name |
| lastName | String | ✗ | `null` | Occupant last name |
| email | String | ✗ | `null` | Occupant email |
| phone | String | ✗ | `null` | Occupant phone |
| supportLevel | SupportLevel | ✗ | `null` | Support level: 1, 2, 3, 4 |
| sign | Boolean | ✓ | `false` | Sign requested flag |
| signSize | String | ✗ | `null` | Sign size |
| notes | String | ✗ | `null` | Canvassing notes (long text) |
| createdByUserId | String | ✗ | `null` | Foreign key to User (creator) |
| updatedByUserId | String | ✗ | `null` | Foreign key to User (last updater) |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Unique: `addrGuid`
- Foreign key: `locationId`
- Composite: `[locationId, unitNumber]` (unit lookups)
**Relations:**
- location → Location (onDelete: Cascade)
- createdByUser → User (onDelete: SetNull)
- updatedByUser → User (onDelete: SetNull)
- canvassVisits → CanvassVisit[]
### LocationHistory
**Table:** `location_history`
**Description:** Audit trail for location changes with action types.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| locationId | String | ✓ | — | Foreign key to Location |
| userId | String | ✗ | `null` | Foreign key to User |
| action | LocationHistoryAction | ✓ | — | Action: CREATED, UPDATED, GEOCODED, BULK_GEOCODED, MOVED_ON_MAP, IMPORTED_CSV, IMPORTED_NAR |
| field | String | ✗ | `null` | Field name that changed |
| oldValue | String | ✗ | `null` | Old value (long text) |
| newValue | String | ✗ | `null` | New value (long text) |
| metadata | Json | ✗ | `null` | Provider, confidence, etc. |
| createdAt | DateTime | ✓ | `now()` | Timestamp |
**Indexes:**
- Foreign key: `locationId`
- Foreign key: `userId`
- Non-unique: `createdAt` (temporal queries)
**Relations:**
- location → Location (onDelete: Cascade)
- user → User (onDelete: SetNull)
---
## Map — Shifts & Cuts
### Shift
**Table:** `shifts`
**Description:** Volunteer shifts with scheduling and capacity tracking.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| title | String | ✓ | — | Shift title |
| description | String | ✗ | `null` | Shift description (long text) |
| date | DateTime | ✓ | — | Shift date (date only, no time) |
| startTime | String | ✓ | — | Start time (HH:MM format) |
| endTime | String | ✓ | — | End time (HH:MM format) |
| location | String | ✗ | `null` | Shift location description |
| maxVolunteers | Int | ✓ | — | Maximum volunteer capacity |
| currentVolunteers | Int | ✓ | `0` | Current signup count |
| status | ShiftStatus | ✓ | `OPEN` | Status: OPEN, FULL, CANCELLED |
| isPublic | Boolean | ✓ | `false` | Public signup allowed |
| cutId | String | ✗ | `null` | Foreign key to Cut |
| createdBy | String | ✗ | `null` | Creator user ID (string, not FK) |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Foreign key: `cutId`
**Relations:**
- cut → Cut (onDelete: SetNull)
- signups → ShiftSignup[]
- canvassVisits → CanvassVisit[]
- canvassSessions → CanvassSession[]
### ShiftSignup
**Table:** `shift_signups`
**Description:** Shift signup tracking with source attribution.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| shiftId | String | ✓ | — | Foreign key to Shift |
| shiftTitle | String | ✗ | `null` | Denormalized shift title |
| userId | String | ✗ | `null` | Foreign key to User |
| userEmail | String | ✓ | — | User email (for guest signups) |
| userName | String | ✗ | `null` | User name |
| userPhone | String | ✗ | `null` | User phone |
| signupDate | DateTime | ✓ | `now()` | Signup timestamp |
| status | SignupStatus | ✓ | `CONFIRMED` | Status: CONFIRMED, CANCELLED |
| signupSource | SignupSource | ✓ | `AUTHENTICATED` | Source: AUTHENTICATED, PUBLIC, ADMIN |
**Indexes:**
- Unique: `[shiftId, userEmail]` (prevent duplicate signups)
- Foreign key: `shiftId`
**Relations:**
- shift → Shift (onDelete: Cascade)
- user → User (onDelete: SetNull)
### Cut
**Table:** `cuts`
**Description:** GeoJSON polygon overlays for map filtering and canvassing.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| name | String | ✓ | — | Cut name |
| description | String | ✗ | `null` | Cut description (long text) |
| color | String | ✓ | `"#3388ff"` | Polygon fill color (hex) |
| opacity | Decimal(3,2) | ✓ | `0.3` | Polygon opacity (0.00-1.00) |
| category | CutCategory | ✗ | `null` | Category: CUSTOM, WARD, NEIGHBORHOOD, DISTRICT |
| isPublic | Boolean | ✓ | `false` | Public visibility flag |
| isOfficial | Boolean | ✓ | `false` | Official boundary flag |
| geojson | String | ✓ | — | GeoJSON polygon data (long text) |
| bounds | String | ✗ | `null` | Bounding box JSON (long text) |
| showLocations | Boolean | ✓ | `true` | Show locations on map |
| exportEnabled | Boolean | ✓ | `true` | Export enabled flag |
| assignedTo | String | ✗ | `null` | Assigned user ID (string, not FK) |
| filterSettings | Json | ✗ | `null` | Filter configuration object |
| lastCanvassed | DateTime | ✗ | `null` | Last canvass timestamp |
| completionPercentage | Int | ✓ | `0` | Canvass completion percentage |
| createdByUserId | String | ✗ | `null` | Foreign key to User (creator) |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:** None
**Relations:**
- createdByUser → User (onDelete: SetNull)
- shifts → Shift[]
- canvassSessions → CanvassSession[]
### MapSettings
**Table:** `map_settings`
**Description:** Singleton for map center/zoom and walk sheet configuration.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key (always "default") |
| latitude | Decimal(10,8) | ✗ | `null` | Map center latitude |
| longitude | Decimal(11,8) | ✗ | `null` | Map center longitude |
| zoom | Int | ✗ | `null` | Default map zoom level |
| walkSheetTitle | String | ✗ | `null` | Walk sheet header title |
| walkSheetSubtitle | String | ✗ | `null` | Walk sheet header subtitle |
| walkSheetFooter | String | ✗ | `null` | Walk sheet footer text (long text) |
| qrCode1Url | String | ✗ | `null` | QR code 1 URL |
| qrCode1Label | String | ✗ | `null` | QR code 1 label |
| qrCode2Url | String | ✗ | `null` | QR code 2 URL |
| qrCode2Label | String | ✗ | `null` | QR code 2 label |
| qrCode3Url | String | ✗ | `null` | QR code 3 URL |
| qrCode3Label | String | ✗ | `null` | QR code 3 label |
| createdBy | String | ✗ | `null` | Creator user ID (string, not FK) |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:** None
**Relations:** None (singleton)
---
## Canvassing
### CanvassSession
**Table:** `canvass_sessions`
**Description:** Canvassing session lifecycle with status tracking.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| userId | String | ✓ | — | Foreign key to User |
| cutId | String | ✓ | — | Foreign key to Cut |
| shiftId | String | ✗ | `null` | Foreign key to Shift |
| status | CanvassSessionStatus | ✓ | `ACTIVE` | Status: ACTIVE, COMPLETED, ABANDONED |
| startedAt | DateTime | ✓ | `now()` | Session start timestamp |
| endedAt | DateTime | ✗ | `null` | Session end timestamp |
| startLatitude | Decimal(10,8) | ✗ | `null` | Starting latitude |
| startLongitude | Decimal(11,8) | ✗ | `null` | Starting longitude |
**Indexes:**
- Foreign key: `userId`
- Foreign key: `cutId`
- Foreign key: `shiftId`
**Relations:**
- user → User (onDelete: Cascade)
- cut → Cut (onDelete: Cascade)
- shift → Shift (onDelete: SetNull)
- visits → CanvassVisit[]
- trackingSession → TrackingSession (one-to-one)
### CanvassVisit
**Table:** `canvass_visits`
**Description:** Visit recording with outcome tracking.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| addressId | String | ✓ | — | Foreign key to Address |
| userId | String | ✓ | — | Foreign key to User |
| shiftId | String | ✗ | `null` | Foreign key to Shift |
| sessionId | String | ✗ | `null` | Foreign key to CanvassSession |
| outcome | VisitOutcome | ✓ | — | Outcome: NOT_HOME, REFUSED, MOVED, ALREADY_VOTED, SPOKE_WITH, LEFT_LITERATURE, COME_BACK_LATER |
| supportLevel | SupportLevel | ✗ | `null` | Support level: 1, 2, 3, 4 |
| signRequested | Boolean | ✓ | `false` | Sign requested flag |
| signSize | String | ✗ | `null` | Sign size |
| notes | String | ✗ | `null` | Visit notes (long text) |
| durationSeconds | Int | ✗ | `null` | Visit duration in seconds |
| visitedAt | DateTime | ✓ | `now()` | Visit timestamp |
**Indexes:**
- Foreign key: `addressId`
- Foreign key: `userId`
- Foreign key: `shiftId`
- Foreign key: `sessionId`
- Non-unique: `visitedAt` (temporal queries)
**Relations:**
- address → Address (onDelete: Cascade)
- user → User (onDelete: Cascade)
- shift → Shift (onDelete: SetNull)
- session → CanvassSession (onDelete: SetNull)
### TrackingSession
**Table:** `tracking_sessions`
**Description:** GPS tracking sessions with distance calculation.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| userId | String | ✓ | — | Foreign key to User |
| canvassSessionId | String | ✗ | `null` | Foreign key to CanvassSession (unique, one-to-one) |
| startedAt | DateTime | ✓ | `now()` | Tracking start timestamp |
| endedAt | DateTime | ✗ | `null` | Tracking end timestamp |
| isActive | Boolean | ✓ | `true` | Active tracking flag |
| totalPoints | Int | ✓ | `0` | Total GPS points recorded |
| totalDistanceM | Float | ✓ | `0` | Total distance in meters |
| lastLatitude | Decimal(10,8) | ✗ | `null` | Last recorded latitude |
| lastLongitude | Decimal(11,8) | ✗ | `null` | Last recorded longitude |
| lastRecordedAt | DateTime | ✗ | `null` | Last GPS point timestamp |
**Indexes:**
- Unique: `canvassSessionId`
- Foreign key: `userId`
- Non-unique: `isActive`
- Composite: `[isActive, lastRecordedAt]` (cleanup queries)
**Relations:**
- user → User (onDelete: Cascade)
- canvassSession → CanvassSession (onDelete: SetNull)
- trackPoints → TrackPoint[]
### TrackPoint
**Table:** `track_points`
**Description:** GPS breadcrumb trail with event types.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| trackingSessionId | String | ✓ | — | Foreign key to TrackingSession |
| latitude | Decimal(10,8) | ✓ | — | GPS latitude |
| longitude | Decimal(11,8) | ✓ | — | GPS longitude |
| accuracy | Float | ✗ | `null` | GPS accuracy in meters |
| recordedAt | DateTime | ✓ | `now()` | GPS point timestamp |
| eventType | TrackPointEvent | ✗ | `null` | Event: LOCATION_ADDED, VISIT_RECORDED, SESSION_STARTED, SESSION_ENDED |
**Indexes:**
- Composite: `[trackingSessionId, recordedAt]` (temporal queries)
- Non-unique: `recordedAt`
**Relations:**
- trackingSession → TrackingSession (onDelete: Cascade)
---
## Email Templates
### EmailTemplate
**Table:** `email_templates`
**Description:** Email template master records with category organization.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| key | String | ✓ | — | Template key (unique, e.g., "campaign-email") |
| name | String | ✓ | — | Display name |
| description | String | ✗ | `null` | Template description (long text) |
| category | EmailTemplateCategory | ✓ | — | Category: INFLUENCE, MAP, SYSTEM |
| subjectLine | String | ✓ | — | Subject line with {{VAR}} support |
| htmlContent | String | ✓ | — | HTML template (long text) |
| textContent | String | ✓ | — | Plain text template (long text) |
| isSystem | Boolean | ✓ | `false` | System template (prevent deletion) |
| isActive | Boolean | ✓ | `true` | Active status |
| createdByUserId | String | ✓ | — | Foreign key to User (creator) |
| updatedByUserId | String | ✗ | `null` | Foreign key to User (last updater) |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Unique: `key`
- Non-unique: `category`
- Non-unique: `isActive`
**Relations:**
- createdBy → User
- updatedBy → User
- variables → EmailTemplateVariable[]
- versions → EmailTemplateVersion[]
- testLogs → EmailTemplateTestLog[]
### EmailTemplateVariable
**Table:** `email_template_variables`
**Description:** Template variable definitions with validation.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| templateId | String | ✓ | — | Foreign key to EmailTemplate |
| key | String | ✓ | — | Variable key (e.g., "USER_NAME") |
| label | String | ✓ | — | Variable label (e.g., "User Name") |
| description | String | ✗ | `null` | Variable description (long text) |
| isRequired | Boolean | ✓ | `true` | Required flag |
| isConditional | Boolean | ✓ | `false` | Conditional variable (used in {{#if}}) |
| sampleValue | String | ✗ | `null` | Sample value for testing (long text) |
| sortOrder | Int | ✓ | `0` | Display order |
**Indexes:**
- Unique: `[templateId, key]` (unique variable keys per template)
- Foreign key: `templateId`
**Relations:**
- template → EmailTemplate (onDelete: Cascade)
### EmailTemplateVersion
**Table:** `email_template_versions`
**Description:** Template version history with auto-increment version numbers.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| templateId | String | ✓ | — | Foreign key to EmailTemplate |
| versionNumber | Int | ✓ | — | Auto-increment version number per template |
| subjectLine | String | ✓ | — | Subject line snapshot |
| htmlContent | String | ✓ | — | HTML content snapshot (long text) |
| textContent | String | ✓ | — | Plain text snapshot (long text) |
| changeNotes | String | ✗ | `null` | Version notes (long text) |
| createdByUserId | String | ✓ | — | Foreign key to User |
| createdAt | DateTime | ✓ | `now()` | Version timestamp |
**Indexes:**
- Unique: `[templateId, versionNumber]` (sequential version numbers)
- Composite: `[templateId, createdAt(sort: Desc)]` (recent versions)
**Relations:**
- template → EmailTemplate (onDelete: Cascade)
- createdBy → User
### EmailTemplateTestLog
**Table:** `email_template_test_logs`
**Description:** Test email audit logs.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| templateId | String | ✓ | — | Foreign key to EmailTemplate |
| recipientEmail | String | ✓ | — | Test recipient email |
| testData | Json | ✓ | — | Sample variable values JSON |
| success | Boolean | ✓ | — | Test success flag |
| errorMessage | String | ✗ | `null` | Error message (long text) |
| messageId | String | ✗ | `null` | Nodemailer message ID |
| sentByUserId | String | ✓ | — | Foreign key to User |
| sentAt | DateTime | ✓ | `now()` | Send timestamp |
**Indexes:**
- Composite: `[templateId, sentAt(sort: Desc)]` (recent tests)
**Relations:**
- template → EmailTemplate (onDelete: Cascade)
- sentBy → User
---
## Landing Pages
### LandingPage
**Table:** `landing_pages`
**Description:** GrapesJS editor output with MkDocs export support.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| slug | String | ✓ | — | URL slug (unique) |
| title | String | ✓ | — | Page title |
| description | String | ✗ | `null` | Page description (long text) |
| blocks | Json | ✓ | — | GrapesJS editor JSON |
| htmlOutput | String | ✗ | `null` | Rendered HTML (long text) |
| cssOutput | String | ✗ | `null` | Rendered CSS (long text) |
| editorMode | EditorMode | ✓ | `VISUAL` | Editor mode: VISUAL, CODE |
| mkdocsPath | String | ✗ | `null` | Path in mkdocs/overrides/ |
| mkdocsStubPath | String | ✗ | `null` | Path to .md stub in mkdocs/docs/ |
| mkdocsExportMode | MkdocsExportMode | ✓ | `THEMED` | Export mode: THEMED, STANDALONE |
| mkdocsHideNav | Boolean | ✓ | `true` | Hide navigation in MkDocs |
| mkdocsHideToc | Boolean | ✓ | `true` | Hide table of contents in MkDocs |
| mkdocsSkipExport | Boolean | ✓ | `false` | Skip MkDocs export flag |
| published | Boolean | ✓ | `false` | Published status |
| seoTitle | String | ✗ | `null` | SEO title override |
| seoDescription | String | ✗ | `null` | SEO description (long text) |
| seoImage | String | ✗ | `null` | SEO image URL |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:**
- Unique: `slug`
**Relations:** None
### PageBlock
**Table:** `page_blocks`
**Description:** Reusable block library for GrapesJS editor.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key |
| type | String | ✓ | — | Block type (hero, text, image, cta, features, testimonials, form) |
| label | String | ✓ | — | Block label |
| schema | Json | ✓ | — | Block configuration schema JSON |
| defaults | Json | ✓ | — | Default values JSON |
| thumbnail | String | ✗ | `null` | Thumbnail URL |
| category | String | ✗ | `null` | Block category |
| sortOrder | Int | ✓ | `0` | Display order |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:** None
**Relations:** None
---
## Site Settings
### SiteSettings
**Table:** `site_settings`
**Description:** Global site configuration singleton for branding, theme, SMTP, and feature toggles.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | String | ✓ | `cuid()` | Primary key (always "default") |
| organizationName | String | ✓ | `"Changemaker Lite"` | Organization name |
| organizationShortName | String | ✓ | `"CML"` | Short name/acronym |
| organizationLogoUrl | String | ✗ | `null` | Logo URL |
| organizationFaviconUrl | String | ✗ | `null` | Favicon URL |
| adminColorPrimary | String | ✓ | `"#9d4edd"` | Admin primary color (hex) |
| adminColorBgBase | String | ✓ | `"#1a1025"` | Admin background color (hex) |
| publicColorPrimary | String | ✓ | `"#3498db"` | Public primary color (hex) |
| publicColorBgBase | String | ✓ | `"#0d1b2a"` | Public background color (hex) |
| publicColorBgContainer | String | ✓ | `"#1b2838"` | Public container color (hex) |
| publicHeaderGradient | String | ✓ | `"linear-gradient(135deg, #005a9c 0%, #007acc 100%)"` | Public header gradient (CSS) |
| footerText | String | ✓ | `"Powered by Changemaker Lite"` | Footer text |
| loginSubtitle | String | ✓ | `"Admin"` | Login page subtitle |
| emailFromName | String | ✓ | `"Changemaker Lite"` | Email from name |
| smtpHost | String | ✓ | `""` | SMTP host (empty = use env) |
| smtpPort | Int | ✓ | `0` | SMTP port (0 = use env) |
| smtpUser | String | ✓ | `""` | SMTP username (empty = use env) |
| smtpPass | String | ✓ | `""` | SMTP password (empty = use env) |
| smtpFromAddress | String | ✓ | `""` | SMTP from address (empty = use env) |
| smtpActiveProvider | String | ✓ | `"mailhog"` | Active provider: "mailhog", "production" |
| emailTestMode | Boolean | ✓ | `true` | Email test mode flag |
| testEmailRecipient | String | ✓ | `""` | Test email recipient |
| enableInfluence | Boolean | ✓ | `true` | Enable Influence module |
| enableMap | Boolean | ✓ | `true` | Enable Map module |
| enableNewsletter | Boolean | ✓ | `true` | Enable Newsletter module |
| enableLandingPages | Boolean | ✓ | `true` | Enable Landing Pages module |
| createdAt | DateTime | ✓ | `now()` | Creation timestamp |
| updatedAt | DateTime | ✓ | Auto | Last update timestamp |
**Indexes:** None
**Relations:** None (singleton)
---
## Media (Drizzle ORM)
### videos
**Table:** `videos`
**Description:** Video library with metadata extraction and engagement tracking.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | serial | ✓ | Auto | Primary key (auto-increment) |
| path | text | ✓ | — | File path (unique) |
| filename | text | ✓ | — | File name |
| producer | text | ✗ | `null` | Producer name |
| creator | text | ✗ | `null` | Creator name |
| title | text | ✗ | `null` | Video title |
| durationSeconds | integer | ✗ | `null` | Duration in seconds (FFprobe) |
| quality | text | ✗ | `null` | Quality string (e.g., "1080p") |
| orientation | text | ✗ | `null` | Orientation: portrait, landscape, square |
| hasAudio | boolean | ✓ | `true` | Audio track present flag |
| fileSize | bigint | ✗ | `null` | File size in bytes |
| fileHash | text | ✗ | `null` | MD5 hash |
| width | integer | ✗ | `null` | Video width (FFprobe) |
| height | integer | ✗ | `null` | Video height (FFprobe) |
| lastValidated | timestamp | ✗ | `null` | Last validation timestamp |
| isValid | boolean | ✓ | `true` | Valid file flag |
| thumbnailPath | text | ✗ | `null` | Thumbnail file path |
| createdAt | timestamp | ✓ | `now()` | Creation timestamp |
| tags | jsonb | ✗ | `null` | Array of tag strings |
| directoryType | text | ✗ | `null` | Directory type: studios, gifs, private, inbox, curated, playback, compilations, videos, highlights |
| publicViewCount | integer | ✗ | `null` | Public view count (historical) |
| publicUpvoteCount | integer | ✗ | `null` | Public upvote count (historical) |
| publicCommentCount | integer | ✗ | `null` | Public comment count (historical) |
| publicCompletionCount | integer | ✗ | `null` | Public completion count (historical) |
| publicTotalWatchTime | integer | ✗ | `null` | Public total watch time (historical) |
| movedFromPublicAt | timestamp | ✗ | `null` | Timestamp when moved from public media |
| originalFilename | text | ✗ | `null` | Original filename before standardization |
| originalPath | text | ✗ | `null` | Original path before standardization |
| standardizedAt | timestamp | ✗ | `null` | Standardization timestamp |
**Indexes:**
- Unique: `path`
- Non-unique: `orientation`
- Non-unique: `producer`
- Non-unique: `isValid`
- Non-unique: `directoryType`
- Composite: `[durationSeconds, fileSize, width, height]` (fingerprint)
- Composite: `[directoryType, isValid, orientation]` (common filtering)
**Relations:** None (standalone)
### compilations
**Table:** `compilations`
**Description:** Video compilation tracking.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | serial | ✓ | Auto | Primary key (auto-increment) |
| filename | text | ✓ | — | Compilation filename |
| path | text | ✗ | `null` | Compilation file path |
| durationSeconds | integer | ✗ | `null` | Total duration in seconds |
| videoIds | jsonb | ✗ | `null` | Array of video IDs included |
| settings | jsonb | ✗ | `null` | Compilation settings object |
| createdAt | timestamp | ✓ | `now()` | Creation timestamp |
**Indexes:** None
**Relations:** None (video IDs stored as JSON array)
### jobs
**Table:** `jobs`
**Description:** Job queue with resource category management.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| id | serial | ✓ | Auto | Primary key (auto-increment) |
| type | text | ✓ | — | Job type (compilation, scan, organize, etc.) |
| status | text | ✓ | `"pending"` | Status: pending, queued, running, completed, failed, cancelled |
| progress | integer | ✓ | `0` | Progress percentage (0-100) |
| log | text | ✗ | `null` | Job log output |
| params | jsonb | ✗ | `null` | Job parameters object |
| startedAt | timestamp | ✗ | `null` | Job start timestamp |
| completedAt | timestamp | ✗ | `null` | Job completion timestamp |
| createdAt | timestamp | ✓ | `now()` | Creation timestamp |
| resourceCategory | text | ✓ | `"cpu"` | Resource category: gpu_ai, gpu_encode, cpu |
| vramRequired | integer | ✓ | `0` | VRAM required in MB |
| queuePosition | integer | ✗ | `null` | Queue position |
| waitingReason | text | ✗ | `null` | Reason for waiting |
| priority | integer | ✓ | `5` | Job priority (lower = higher priority) |
| pipelineId | integer | ✗ | `null` | Pipeline ID (for pipeline jobs) |
| pipelineStepId | integer | ✗ | `null` | Pipeline step ID |
**Indexes:**
- Composite: `[status, priority, createdAt]` (queue processing)
- Composite: `[resourceCategory, status]` (resource filtering)
- Non-unique: `pipelineId`
**Relations:** None (pipeline relations are external)
---
## Related Documentation
- [Database Overview](./index.md) — Complete ER diagram and architecture
- [Migration Workflow](./migrations.md) — Prisma and Drizzle migration processes
- [Seeding](./seeding.md) — Default data and seed script
- [Indexes](./indexes.md) — Index strategy and performance
- [Auth Models](./models/auth.md) — User and authentication models
- [Influence Models](./models/influence.md) — Campaign and advocacy models
- [Map Models](./models/map.md) — Location, shift, and cut models
- [Canvassing Models](./models/canvass.md) — Session and visit tracking
- [Email Template Models](./models/email-templates.md) — Template system models
- [Landing Page Models](./models/pages.md) — Page builder models
- [Settings Models](./models/settings.md) — Site and map settings
- [Media Models](./models/media.md) — Video library models (Drizzle)