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:
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:
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)¶
Database Browser¶
View data via:
- Prisma Studio:
npx prisma studio - NocoDB: http://localhost:8091 (read-only)
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
Data Types¶
Common Types¶
- ID:
Int @id @default(autoincrement()) - String:
StringorString @db.Text(long text) - Number:
IntorFloat - Boolean:
Boolean @default(false) - Date:
DateTime @default(now()) - JSON:
JsonorJson? - Enum:
Role,VisitOutcome, etc.
Spatial Data¶
GeoJSON stored as JSON:
Coordinates as separate fields:
Database Configuration¶
Connection String¶
Connection Pool¶
Prisma connection pool: