# 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)