OpenCouncil uses PostgreSQL with the PostGIS extension for geospatial features. This guide covers database setup, schema migrations, and seeding.
Requirements
OpenCouncil requires PostgreSQL 14+ with PostGIS 3.3.5+ for production. Development environments use these versions by default.
Core requirements:
PostgreSQL 14 or higher
PostGIS 3.3.5 or higher (3.5 works but 3.3.5 matches production)
Prisma CLI (included in dev environments)
Database schema
OpenCouncil uses Prisma as an ORM. The schema is defined in prisma/schema.prisma with 30+ models:
Key models:
City - Municipalities with PostGIS geometry
CouncilMeeting - Meetings with video/audio media
Person - Council members with voice recognition
Party - Political parties
SpeakerSegment - Speaker time intervals
Utterance & Word - Transcription data
Subject - Agenda items with location/topic
Notification - User notifications
TaskStatus - Async job tracking
Multi-tenant design: Many models use composite keys (cityId, id) for data isolation.
View the full schema:
Setting up a new database
Create database with PostGIS
Self-managed PostgreSQL
Managed services (Supabase, RDS, etc.)
CREATE DATABASE opencouncil ;
\c opencouncil
CREATE EXTENSION IF NOT EXISTS postgis;
Configure connection strings
Add to .env: DATABASE_URL = "postgresql://user:password@host:5432/opencouncil?sslmode=require"
DIRECT_URL = "postgresql://user:password@host:5432/opencouncil?sslmode=require"
See Environment setup for details.
Run migrations
npx prisma migrate deploy
This applies all migrations from prisma/migrations/.
Seed the database (optional)
Schema migrations
OpenCouncil uses Prisma Migrate for schema versioning and deployments.
Development workflow
Always use --create-only when making schema changes. This prevents accidentally applying migrations to the wrong database.
Edit the schema
Modify prisma/schema.prisma: model City {
id String @id @default ( cuid ())
name String
// Add new field
population Int ?
// ...
}
Create migration (do not apply)
npx prisma migrate dev --name add_population_to_cities --create-only
This generates a migration file in prisma/migrations/ but does not apply it .
Review the migration SQL
cat prisma/migrations/ < timestam p > _add_population_to_cities/migration.sql
Verify the SQL is correct before applying.
Test against local database
npx prisma migrate deploy
Apply to your local database and test the application.
Commit and deploy
git add prisma/
git commit -m "Add population field to cities"
Production deployments will apply the migration automatically.
Production deployments
Migrations are applied automatically during deployment:
Nix dev runner
Docker
Manual deployment
nix run .#dev # Runs migrations on startup
Migration history
OpenCouncil has 80+ migrations dating back to September 2024. View all migrations:
Example migrations:
20260207211304_add_speaker_name_to_contributions/
20260202164839_add_diavgeia_decisions/
20260125144744_add_discussed_in_field/
20251026111508_add_notifications/
20250703144238_add_equipment_and_physical_presence_to_offers/
20250203100741_add_administrative_bodies/
20250112125551_add_geometry_to_cities/
Rollback (not recommended)
Prisma Migrate does not support automatic rollbacks. To revert:
Create a new migration that reverses the changes
Test thoroughly
Deploy the new migration
Never manually edit applied migrations in prisma/migrations/. This breaks migration history.
Database seeding
OpenCouncil provides two seeding workflows:
Automatic seeding (development)
Local development environments automatically seed the database:
nix run .#dev # Seeds automatically on first run
Seed data source
The seed script (prisma/seed.ts):
Checks for prisma/seed_data.json locally
If not found, downloads from GitHub repository
Creates test users with different permission levels
Seeds cities, meetings, transcripts, and related data
Test users created:
Super Admin (full access)
City Admin (city-level access)
Party Admin (party-level access)
Person Admin (person-level access)
Read Only (no admin permissions)
In development, a floating panel allows instant user switching without email authentication.
Custom seed data
Generate seed data from an existing database:
npm run generate-seed -- \
--source= "postgresql://user:pass@host:5432/db" \
--pairs=chania/latest,athens/latest \
--output=./prisma/seed_data.json
Options:
--source: Source database URL
--pairs: Comma-separated cityId/meetingId pairs (use latest for most recent meeting)
--output: Path to save JSON file
Seed data structure:
{
"topics" : [ ... ],
"cities" : [ ... ],
"administrativeBodies" : [ ... ],
"parties" : [ ... ],
"persons" : [ ... ],
"meetings" : [ ... ],
"voiceprints" : [ ... ]
}
See docs/database-seeding.md in the source repository for details.
Environment variables
Customize seeding behavior:
# URL to download seed data from
SEED_DATA_URL = https://custom-url/seed_data.json
# Path to local seed data file
SEED_DATA_PATH = ./custom/path/seed_data.json
# City ID for test users
DEV_TEST_CITY_ID = chania
Database maintenance
Reset local database
This is destructive and cannot be undone. All data will be lost.
Nix
Docker
Prisma (any environment)
nix run .#cleanup # Removes .data/postgres and .next
nix run .#dev # Creates fresh database with seed data
Prisma Studio (visual database editor)
Open Prisma Studio to browse and edit data:
Automatic (Nix/Docker dev mode)
Standalone
Nix (custom database)
# Starts automatically with dev server
# Access at http://localhost:5555
Backup and restore
Backup with pg_dump:
# Full backup
pg_dump "postgresql://user:pass@host:5432/db" > backup.sql
# Schema only
pg_dump --schema-only "postgresql://user:pass@host:5432/db" > schema.sql
# Data only
pg_dump --data-only "postgresql://user:pass@host:5432/db" > data.sql
Restore:
psql "postgresql://user:pass@host:5432/newdb" < backup.sql
Database size monitoring
Check database size:
SELECT
pg_size_pretty(pg_database_size( 'opencouncil' )) as db_size,
pg_size_pretty(pg_total_relation_size( '"CouncilMeeting"' )) as meetings_size,
pg_size_pretty(pg_total_relation_size( '"Utterance"' )) as utterances_size;
Connection pooling
For production deployments with high concurrency, use connection pooling:
PgBouncer (recommended)
# Install PgBouncer
sudo apt-get install pgbouncer
# Configure pgbouncer.ini
[databases]
opencouncil = host=localhost port= 5432 dbname=opencouncil
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
Update .env:
# Pooled connection
DATABASE_URL = "postgresql://user:pass@localhost:6432/opencouncil"
# Direct connection (for migrations)
DIRECT_URL = "postgresql://user:pass@localhost:5432/opencouncil"
Managed pooling
Many managed PostgreSQL services offer built-in pooling:
Supabase : Provides connection pooler on port 6543
Neon : Automatic connection pooling
AWS RDS Proxy : Managed connection pooling
Troubleshooting
PostGIS extension not found
CREATE EXTENSION IF NOT EXISTS postgis;
If this fails, install PostGIS on your server:
# Ubuntu/Debian
sudo apt-get install postgresql-14-postgis-3
# macOS (Homebrew)
brew install postgis
Migration failed: version mismatch
Ensure PostGIS versions match between local and production:
SELECT PostGIS_Version();
OpenCouncil targets PostGIS 3.3.5 for production compatibility.
Shadow database access denied
Prisma needs a “shadow database” for migrations. Grant permissions:
GRANT CREATE ON DATABASE opencouncil TO your_user;
Or use DIRECT_URL pointing to a database where you have full permissions.
Connection limit reached
Check active connections:
SELECT count ( * ) FROM pg_stat_activity;
Increase max_connections or use connection pooling.
Next steps
Environment setup Configure database URLs and secrets
Docker deployment Deploy with Docker Compose
Nix deployment Deploy with Nix flakes