29 KiB

Database Documentation

Overview

Changemaker Lite V2 uses a dual ORM architecture with PostgreSQL 16 as the backing database:

  • Prisma ORM (Express API, port 4000) — 30 models for auth, influence, map, canvassing, email templates, landing pages, and tracking
  • Drizzle ORM (Fastify Media API, port 4100) — 3 models for video library, compilations, and job queue

Both ORMs share the same PostgreSQL database but maintain separate schemas and migration workflows.

Database Architecture

Database: PostgreSQL 16 Connection: DATABASE_URL environment variable Total Models: 33 models organized into 9 groups Migration Tools: Prisma Migrate (main API), Drizzle Kit (media API)

Key Design Patterns

  1. Audit Fields — Most models include:

    • createdAt / updatedAt timestamps
    • createdByUserId / updatedByUserId user references
    • Automatic tracking via Prisma middleware
  2. Soft Deletes — Some models use status fields instead of hard deletes:

    • User: status (ACTIVE/INACTIVE/SUSPENDED/EXPIRED)
    • Campaign: status (DRAFT/ACTIVE/PAUSED/ARCHIVED)
    • Shift: status (OPEN/FULL/CANCELLED)
  3. JSON Fields — Used for flexible schema:

    • permissions (User) — granular per-app permissions
    • offices (Representative) — array of office contact info
    • tags (videos) — array of tag strings
    • geojson (Cut) — GeoJSON polygon coordinates
    • blocks (LandingPage) — GrapesJS editor output
  4. Enums — 18 enums for type safety:

    • UserRole, UserStatus, CampaignStatus, GovernmentLevel, EmailMethod, ResponseType, ResponseStatus, SupportLevel, GeocodeProvider, BuildingType, LocationHistoryAction, ShiftStatus, SignupStatus, SignupSource, CutCategory, VisitOutcome, CanvassSessionStatus, TrackPointEvent, EmailTemplateCategory, EditorMode, MkdocsExportMode
  5. Cascade Deletes — Foreign keys with onDelete: Cascade:

    • Deleting a Campaign deletes all CampaignEmail, RepresentativeResponse, CustomRecipient, Call records
    • Deleting a Location deletes all Address and LocationHistory records
    • Deleting a Shift deletes all ShiftSignup records
    • Deleting a CanvassSession deletes all CanvassVisit records
  6. Indexes — Strategic indexing for performance:

    • All foreign keys indexed (userId, campaignId, locationId, etc.)
    • Composite indexes for common queries (latitude+longitude, locationId+unitNumber, etc.)
    • Unique constraints (email, slug, postalCode, token, etc.)

Complete Entity Relationship Diagram

