Turbine

Quick Start

Install Turbine, connect it to PostgreSQL, generate a typed client and run your first query. It takes about five minutes.

Requirements#

  • Node.js 20 or later. The optional SQLite engine needs Node 22.5 or later.
  • PostgreSQL 14 or later, local or hosted (Neon, Supabase, Vercel Postgres and others). For other databases, see Database Engines.

1. Install#

npm install turbine-orm
npm install --save-dev tsx

pg is Turbine's only runtime dependency. tsx lets the CLI load TypeScript files such as turbine.config.ts and your schema file.

Turbine works from ESM and CommonJS. ESM ("type": "module" in package.json) is recommended.

2. Connect#

Put your connection string in a .env file:

# .env
DATABASE_URL=postgres://user:pass@localhost:5432/mydb

Then initialize the project:

npx turbine init

init writes turbine.config.ts, creates turbine/migrations/, and offers to scaffold a schema file and a seed file:

// turbine.config.ts (excerpt)
import type { TurbineCliConfig } from 'turbine-orm/cli';
 
const config: TurbineCliConfig = {
  url: process.env.DATABASE_URL,
  out: './generated/turbine',
  schema: 'public',
  migrationsDir: './turbine/migrations',
  schemaFile: './turbine/schema.ts',
};
 
export default config;

schema is the Postgres schema name, almost always 'public'. schemaFile is the path to your defineSchema() file. Do not put a file path in schema: introspection would find no tables.

3. Generate the typed client#

If your database already has tables, introspect it:

npx turbine generate

If it is empty, describe your tables in turbine/schema.ts:

// turbine/schema.ts
import { defineSchema } from 'turbine-orm';
 
export default defineSchema({
  users: {
    id: { type: 'serial', primaryKey: true },
    email: { type: 'text', unique: true, notNull: true },
    role: { type: 'text', notNull: true, default: "'member'" },
    createdAt: { type: 'timestamptz', notNull: true, default: 'now()' },
  },
  posts: {
    id: { type: 'serial', primaryKey: true },
    userId: { type: 'integer', notNull: true, references: 'users.id' },
    title: { type: 'text', notNull: true },
    createdAt: { type: 'timestamptz', notNull: true, default: 'now()' },
  },
});

Then create the tables and generate the client:

npx turbine push       # apply the schema to the database
npx turbine generate   # write the typed client

push is for development. In production, ship migrations.

generate writes three files to ./generated/turbine/: types.ts (entity and input types), metadata.ts (runtime schema metadata) and index.ts (the typed client).

4. Run a query#

import { turbine } from './generated/turbine/index.js';
 
const db = turbine(); // reads DATABASE_URL from the environment
 
const admins = await db.users.findMany({
  where: { role: 'admin' },
  orderBy: { createdAt: 'desc' },
  limit: 10,
});
 
console.log(admins[0]?.email);
 
await db.disconnect();

Every column, operator and relation autocompletes, and results are typed: admins[0].createdAt is a Date.

5. Load relations#

const users = await db.users.findMany({
  with: {
    posts: { orderBy: { createdAt: 'desc' }, limit: 5 },
  },
});
 
users[0]?.posts[0]?.title; // typed all the way down

Relation names come from foreign keys: posts.user_id gives posts.user and users.posts. To rename one, see relationNames.

Turbine usually loads the whole with tree in one SQL statement, a plan that relies on foreign-key indexes. npx turbine doctor lists missing ones, and Load strategies explains when a relation loads in a second query.

Next steps#