S
Solo Kit
DocumentationComponentsPricingChangelogRoadmapFAQContact
LoginGet Started
DocumentationComponentsPricing
LoginGet Started
Welcome to Solo Kit DocumentationIntroductionTech StackRoadmapFAQGetting Started
Database OverviewSchema DesignDatabase QueriesDatabase MigrationsDatabase SeedingDatabase PerformanceDatabase SecurityBackup & RestoreDatabase MonitoringAdvanced Database Features
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:

  1. Schema Design - Design robust database schemas
  2. Queries - Master type-safe database operations
  3. Migrations - Manage schema evolution
  4. 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.

Getting Started

Learn how to set up and run Solo Kit locally

Schema Design

Design robust, scalable database schemas with Drizzle ORM, proper relationships, and PostgreSQL best practices

On this page

Database OverviewπŸ—οΈ Architecture OverviewPostgreSQL-First DesignTechnology StackπŸ—‚οΈ Database StructureSchema OrganizationπŸ”§ Core ComponentsDrizzle ORMDatabase ClientπŸ“‹ Table OverviewCore Tables🌟 Key Features1. Type Safety2. Migration System3. Prepared Statements4. Relations and JoinsπŸš€ Getting Started1. Environment Setup2. Initialize Database3. Basic UsageπŸ“Š Database ProvidersRecommended ProvidersConnection StringsπŸ› οΈ Development WorkflowDaily DevelopmentSchema ChangesData Management🎯 Best Practices1. Schema Design2. Query Organization3. Error HandlingπŸ“ˆ Performance ConsiderationsIndexing StrategyQuery OptimizationπŸ”’ SecurityInput ValidationRow Level Security🎯 Next Steps