Skip to main content
BunShip uses Drizzle ORM with Turso (libSQL/SQLite). Schema files live in packages/database/src/schema/ and define both database structure and TypeScript types in a single source of truth.

Schema Overview

Each table is defined in its own file. Relations between tables are centralized in index.ts.

How a Schema File Works

Here is the users table as a reference for the patterns used throughout the codebase:
Key patterns to note:
  • CUID2 primary keys generated with $defaultFn(() => createId())
  • SQLite column modes for booleans ({ mode: "boolean" }), timestamps ({ mode: "timestamp" }), and JSON ({ mode: "json" })
  • Typed JSON columns with .$type<YourInterface>()
  • Soft deletes via a deletedAt column
  • Automatic timestamps with $defaultFn and $onUpdateFn
  • Exported types for both select (User) and insert (NewUser)

Adding New Tables

1

Create the schema file

Create a new file in packages/database/src/schema/:
2

Define relations

Add relations to packages/database/src/schema/index.ts:
If widgets should appear in organization queries, add a reverse relation to the existing organizationsRelations:
3

Export the schema

Add the exports to the bottom of packages/database/src/schema/index.ts:
4

Generate and apply the migration

Adding Columns to Existing Tables

To add a column to an existing table, edit the table’s schema file directly and then generate a migration. For example, adding a bio field to the users table:
Then generate and apply:
When adding columns to tables that already have data, make the column nullable or provide a .default() value. A notNull() column without a default will fail if the table contains existing rows.

Column Types

SQLite has a limited type system. Drizzle maps TypeScript types to SQLite storage using column modes.

Text Columns

Integer Columns

Default Values

Indexes and Constraints

Indexes

Define indexes in the third argument to sqliteTable:
Add indexes on columns you frequently filter or sort by. Foreign key columns (organizationId, createdBy) and status columns are good candidates.

Unique Constraints

Foreign Keys

Migrations Workflow

BunShip uses Drizzle Kit for migrations. The workflow is:
  1. Edit schema files in packages/database/src/schema/
  2. Generate a migration SQL file
  3. Apply the migration to your database

Generate a Migration

This compares your schema files against the previous migration state and produces a new SQL migration file in the migrations/ directory.

Apply Migrations

Runs all pending migrations against the database specified by DATABASE_URL.

Push (Development Shortcut)

During development, you can push schema changes directly without generating migration files:
db:push modifies the database schema in place without creating migration files. Only use this in local development. For staging and production, always use db:generate + db:migrate so changes are tracked and reproducible.

Inspect Your Database

Open Drizzle Studio to browse your data:
This launches a web UI at https://local.drizzle.studio where you can view tables, run queries, and inspect data.

Relations and Joins

Drizzle supports relational queries through the relations() function, which enables nested data fetching without writing manual joins.

Defining Relations

Querying with Relations

Once relations are defined, use db.query to fetch nested data:

Manual Joins

For more control, use Drizzle’s SQL-like query builder:

Next Steps

Adding Routes

Build API endpoints that use your new tables

Email Templates

Send notifications for your new resources