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:
- 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
# Run seed
cd api && npx prisma db seed
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:
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'],
});