erDiagram
    %% ============================================================================
    %% AUTH & USERS
    %% ============================================================================

    User ||--o{ RefreshToken : has
    User ||--o{ Campaign : creates
    User ||--o{ CampaignEmail : sends
    User ||--o{ RepresentativeResponse : submits
    User ||--o{ ResponseUpvote : upvotes
    User ||--o{ ShiftSignup : "signs up for"
    User ||--o{ Location : creates
    User ||--o{ Location : updates
    User ||--o{ Address : "creates (addresses)"
    User ||--o{ Address : "updates (addresses)"
    User ||--o{ LocationHistory : edits
    User ||--o{ Cut : "creates (cuts)"
    User ||--o{ CanvassVisit : visits
    User ||--o{ CanvassSession : "has (sessions)"
    User ||--o{ TrackingSession : "tracks (gps)"
    User ||--o{ EmailTemplate : "creates (templates)"
    User ||--o{ EmailTemplate : "updates (templates)"
    User ||--o{ EmailTemplateVersion : "versions (templates)"
    User ||--o{ EmailTemplateTestLog : "tests (templates)"

    User {
        String id PK
        String email UK "bcrypt hashed"
        String password "bcrypt"
        String name
        String phone
        UserRole role "SUPER_ADMIN | INFLUENCE_ADMIN | MAP_ADMIN | USER | TEMP"
        UserStatus status "ACTIVE | INACTIVE | SUSPENDED | EXPIRED"
        Json permissions "granular per-app"
        UserCreatedVia createdVia "ADMIN | PUBLIC_SHIFT_SIGNUP | STANDARD"
        DateTime expiresAt "for TEMP users"
        Int expireDays
        DateTime lastLoginAt
        Boolean emailVerified
        DateTime createdAt
        DateTime updatedAt
    }

    RefreshToken {
        String id PK
        String token UK "JWT refresh token"
        String userId FK
        DateTime expiresAt
        DateTime createdAt
    }

    %% ============================================================================
    %% INFLUENCE — CAMPAIGNS
    %% ============================================================================

    Campaign ||--o{ CampaignEmail : sends
    Campaign ||--o{ RepresentativeResponse : receives
    Campaign ||--o{ CustomRecipient : targets
    Campaign ||--o{ Call : tracks

    Campaign {
        String id PK
        String slug UK
        String title
        String description
        String emailSubject
        String emailBody
        String callToAction
        String coverPhoto
        CampaignStatus status "DRAFT | ACTIVE | PAUSED | ARCHIVED"
        Boolean allowSmtpEmail "default: true"
        Boolean allowMailtoLink "default: true"
        Boolean collectUserInfo "default: true"
        Boolean showEmailCount "default: true"
        Boolean showCallCount "default: true"
        Boolean allowEmailEditing "default: false"
        Boolean allowCustomRecipients "default: false"
        Boolean showResponseWall "default: false"
        Boolean highlightCampaign "default: false"
        GovernmentLevel[] targetGovernmentLevels
        String createdByUserId FK
        String createdByUserEmail
        String createdByUserName
        DateTime createdAt
        DateTime updatedAt
    }

    CampaignEmail {
        String id PK
        String campaignId FK
        String campaignSlug
        String userId FK
        String userEmail
        String userName
        String userPostalCode
        String recipientEmail
        String recipientName
        String recipientTitle
        GovernmentLevel recipientLevel
        EmailMethod emailMethod "SMTP | MAILTO"
        String subject
        String message
        CampaignEmailStatus status "QUEUED | SENT | FAILED | CLICKED | USER_INFO_CAPTURED"
        String senderIp
        DateTime sentAt
    }

    Representative {
        String id PK
        String postalCode IDX
        String name
        String email
        String districtName
        String electedOffice
        String partyName
        String representativeSetName
        String url
        String photoUrl
        Json offices "array of office contact info"
        DateTime cachedAt
    }

    CustomRecipient {
        String id PK
        String campaignId FK
        String campaignSlug
        String recipientName
        String recipientEmail
        String recipientTitle
        String recipientOrganization
        String notes
        Boolean isActive
        DateTime createdAt
        DateTime updatedAt
    }

    PostalCodeCache {
        String id PK
        String postalCode UK
        String city
        String province
        Decimal centroidLat
        Decimal centroidLng
        DateTime lastUpdated
    }

    Call {
        String id PK
        String representativeName
        String representativeTitle
        String phoneNumber
        String officeType
        String callerName
        String callerEmail
        String postalCode
        String campaignId FK
        String campaignSlug
        String callerIp
        DateTime calledAt
    }

    %% ============================================================================
    %% INFLUENCE — RESPONSE WALL
    %% ============================================================================

    RepresentativeResponse ||--o{ ResponseUpvote : gets

    RepresentativeResponse {
        String id PK
        String campaignId FK
        String campaignSlug
        String representativeName
        String representativeTitle
        GovernmentLevel representativeLevel
        String representativeEmail
        ResponseType responseType "EMAIL | LETTER | PHONE_CALL | MEETING | SOCIAL_MEDIA | OTHER"
        String responseText
        String userComment
        String screenshotUrl
        String submittedByUserId FK
        String submittedByName
        String submittedByEmail
        Boolean isAnonymous
        ResponseStatus status "PENDING | APPROVED | REJECTED"
        Boolean isVerified
        String verificationToken
        DateTime verificationSentAt
        DateTime verifiedAt
        String verifiedBy
        Int upvoteCount
        String submittedIp
        DateTime createdAt
        DateTime updatedAt
    }

    ResponseUpvote {
        String id PK
        String responseId FK
        String userId FK
        String userEmail
        String upvotedIp
    }

    EmailLog {
        String id PK
        String recipientEmail
        String senderName
        String senderEmail
        String subject
        String message
        String postalCode
        String status "sent | failed | previewed"
        String senderIp
        DateTime sentAt
    }

    EmailVerification {
        String id PK
        String token UK
        String email
        String tempCampaignData "JSON"
        DateTime createdAt
        DateTime expiresAt
        Boolean used
    }

    %% ============================================================================
    %% MAP — LOCATIONS
    %% ============================================================================

    Location ||--o{ Address : contains
    Location ||--o{ LocationHistory : logs

    Location {
        String id PK
        Decimal latitude "required, precision: 10,8"
        Decimal longitude "required, precision: 11,8"
        String address "base street address, no unit"
        String postalCode
        String province
        String federalDistrict
        Int buildingUse "NAR BU_USE: 1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown"
        String locGuid UK "NAR LOC_GUID"
        BuildingType buildingType "SINGLE_FAMILY | MULTI_UNIT | MIXED_USE | COMMERCIAL"
        Int totalUnits
        String buildingNotes "access codes, manager contact"
        Int geocodeConfidence "0-100"
        GeocodeProvider geocodeProvider
        String createdByUserId FK
        String updatedByUserId FK
        DateTime createdAt
        DateTime updatedAt
    }

    Address {
        String id PK
        String locationId FK
        String unitNumber
        String addrGuid UK "NAR ADDR_GUID"
        String firstName
        String lastName
        String email
        String phone
        SupportLevel supportLevel "1 | 2 | 3 | 4"
        Boolean sign
        String signSize
        String notes
        String createdByUserId FK
        String updatedByUserId FK
        DateTime createdAt
        DateTime updatedAt
    }

    LocationHistory {
        String id PK
        String locationId FK
        String userId FK
        LocationHistoryAction action "CREATED | UPDATED | GEOCODED | BULK_GEOCODED | MOVED_ON_MAP | IMPORTED_CSV | IMPORTED_NAR"
        String field "which field changed"
        String oldValue
        String newValue
        Json metadata "provider, confidence, etc"
        DateTime createdAt
    }

    %% ============================================================================
    %% MAP — SHIFTS & CUTS
    %% ============================================================================

    Cut ||--o{ Shift : schedules
    Shift ||--o{ ShiftSignup : has
    Shift ||--o{ CanvassVisit : "visits (shift)"
    Shift ||--o{ CanvassSession : "sessions (shift)"

    Shift {
        String id PK
        String title
        String description
        DateTime date
        String startTime "HH:MM"
        String endTime "HH:MM"
        String location
        Int maxVolunteers
        Int currentVolunteers
        ShiftStatus status "OPEN | FULL | CANCELLED"
        Boolean isPublic
        String cutId FK
        String createdBy
        DateTime createdAt
        DateTime updatedAt
    }

    ShiftSignup {
        String id PK
        String shiftId FK
        String shiftTitle
        String userId FK
        String userEmail
        String userName
        String userPhone
        DateTime signupDate
        SignupStatus status "CONFIRMED | CANCELLED"
        SignupSource signupSource "AUTHENTICATED | PUBLIC | ADMIN"
    }

    Cut {
        String id PK
        String name
        String description
        String color
        Decimal opacity
        CutCategory category "CUSTOM | WARD | NEIGHBORHOOD | DISTRICT"
        Boolean isPublic
        Boolean isOfficial
        String geojson "GeoJSON polygon data"
        String bounds "bounding box JSON"
        Boolean showLocations
        Boolean exportEnabled
        String assignedTo
        Json filterSettings
        DateTime lastCanvassed
        Int completionPercentage
        String createdByUserId FK
        DateTime createdAt
        DateTime updatedAt
    }

    MapSettings {
        String id PK
        Decimal latitude
        Decimal longitude
        Int zoom
        String walkSheetTitle
        String walkSheetSubtitle
        String walkSheetFooter
        String qrCode1Url
        String qrCode1Label
        String qrCode2Url
        String qrCode2Label
        String qrCode3Url
        String qrCode3Label
        String createdBy
        DateTime createdAt
        DateTime updatedAt
    }

    %% ============================================================================
    %% CANVASSING
    %% ============================================================================

    Cut ||--o{ CanvassSession : "sessions (cut)"
    CanvassSession ||--o{ CanvassVisit : records
    CanvassSession ||--|| TrackingSession : tracks
    Address ||--o{ CanvassVisit : "visited (address)"

    CanvassSession {
        String id PK
        String userId FK
        String cutId FK
        String shiftId FK
        CanvassSessionStatus status "ACTIVE | COMPLETED | ABANDONED"
        DateTime startedAt
        DateTime endedAt
        Decimal startLatitude
        Decimal startLongitude
    }

    CanvassVisit {
        String id PK
        String addressId FK
        String userId FK
        String shiftId FK
        String sessionId FK
        VisitOutcome outcome "NOT_HOME | REFUSED | MOVED | ALREADY_VOTED | SPOKE_WITH | LEFT_LITERATURE | COME_BACK_LATER"
        SupportLevel supportLevel
        Boolean signRequested
        String signSize
        String notes
        Int durationSeconds
        DateTime visitedAt
    }

    TrackingSession {
        String id PK
        String userId FK
        String canvassSessionId UK
        DateTime startedAt
        DateTime endedAt
        Boolean isActive
        Int totalPoints
        Float totalDistanceM
        Decimal lastLatitude
        Decimal lastLongitude
        DateTime lastRecordedAt
    }

    TrackingSession ||--o{ TrackPoint : logs

    TrackPoint {
        String id PK
        String trackingSessionId FK
        Decimal latitude
        Decimal longitude
        Float accuracy
        DateTime recordedAt
        TrackPointEvent eventType "LOCATION_ADDED | VISIT_RECORDED | SESSION_STARTED | SESSION_ENDED"
    }

    %% ============================================================================
    %% EMAIL TEMPLATES
    %% ============================================================================

    EmailTemplate ||--o{ EmailTemplateVariable : defines
    EmailTemplate ||--o{ EmailTemplateVersion : versions
    EmailTemplate ||--o{ EmailTemplateTestLog : tests

    EmailTemplate {
        String id PK
        String key UK "e.g., campaign-email"
        String name "display name"
        String description
        EmailTemplateCategory category "INFLUENCE | MAP | SYSTEM"
        String subjectLine "with {{VAR}} support"
        String htmlContent
        String textContent
        Boolean isSystem "prevent deletion"
        Boolean isActive
        String createdByUserId FK
        String updatedByUserId FK
        DateTime createdAt
        DateTime updatedAt
    }

    EmailTemplateVariable {
        String id PK
        String templateId FK
        String key "e.g., USER_NAME"
        String label "e.g., User Name"
        String description
        Boolean isRequired
        Boolean isConditional "used in {{#if}} blocks"
        String sampleValue
        Int sortOrder
    }

    EmailTemplateVersion {
        String id PK
        String templateId FK
        Int versionNumber "auto-increment per template"
        String subjectLine
        String htmlContent
        String textContent
        String changeNotes
        String createdByUserId FK
        DateTime createdAt
    }

    EmailTemplateTestLog {
        String id PK
        String templateId FK
        String recipientEmail
        Json testData "sample variable values"
        Boolean success
        String errorMessage
        String messageId "nodemailer message ID"
        String sentByUserId FK
        DateTime sentAt
    }

    %% ============================================================================
    %% LANDING PAGES
    %% ============================================================================

    LandingPage {
        String id PK
        String slug UK
        String title
        String description
        Json blocks "GrapesJS editor JSON"
        String htmlOutput
        String cssOutput
        EditorMode editorMode "VISUAL | CODE"
        String mkdocsPath "path in mkdocs/overrides/"
        String mkdocsStubPath "path to .md stub"
        MkdocsExportMode mkdocsExportMode "THEMED | STANDALONE"
        Boolean mkdocsHideNav
        Boolean mkdocsHideToc
        Boolean mkdocsSkipExport
        Boolean published
        String seoTitle
        String seoDescription
        String seoImage
        DateTime createdAt
        DateTime updatedAt
    }

    PageBlock {
        String id PK
        String type "hero | text | image | cta | features | testimonials | form"
        String label
        Json schema "block configuration schema"
        Json defaults "default values"
        String thumbnail
        String category
        Int sortOrder
        DateTime createdAt
        DateTime updatedAt
    }

    %% ============================================================================
    %% SITE SETTINGS
    %% ============================================================================

    SiteSettings {
        String id PK
        String organizationName
        String organizationShortName
        String organizationLogoUrl
        String organizationFaviconUrl
        String adminColorPrimary
        String adminColorBgBase
        String publicColorPrimary
        String publicColorBgBase
        String publicColorBgContainer
        String publicHeaderGradient
        String footerText
        String loginSubtitle
        String emailFromName
        String smtpHost
        Int smtpPort
        String smtpUser
        String smtpPass
        String smtpFromAddress
        String smtpActiveProvider "mailhog | production"
        Boolean emailTestMode
        String testEmailRecipient
        Boolean enableInfluence
        Boolean enableMap
        Boolean enableNewsletter
        Boolean enableLandingPages
        DateTime createdAt
        DateTime updatedAt
    }

Model Groups

The database is organized into 9 logical groups:

1. Auth & Users

  • User — User accounts with roles (SUPER_ADMIN, INFLUENCE_ADMIN, MAP_ADMIN, USER, TEMP)
  • RefreshToken — JWT refresh token storage with rotation

Key Features: bcrypt passwords (12+ chars policy), role-based access control, temp user expiration, email verification

2. Influence

  • Campaign — Advocacy campaigns with 12 feature flags
  • Representative — Cached representative data from Represent API
  • CampaignEmail — Email tracking (SMTP vs MAILTO)
  • RepresentativeResponse — Response wall with moderation
  • ResponseUpvote — Upvote tracking with IP + user uniqueness
  • CustomRecipient — Custom email targets
  • PostalCodeCache — Postal code geocoding cache
  • EmailLog — Email audit trail
  • EmailVerification — Verification token storage
  • Call — Phone call tracking

Key Features: Multi-government-level targeting, response moderation workflow (PENDING → APPROVED/REJECTED), BullMQ integration for email queue, upvote deduplication

3. Map — Locations

  • Location — Building-level data with lat/lng, NAR integration
  • Address — Unit-level data with support levels
  • LocationHistory — Audit trail with 7 action types
  • Shift — Volunteer shifts with cut relation
  • ShiftSignup — Signup tracking
  • Cut — GeoJSON polygon overlays
  • MapSettings — Singleton for map center/zoom + walk sheet config

Key Features: Building vs unit architecture, multi-provider geocoding (6 providers), NAR 2025 import support, spatial indexing, GeoJSON storage

4. Canvassing

  • CanvassSession — Session lifecycle (ACTIVE → COMPLETED/ABANDONED)
  • CanvassVisit — Visit recording with 7 outcome types
  • TrackingSession — GPS tracking integration
  • TrackPoint — GPS breadcrumb trail

Key Features: Walking route algorithm, session abandonment logic (12h timeout), distance calculation, support level tracking

5. Email Templates

  • EmailTemplate — Template master with categories
  • EmailTemplateVariable — Variable definitions with validation
  • EmailTemplateVersion — Version history
  • EmailTemplateTestLog — Test email audit

Key Features: Handlebars-style variable interpolation ({{VAR}}), conditional variables, system template protection, version auto-increment

6. Landing Pages

  • LandingPage — GrapesJS editor output with MkDocs export
  • PageBlock — Reusable block library

Key Features: GrapesJS JSON storage, MkDocs export modes (THEMED vs STANDALONE), SEO metadata, slug-based routing

7. Settings

  • SiteSettings — Org branding + theme + SMTP + feature toggles
  • MapSettings — Map center/zoom + walk sheet config

Key Features: Singleton pattern, SMTP override hierarchy (SiteSettings → .env), feature flags

8. Media (Drizzle ORM)

  • videos — Video library with metadata, directory types, engagement stats
  • compilations — Video compilation tracking
  • jobs — Job queue with resource categories

Key Features: Dual ORM architecture, FFprobe metadata extraction, directory type enum (9 types), job queue with GPU/CPU resource tracking

9. Shared/Standalone Models

  • Representative — Shared across campaigns
  • PostalCodeCache — Shared geocoding cache
  • EmailLog — Audit trail (no relations)
  • EmailVerification — Standalone verification tokens

Field Types Reference

Prisma Type PostgreSQL Type Description Example
String text Variable-length text "admin@cmlite.org"
String @db.Text text Long-form text (no char limit) Campaign descriptions
Int integer 32-bit integer 42
BigInt bigint 64-bit integer (Node: number mode) File sizes
Boolean boolean True/false true
Decimal numeric Arbitrary precision decimal Lat/lng coordinates
Decimal @db.Decimal(10, 8) numeric(10, 8) 10 digits, 8 after decimal 53.54612345
DateTime timestamp with time zone Timestamp 2025-02-11T10:30:00Z
DateTime @db.Date date Date only (no time) Shift dates
Json jsonb JSON data (binary storage) Arrays, objects
Enum enum Enumerated type UserRole.SUPER_ADMIN

Enum Definitions

Auth & Users

  • UserRole: SUPER_ADMIN, INFLUENCE_ADMIN, MAP_ADMIN, USER, TEMP
  • UserStatus: ACTIVE, INACTIVE, SUSPENDED, EXPIRED
  • UserCreatedVia: ADMIN, PUBLIC_SHIFT_SIGNUP, STANDARD

Influence

  • CampaignStatus: DRAFT, ACTIVE, PAUSED, ARCHIVED
  • GovernmentLevel: FEDERAL, PROVINCIAL, MUNICIPAL, SCHOOL_BOARD
  • EmailMethod: SMTP, MAILTO
  • CampaignEmailStatus: QUEUED, SENT, FAILED, CLICKED, USER_INFO_CAPTURED
  • ResponseType: EMAIL, LETTER, PHONE_CALL, MEETING, SOCIAL_MEDIA, OTHER
  • ResponseStatus: PENDING, APPROVED, REJECTED

Map

  • SupportLevel: LEVEL_1 (mapped to "1"), LEVEL_2, LEVEL_3, LEVEL_4
  • GeocodeProvider: GOOGLE, MAPBOX, NOMINATIM, PHOTON, LOCATIONIQ, ARCGIS, UNKNOWN
  • BuildingType: SINGLE_FAMILY, MULTI_UNIT, MIXED_USE, COMMERCIAL
  • LocationHistoryAction: CREATED, UPDATED, GEOCODED, BULK_GEOCODED, MOVED_ON_MAP, IMPORTED_CSV, IMPORTED_NAR
  • ShiftStatus: OPEN, FULL, CANCELLED
  • SignupStatus: CONFIRMED, CANCELLED
  • SignupSource: AUTHENTICATED, PUBLIC, ADMIN
  • CutCategory: CUSTOM, WARD, NEIGHBORHOOD, DISTRICT

Canvassing

  • VisitOutcome: NOT_HOME, REFUSED, MOVED, ALREADY_VOTED, SPOKE_WITH, LEFT_LITERATURE, COME_BACK_LATER
  • CanvassSessionStatus: ACTIVE, COMPLETED, ABANDONED
  • TrackPointEvent: LOCATION_ADDED, VISIT_RECORDED, SESSION_STARTED, SESSION_ENDED

Email Templates

  • EmailTemplateCategory: INFLUENCE, MAP, SYSTEM

Landing Pages

  • EditorMode: VISUAL, CODE
  • MkdocsExportMode: THEMED, STANDALONE

Media (Drizzle)

  • DirectoryType (TypeScript literal): 'studios', 'gifs', 'private', 'inbox', 'curated', 'playback', 'compilations', 'videos', 'highlights'
  • ResourceCategory (TypeScript literal): 'gpu_ai', 'gpu_encode', 'cpu'
  • JobStatus (TypeScript literal): 'pending', 'queued', 'running', 'completed', 'failed', 'cancelled'

Index Strategy Overview

Foreign Key Indexes

All foreign key fields are indexed for join performance:

  • userId, campaignId, locationId, addressId, shiftId, cutId, sessionId, templateId, trackingSessionId

Composite Indexes

Strategic multi-column indexes for common query patterns:

  • [latitude, longitude] (Location) — spatial queries
  • [locationId, unitNumber] (Address) — unit lookups
  • [campaignId, status] (RepresentativeResponse) — filtered response lists
  • [isActive, lastRecordedAt] (TrackingSession) — active session cleanup
  • [templateId, createdAt(sort: Desc)] (EmailTemplateVersion) — version history
  • [directoryType, isValid, orientation] (videos) — media library filtering

Unique Constraints

Enforce data integrity:

  • email (User)
  • slug (Campaign, LandingPage)
  • postalCode (PostalCodeCache)
  • token (RefreshToken, EmailVerification)
  • key (EmailTemplate)
  • [responseId, userId] (ResponseUpvote) — prevent duplicate upvotes from logged-in users
  • [responseId, upvotedIp] (ResponseUpvote) — prevent duplicate upvotes from same IP
  • [shiftId, userEmail] (ShiftSignup) — prevent duplicate shift signups
  • [templateId, key] (EmailTemplateVariable) — unique variable keys per template
  • [templateId, versionNumber] (EmailTemplateVersion) — sequential version numbers

Foreign Key Conventions

Cascade Deletes

onDelete: Cascade

Used when child records should be deleted with parent:

  • RefreshToken → User
  • CampaignEmail → Campaign
  • RepresentativeResponse → Campaign
  • CustomRecipient → Campaign
  • Call → Campaign (SetNull)
  • Address → Location
  • LocationHistory → Location
  • ShiftSignup → Shift
  • CanvassVisit → Address, CanvassSession
  • TrackPoint → TrackingSession
  • EmailTemplateVariable → EmailTemplate
  • EmailTemplateVersion → EmailTemplate
  • EmailTemplateTestLog → EmailTemplate

Set Null

onDelete: SetNull

Used when child records should remain but orphan the reference:

  • Campaign.createdByUserId → User
  • CampaignEmail.userId → User
  • RepresentativeResponse.submittedByUserId → User
  • Location.createdByUserId/updatedByUserId → User
  • Shift.cutId → Cut
  • CanvassSession.shiftId → Shift
  • TrackingSession.canvassSessionId → CanvassSession