Codapult
FeaturesPricingAPIHelpChangelog
Codapult

Ship Your SaaS Faster

Product

  • Features
  • Pricing
  • Plugins
  • API Reference
  • Help Center
  • Feature Requests
  • Changelog

Company

  • Contact
  • GitHub

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Codapult. All rights reserved.

All articles

Getting Started

  • Introduction
  • Quick Start
  • Project Structure

Configuration

  • Environment Variables
  • App Configuration

Authentication

  • Authentication

Database

  • Database

Teams

  • Teams & Organizations

Payments

  • Payments & Billing

Api

  • API Layer

Ai

  • AI Features

Email

  • Email

Infrastructure

  • Infrastructure

Ui

  • UI & Theming

I18n

  • Internationalization

Content Management

  • Content Management

Admin

  • Admin Panel

Security

  • Security

Monitoring

  • Analytics & Monitoring

Modules

  • Module Architecture

Plugins

  • Plugin System

Deployment

  • Deployment
  • Troubleshooting

Upgrading

  • Upgrading Codapult

Developer Tools

  • MCP Server
  • Testing
Database

Database

Configure Turso, define schema with Drizzle ORM, and run queries.

Overview

Codapult uses Turso (LibSQL) as its database — an edge-hosted SQLite database with a generous free tier — and Drizzle ORM for type-safe queries. No raw SQL is needed.

ComponentDetails
DatabaseTurso (LibSQL) — edge SQLite, free tier
ORMDrizzle ORM — type-safe, zero-overhead
Schemasrc/lib/db/schema.ts (single source of truth)
Clientsrc/lib/db/index.ts
Seedsrc/lib/db/seed.ts
Migrationssrc/lib/db/migrations/

Schema Conventions

The schema file src/lib/db/schema.ts is the single source of truth for all tables. Follow these conventions when adding or modifying tables:

ConventionRule
Table namesSingular snake_case (user, organization_member)
Column namessnake_case (created_at, user_id)
Primary keystext('id').primaryKey() — UUIDs or nanoid, never auto-increment
Timestampsinteger('col', { mode: 'timestamp' }) with .$defaultFn(() => new Date())
Booleansinteger('col', { mode: 'boolean' }) (SQLite has no native bool)
Foreign keysAlways specify onDelete (cascade, set null)
Type-safe enums.$type<TypeName>() with an exported TypeScript union

Example Table

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export type UserRole = 'user' | 'admin';

export const user = sqliteTable('user', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  role: text('role').$type<UserRole>().notNull().default('user'),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
  updatedAt: integer('updated_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
});

Query Patterns

Import db from @/lib/db and table schemas from @/lib/db/schema. Use Drizzle's query builder with condition helpers from drizzle-orm.

Select

import { db } from '@/lib/db';
import { user } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';

const users = await db.select().from(user).limit(50);

const singleUser = await db.select().from(user).where(eq(user.id, userId)).limit(1);

Insert

import { nanoid } from 'nanoid';

await db.insert(user).values({
  id: nanoid(),
  name: 'Alice',
  email: '[email protected]',
});

Update

await db.update(user).set({ name: 'Alice Smith' }).where(eq(user.id, userId));

Delete

await db.delete(user).where(eq(user.id, userId));

Conditions

import { eq, and, or, like } from 'drizzle-orm';

const admins = await db
  .select()
  .from(user)
  .where(and(eq(user.role, 'admin'), like(user.email, '%@example.com')))
  .limit(100);

Always use .limit() for list queries to prevent unbounded result sets.

Setup & Commands

Initial Setup

# Set your database URL in .env.local
TURSO_DATABASE_URL="file:local.db"  # local development
# TURSO_DATABASE_URL="libsql://your-db.turso.io"  # production

# Create all tables from schema (no migrations needed for fresh databases)
pnpm db:push

# Seed sample data for development
pnpm db:seed

Migration Workflow

For new projects, pnpm db:push applies the full schema directly. Once you have production data, use the migration workflow to make incremental changes safely:

# 1. Edit src/lib/db/schema.ts

# 2. Generate a migration
pnpm db:generate

# 3. Apply the migration
pnpm db:migrate

All db:* scripts auto-load .env.local via process.loadEnvFile() — no manual export needed.

Command Reference

CommandDescription
pnpm db:pushApply schema directly to database
pnpm db:generateGenerate migration from schema diff
pnpm db:migrateRun pending migrations
pnpm db:seedSeed sample data

Local Development

For local development, use an SQLite file instead of a remote Turso database:

TURSO_DATABASE_URL="file:local.db"

This creates a local.db file in the project root. No Turso account required.

Multi-Region Replication

Turso supports read replicas in multiple regions for low-latency reads worldwide. Configure replicas through the Turso dashboard or CLI, then set the replica URLs in your environment. The database client handles automatic routing — reads go to the nearest replica, writes go to the primary.

Next Steps

  • Authentication — user accounts and sessions stored in the database
  • Teams & Organizations — multi-tenant data model
  • Modules — each module's tables can be removed independently
AuthenticationTeams & Organizations