Database
Database Overview
Database Overview
Solo Kit uses a PostgreSQL-first architecture with Drizzle ORM for type-safe database operations. This guide covers everything from basic concepts to advanced database management.
ποΈ Architecture Overview
PostgreSQL-First Design
Solo Kit is built exclusively for PostgreSQL, providing:
- Type Safety - Complete type safety from database to UI
- Performance - Optimized queries with proper indexing
- Scalability - Built for production workloads
- Consistency - Same database technology across all environments
Technology Stack
π Application Layer
β (TypeScript interfaces)
π§ Drizzle ORM
β (SQL queries)
π PostgreSQL Database
β (Data storage)
βοΈ Cloud Provider (Neon, Supabase, Railway)
ποΈ Database Structure
Schema Organization
packages/database/src/
βββ schema/
β βββ index.ts # Main schema exports
β βββ users.ts # User-related tables
β βββ auth.ts # Authentication tables
β βββ subscriptions.ts # Payment/subscription tables
β βββ audit.ts # Audit logging tables
βββ lib/
β βββ db.ts # Database client
β βββ migrate.ts # Migration utilities
β βββ seed.ts # Seed data functions
βββ queries/
β βββ users.ts # User queries
β βββ auth.ts # Auth queries
β βββ subscriptions.ts # Subscription queries
βββ migrations/ # Database migrations
βββ 0001_initial.sql
βββ meta/ # Migration metadata
π§ Core Components
Drizzle ORM
Type-Safe Database Client:
import { db } from '@packages/database';
import { users } from '@packages/database/schema';
import { eq } from 'drizzle-orm';
// Type-safe queries
const user = await db.select().from(users).where(eq(users.id, userId));
// user is typed as User automatically
Benefits:
- Zero runtime overhead
- Full TypeScript integration
- SQL-like syntax
- Automatic type inference
Database Client
Centralized Connection:
// packages/database/src/lib/db.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString);
export const db = drizzle(client, { schema });
π Table Overview
Core Tables
Users & Authentication:
users -- User profiles
accounts -- OAuth accounts (GitHub, Google)
sessions -- User sessions
verification_tokens -- Email verification
password_reset_tokens -- Password reset
Business Logic:
subscriptions -- User subscriptions
payments -- Payment records
audit_logs -- System audit trail
user_preferences -- User settings
Example Schema (Users):
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
avatar: text('avatar_url'),
role: text('role', { enum: ['user', 'admin'] }).default('user'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
π Key Features
1. Type Safety
Automatic Type Generation:
// Types are automatically inferred
type User = typeof users.$inferSelect;
type CreateUser = typeof users.$inferInsert;
// Usage
const createUser = async (userData: CreateUser): Promise<User> => {
const [user] = await db.insert(users).values(userData).returning();
return user;
};
2. Migration System
Schema Evolution:
# Generate migrations after schema changes
pnpm db:generate
# Apply migrations
pnpm db:push # Development
pnpm db:migrate # Production
3. Prepared Statements
Performance Optimization:
// Automatic prepared statement caching
const getUserById = async (userId: string) => {
return db.select().from(users).where(eq(users.id, userId));
};
// Drizzle automatically caches this query as a prepared statement
4. Relations and Joins
Type-Safe Relationships:
export const usersRelations = relations(users, ({ many, one }) => ({
accounts: many(accounts),
sessions: many(sessions),
subscription: one(subscriptions, {
fields: [users.id],
references: [subscriptions.userId],
}),
}));
// Usage with relations
const userWithSubscription = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
subscription: true,
},
});
π Getting Started
1. Environment Setup
# Add to .env.local
DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require"
2. Initialize Database
# Push schema to database
pnpm db:push
# Open visual database browser
pnpm db:studio
3. Basic Usage
import { db } from '@packages/database';
import { users } from '@packages/database/schema';
// Create user
const newUser = await db.insert(users).values({
email: 'user@example.com',
name: 'John Doe',
}).returning();
// Read user
const user = await db.select().from(users).where(eq(users.id, userId));
// Update user
await db.update(users)
.set({ name: 'Jane Doe', updatedAt: new Date() })
.where(eq(users.id, userId));
// Delete user
await db.delete(users).where(eq(users.id, userId));
π Database Providers
Recommended Providers
1. Neon (Recommended)
- Free tier: 10GB storage, 1M rows
- Features: Branching, point-in-time recovery
- Best for: Development and production
2. Supabase
- Free tier: 500MB storage, 2 CPUs
- Features: Real-time subscriptions, built-in auth
- Best for: Real-time applications
3. Railway
- Free tier: $5/month credit
- Features: Simple deployment, auto-scaling
- Best for: Simple deployment needs
Connection Strings
# Neon
DATABASE_URL="postgresql://username:password@ep-xyz.us-east-1.aws.neon.tech/neondb?sslmode=require"
# Supabase
DATABASE_URL="postgresql://postgres:password@db.xyz.supabase.co:5432/postgres"
# Railway
DATABASE_URL="postgresql://postgres:password@roundhouse.proxy.rlwy.net:12345/railway"
π οΈ Development Workflow
Daily Development
# 1. Start development
pnpm dev
# 2. Make schema changes
# Edit: packages/database/src/schema/
# 3. Push changes to database
pnpm db:push
# 4. Visual database management
pnpm db:studio
Schema Changes
# 1. Modify schema files
# packages/database/src/schema/users.ts
# 2. Generate migration (production)
pnpm db:generate
# 3. Apply changes
pnpm db:push # Development (direct schema sync)
pnpm db:migrate # Production (apply migrations)
Data Management
# Add demo data for development
pnpm seed:demo
# Clear all data
pnpm seed:clear
# Reset and reseed
pnpm seed:reset
π― Best Practices
1. Schema Design
// β
Good - Descriptive names, proper constraints
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
}));
// β Avoid - Generic names, missing constraints
export const users = pgTable('users', {
id: text('id'), // No primary key constraint
data: text('data'), // Generic column name
created: timestamp('created'), // No default value
});
2. Query Organization
// β
Good - Dedicated query functions
export const getUserById = async (userId: string) => {
return db.query.users.findFirst({
where: eq(users.id, userId),
with: {
subscription: true,
},
});
};
export const getUsersByRole = async (role: 'user' | 'admin') => {
return db.select().from(users).where(eq(users.role, role));
};
// β Avoid - Inline queries in components
const user = await db.select().from(users).where(eq(users.id, userId));
3. Error Handling
import { DatabaseError } from '@packages/database/errors';
export const createUser = async (userData: CreateUser) => {
try {
const [user] = await db.insert(users).values(userData).returning();
return { success: true, data: user };
} catch (error) {
if (error.code === '23505') { // Unique constraint violation
return { success: false, error: 'Email already exists' };
}
throw new DatabaseError('Failed to create user', error);
}
};
π Performance Considerations
Indexing Strategy
// Add indexes for frequently queried columns
export const users = pgTable('users', {
// ... columns
}, (table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
roleIdx: index('users_role_idx').on(table.role),
createdAtIdx: index('users_created_at_idx').on(table.createdAt),
}));
Query Optimization
// β
Good - Select only needed columns
const userEmails = await db
.select({ email: users.email, name: users.name })
.from(users);
// β Avoid - Select all columns when not needed
const userEmails = await db.select().from(users);
π Security
Input Validation
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
role: z.enum(['user', 'admin']).default('user'),
});
export const createUser = async (input: unknown) => {
const validated = createUserSchema.parse(input);
// Safe to use validated data
return db.insert(users).values(validated).returning();
};
Row Level Security
-- Enable RLS on sensitive tables (if needed)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- Policy examples (applied via migrations)
CREATE POLICY users_select_policy ON users
FOR SELECT USING (auth.uid() = id);
π― Next Steps
Ready to dive deeper into database development:
- Schema Design - Design robust database schemas
- Queries - Master type-safe database operations
- Migrations - Manage schema evolution
- Performance - Optimize database performance
The PostgreSQL + Drizzle combination provides a solid foundation for scalable, type-safe applications. Master these concepts to build robust data layers for your Solo Kit applications.