Featul uses PostgreSQL with Drizzle ORM for type-safe database operations. This guide covers database provisioning, schema migrations, and production best practices.
Database Requirements
Featul requires a PostgreSQL database with the following specifications:
PostgreSQL Version : 14 or higher
Connection Method : Neon serverless HTTP or standard PostgreSQL
SSL : Required for production (sslmode=require)
Extensions : None required (uses standard PostgreSQL features)
Recommended Providers
Neon Serverless PostgreSQL with instant branching and auto-scaling
Supabase Open-source Firebase alternative with PostgreSQL
Railway Developer-first cloud platform with PostgreSQL
Featul is optimized for Neon with the @neondatabase/serverless driver for HTTP-based connections.
Provisioning a Database
Neon (Recommended)
Supabase
Self-Hosted
Create Neon Account
Sign up at neon.tech and create a new project.
Get Connection String
From your Neon dashboard:
Select your project
Go to Connection Details
Copy the Pooled connection string
Format:
Set Environment Variable
Add to your .env.local or hosting platform: Neon’s serverless driver works over HTTP, enabling fast, stateless connections ideal for serverless deployments.
Get Connection String
From your Supabase dashboard:
Go to Settings > Database
Copy the Connection string (Transaction mode)
Replace [YOUR-PASSWORD] with your database password
Configure Environment
DATABASE_URL = postgresql://postgres:[PASSWORD ]@db.xxx.supabase.co:5432/postgres ? sslmode = require
Install PostgreSQL
# Ubuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib
# macOS (Homebrew)
brew install postgresql@16
brew services start postgresql@16
Create Database and User
-- Connect as postgres user
sudo - u postgres psql
-- Create database
CREATE DATABASE featul ;
-- Create user with password
CREATE USER featul_user WITH PASSWORD 'secure_password' ;
-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE featul TO featul_user;
Configure Connection String
DATABASE_URL = postgresql://featul_user:secure_password@localhost:5432/featul
For production self-hosting, enable SSL/TLS and use a connection pooler like PgBouncer.
Database Schema
Featul’s database schema is defined in packages/db/schema/ with the following tables:
Core Tables
user - User accounts and profiles
session - Active user sessions
account - OAuth provider accounts
verification - Email verification tokens
passkeyTable - WebAuthn passkey credentials
twoFactorTable - Two-factor authentication settings
Workspace Tables
workspace - Multi-tenant workspaces
workspaceMember - Workspace membership and roles
workspaceDomain - Custom domain configurations
workspaceInvite - Pending workspace invitations
workspaceSlugReservation - Reserved workspace slugs
Feedback Tables
board - Feedback boards configuration
post - Feedback posts and feature requests
tag - Tags for categorization
postTag - Many-to-many post-tag relationships
postUpdate - Status updates on posts
postReport - Reported posts
postMerge - Merged/duplicate posts
Engagement Tables
comment - Comments on posts
commentReaction - Emoji reactions on comments
commentMention - User mentions in comments
commentReport - Reported comments
vote - User votes on posts
Additional Tables
subscription - Workspace subscriptions and billing
brandingConfig - Custom branding settings
changelogEntry - Product changelog entries
activityLog - Audit log for workspace activities
workspaceIntegration - Third-party integrations
workspaceNotraConnection - Notra credential connections
Running Migrations
Featul uses Drizzle Kit for schema migrations. All migration files are in packages/db/drizzle/.
Migrations are automatically generated from TypeScript schema files using Drizzle Kit.
Initial Setup
Set Database URL
Ensure DATABASE_URL is set in your environment: export DATABASE_URL = "postgresql://user:password@host:5432/database?sslmode=require"
Run Migrations
This executes all SQL migration files in packages/db/drizzle/ in order.
Verify Schema
# Open Drizzle Studio to inspect database
bun run db:studio
Access at https://local.drizzle.studio to browse tables and data.
Development Workflow
Generate New Migration
Push Schema (Development Only)
Run Migrations
Open Database Studio
# After modifying schema files in packages/db/schema/
bun run db:generate
db:push bypasses migrations and directly syncs schema. Only use in development. Always use db:migrate for production.
Production Migrations
For production deployments:
Backup Database
# Neon: Use branch feature for zero-downtime migrations
# Self-hosted: Create backup
pg_dump $DATABASE_URL > backup- $( date +%Y%m%d ) .sql
Test Migration Locally
# Clone production data to staging
# Run migration on staging first
DATABASE_URL = $STAGING_DATABASE_URL bun run db:migrate
Run Production Migration
# Set production DATABASE_URL
export DATABASE_URL = $PRODUCTION_DATABASE_URL
# Run migrations
bun run db:migrate
Verify Migration
# Check migration status
bun run db:studio
# Verify application works
curl https://app.featul.com/api/health
Drizzle Configuration
Drizzle is configured in packages/db/drizzle.config.ts:
import { defineConfig } from "drizzle-kit"
import "dotenv/config"
export default defineConfig ({
out: "./drizzle" , // Migration output directory
schema: "./schema/index.ts" , // Schema definition files
dialect: "postgresql" , // Database dialect
dbCredentials: {
url: process . env . DATABASE_URL ?? "" ,
} ,
})
Database connection is initialized in packages/db/index.ts:
import { drizzle } from "drizzle-orm/neon-http"
import { neon } from "@neondatabase/serverless"
import * as schema from "./schema"
const DATABASE_URL = process . env . DATABASE_URL
if ( ! DATABASE_URL ) {
throw new Error ( "DATABASE_URL is not set" )
}
export const db = drizzle ( neon ( DATABASE_URL ), { schema })
Connection Pooling
For high-traffic production deployments, use connection pooling:
Neon (Built-in)
PgBouncer
Supabase Pooler
Neon automatically handles connection pooling with their serverless driver. Use the Pooled connection string from dashboard: No additional configuration needed. For self-hosted PostgreSQL, use PgBouncer: # Install PgBouncer
sudo apt install pgbouncer
# Configure /etc/pgbouncer/pgbouncer.ini
[databases]
featul = host=localhost port= 5432 dbname=featul
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
Update DATABASE_URL to use port 6432: DATABASE_URL = postgresql://user:password@localhost:6432/featul
Supabase provides a built-in connection pooler: From Settings > Database , use the Connection pooling string: postgresql://postgres:[PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
Recommended pool mode: Transaction
Database Seeding (Optional)
For development or demo environments, seed the database with sample data:
# Seed workspace posts (development only)
bun run db:seed-posts
Seeding scripts are for development only. Never run on production databases.
Monitoring and Maintenance
-- Find slow queries (PostgreSQL)
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10 ;
Database Size
-- Check table sizes
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC ;
Backup Strategy
Implement automated backups for production databases.
Neon provides:
Point-in-time recovery : Restore to any point in the last 7 days (paid plans)
Branch feature : Create database branches for testing
Automated backups : Daily backups included
Create a branch for testing: # Using Neon CLI
neon branches create --project-id PROJECT_ID --name staging
# Daily backup script
#!/bin/bash
BACKUP_DIR = "/var/backups/postgresql"
DATE = $( date +%Y%m%d_%H%M%S )
pg_dump $DATABASE_URL | gzip > " $BACKUP_DIR /featul_ $DATE .sql.gz"
# Keep last 30 days
find $BACKUP_DIR -name "featul_*.sql.gz" -mtime +30 -delete
Add to crontab: # Run daily at 2 AM
0 2 * * * /usr/local/bin/backup-featul.sh
Troubleshooting
Migration Fails
Error: relation "user" does not exist
Solution : Run all migrations from the beginning:
Connection Timeout
Error: Connection timeout
Solution :
Verify DATABASE_URL is correct
Check database is running and accessible
Ensure firewall allows connections
For Neon: Verify project is not suspended
SSL Required Error
Solution : Add ?sslmode=require to connection string:
DATABASE_URL = postgresql://user:password@host:5432/db? sslmode = require
Permission Denied
Error: permission denied for table
Solution : Grant necessary permissions:
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO featul_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO featul_user;
Next Steps
Environment Variables Configure DATABASE_URL and other variables
Deploy to Vercel Deploy your application with database configured