6.8 KiB

Database Models

Changemaker Lite V2 uses a comprehensive PostgreSQL database schema with 30+ models across authentication, campaigns, locations, media, and content management. The schema is managed via Prisma ORM (main API) and Drizzle ORM (media API).

Model Organization

Models are organized by feature area:

Authentication & Users

Core authentication and user management:

  • User - User accounts with roles and authentication
  • RefreshToken - JWT refresh token tracking
  • Session - User session management (future)

Influence Module

Advocacy campaign models:

  • Campaign - Campaign definitions and settings
  • CampaignEmail - Sent email tracking
  • Response - Public response wall submissions
  • PostalCodeCache - Representative lookup cache

Map Module

Location and geographic models:

  • Location - Address database with geocoding
  • Cut - Geographic polygon organization
  • Shift - Volunteer shift scheduling
  • MapSettings - Map configuration singleton

Canvassing

Door-to-door canvassing models:

  • CanvassSession - Canvassing session tracking
  • CanvassVisit - Visit outcome recording
  • TrackingSession - GPS tracking (future)

Content Management

Landing pages and content:

  • Page - Landing page definitions
  • PageBlock - Reusable content blocks

Email Templates

Email template system:

  • EmailTemplate - Template definitions
  • EmailTemplateVersion - Version history (future)

Media

Video library (Drizzle ORM):

  • videos - Video metadata and files
  • shared_media - Public gallery assignments
  • media_reactions - Emoji reactions
  • media_jobs - Background job queue

Settings

Global configuration:

  • Settings - Site-wide settings singleton

ORM Architecture

Prisma (Main API)

Used for 95% of models:

  • Schema: api/prisma/schema.prisma
  • Migrations: api/prisma/migrations/
  • Client: Auto-generated TypeScript types
  • Database: PostgreSQL 16

Drizzle (Media API)

Used for media models only:

  • Schema: api/src/modules/media/db/schema.ts
  • Migrations: None (push-based)
  • Client: Manual schema definition
  • Database: Same PostgreSQL 16

Common Patterns

Timestamps

Most models include:

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

Foreign Keys

Relations use explicit foreign key fields:

model Campaign {
  id              Int     @id @default(autoincrement())
  createdByUserId Int
  createdBy       User    @relation(fields: [createdByUserId], references: [id])
}

JSON Fields

Flexible data stored as JSON:

model Campaign {
  emailTemplate Json?
  settings      Json?
}

TypeScript types:

import { Prisma } from '@prisma/client';

const template: Prisma.InputJsonValue = {
  subject: 'Email subject',
  body: 'Email body',
};

Enums

Type-safe enumerations:

enum Role {
  SUPER_ADMIN
  INFLUENCE_ADMIN
  MAP_ADMIN
  USER
  TEMP
}

enum VisitOutcome {
  SUCCESS
  NOT_HOME
  MOVED
  REFUSED
  WRONG_ADDRESS
  INACCESSIBLE
  OTHER
}

Model Count by Category

Category Models ORM
Authentication 3 Prisma
Influence 4 Prisma
Map 4 Prisma
Canvassing 3 Prisma
Content 2 Prisma
Email Templates 2 Prisma
Media 4 Drizzle
Settings 2 Prisma
Total 24 Mixed

Database Operations

Migrations (Prisma)

# Create migration
cd api && npx prisma migrate dev --name add_field

# Deploy migrations
cd api && npx prisma migrate deploy

# Reset database (dev only)
cd api && npx prisma migrate reset

Schema Push (Drizzle)

# Push schema changes (media API)
cd api && npx drizzle-kit push

Database Browser

View data via:

Indexes

Key indexes for performance:

model Location {
  @@index([cutId])
  @@index([lastVisitedAt])
}

model Campaign {
  @@index([published])
  @@index([createdByUserId])
}

model CanvassSession {
  @@index([userId])
  @@index([status])
}

Constraints

Unique Constraints

model User {
  email String @unique
}

model Page {
  slug String @unique
}

model Cut {
  name String @unique
}

Check Constraints

Enforced at application level:

  • Email format validation
  • Password complexity (12+ chars)
  • Coordinate bounds (-90 to 90 lat, -180 to 180 lng)

Relations

One-to-Many

model User {
  id        Int        @id @default(autoincrement())
  campaigns Campaign[]
}

model Campaign {
  id              Int  @id @default(autoincrement())
  createdByUserId Int
  createdBy       User @relation(fields: [createdByUserId], references: [id])
}

Many-to-Many

Via junction tables:

model Shift {
  id      Int            @id @default(autoincrement())
  signups ShiftSignup[]
}

model User {
  id      Int            @id @default(autoincrement())
  signups ShiftSignup[]
}

model ShiftSignup {
  id      Int   @id @default(autoincrement())
  shiftId Int
  userId  Int
  shift   Shift @relation(fields: [shiftId], references: [id])
  user    User  @relation(fields: [userId], references: [id])

  @@unique([shiftId, userId])
}

Seeding

Initial data in api/prisma/seed.ts:

  • Admin user (admin@example.com)
  • Default settings
  • Sample page blocks
  • System email templates
# Run seed
cd api && npx prisma db seed

Data Types

Common Types

  • ID: Int @id @default(autoincrement())
  • String: String or String @db.Text (long text)
  • Number: Int or Float
  • Boolean: Boolean @default(false)
  • Date: DateTime @default(now())
  • JSON: Json or Json?
  • Enum: Role, VisitOutcome, etc.

Spatial Data

GeoJSON stored as JSON:

model Cut {
  geometry Json  // GeoJSON Polygon
}

Coordinates as separate fields:

model Location {
  latitude  Float
  longitude Float
}

Database Configuration

Connection String

DATABASE_URL="postgresql://user:password@localhost:5432/changemaker_v2?schema=public"

Connection Pool

Prisma connection pool:

// api/src/server.ts
const prisma = new PrismaClient({
  log: ['error', 'warn'],
});