Skip to main content

Overview

The Drizzle + Turso provider adds a typed database port to your Contract Kit application using Drizzle ORM with Turso’s libSQL. It follows the ports & adapters pattern, providing type-safe database access while maintaining clean separation of concerns.

Features

  • 🎯 Factory-based: Create providers with your schema at the call site
  • πŸ”’ Type-safe: Full TypeScript inference from your Drizzle schema
  • 🌐 Turso-ready: Works with Turso cloud or local libSQL
  • πŸ—οΈ Schema-agnostic: You control where your schema files live
  • πŸ”Œ Clean separation: Runtime provider is separate from build-time CLI config

Installation

npm install @contract-kit/provider-drizzle-turso drizzle-orm @libsql/client

Setup

1. Define your schema

Create your Drizzle schema file wherever makes sense for your app:
// src/db/schema.ts
import { sqliteTable, text } from "drizzle-orm/sqlite-core";

export const todos = sqliteTable("todos", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  completed: text("completed").notNull().default("false"),
});

2. Configure Drizzle CLI (build-time)

Create drizzle.config.ts in your app root for the Drizzle CLI:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "sqlite",
  dbCredentials: {
    url: process.env.TURSO_DB_URL!,
  },
});

3. Create the provider (runtime)

Import your schema and create the provider:
// src/lib/providers/db.ts
import * as schema from "@/db/schema";
import { createDrizzleTursoProvider } from "@contract-kit/provider-drizzle-turso";

export const drizzleTursoProvider = createDrizzleTursoProvider({ schema });

4. Type your ports

Define your app’s ports type:
// src/lib/ports.ts
import type { DbPort } from "@contract-kit/provider-drizzle-turso";
import * as schema from "@/db/schema";

export type AppPorts = {
  db: DbPort<typeof schema>;
  // other ports...
};

5. Wire it up in your server

// src/lib/app.ts
import { createServer } from "@contract-kit/server";
import { drizzleTursoProvider } from "./providers/db";

export const app = createServer({
  ports: {},
  providers: [drizzleTursoProvider],
  // ... other config
});

6. Use in your use cases

// src/application/todos/listTodos.ts
import { useCase } from "@/lib/useCase";
import * as schema from "@/db/schema";

export const listTodos = useCase
  .query("todos.list")
  .input(z.object({}))
  .output(z.array(z.object({ id: z.string(), title: z.string() })))
  .run(async ({ ctx }) => {
    const db = ctx.ports.db.db; // LibSQLDatabase<typeof schema> - fully typed!

    const rows = await db.select().from(schema.todos);
    return rows;
  });

Configuration

The provider reads configuration from environment variables:
VariableRequiredDescription
TURSO_DB_URLYesTurso/libSQL database URL (e.g., libsql://your-db.turso.io or file:local.db)
TURSO_DB_AUTH_TOKENNoTurso auth token (required for cloud databases, optional for local)

Example .env

# For Turso cloud
TURSO_DB_URL=libsql://my-app-db.turso.io
TURSO_DB_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI...

# For local development
TURSO_DB_URL=file:local.db

API Reference

DbPort<TSchema>

The port interface exposed on ctx.ports.db:
interface DbPort<TSchema extends Record<string, any> = any> {
  db: LibSQLDatabase<TSchema>;
  client: Client;
}
  • db: The typed Drizzle database instance for ORM operations
  • client: The underlying libSQL client for advanced operations not covered by Drizzle

createDrizzleTursoProvider<TSchema>(options)

Factory function to create a Drizzle Turso provider. Parameters:
  • options.schema (required): Your Drizzle schema object
  • options.portName (optional): Port name, defaults to "db"
Returns: A ServiceProvider that can be used with createServer Example:
const provider = createDrizzleTursoProvider({
  schema: mySchema,
  portName: "db", // optional
});

Usage Examples

Basic CRUD Operations

import { eq } from "drizzle-orm";

const db = ctx.ports.db.db;

// Select
await db.select().from(schema.todos);

// Insert
await db.insert(schema.todos).values({ id: "1", title: "Hello" });

// Update
await db.update(schema.todos).set({ title: "Updated" }).where(eq(schema.todos.id, "1"));

// Delete
await db.delete(schema.todos).where(eq(schema.todos.id, "1"));

Transactions

await db.transaction(async (tx) => {
  await tx.insert(schema.todos).values({ id: "1", title: "Todo 1" });
  await tx.insert(schema.todos).values({ id: "2", title: "Todo 2" });
});

Advanced libSQL Operations

Access the underlying libSQL client for operations not covered by Drizzle:
const client = ctx.ports.db.client;
const result = await client.execute("SELECT * FROM todos WHERE id = ?", ["1"]);

Multiple Databases

You can create multiple database providers with different port names:
import * as mainSchema from "@/db/schema";
import * as analyticsSchema from "@/db/analytics-schema";
import { createDrizzleTursoProvider } from "@contract-kit/provider-drizzle-turso";

export const mainDbProvider = createDrizzleTursoProvider({
  schema: mainSchema,
  portName: "db", // default
});

export const analyticsDbProvider = createDrizzleTursoProvider({
  schema: analyticsSchema,
  portName: "analyticsDb",
});

// Then in your ports type:
export type AppPorts = {
  db: DbPort<typeof mainSchema>;
  analyticsDb: DbPort<typeof analyticsSchema>;
};

Key Design Principles

Runtime vs. Build-time Separation

This provider follows a clean separation of concerns:
  • Build-time (Drizzle CLI): Configured via drizzle.config.ts
    • Used for generating migrations
    • Used for introspecting the database
    • Lives in your app repository
  • Runtime (Provider): Configured via factory function
    • Used for connecting to the database at runtime
    • Used for executing queries in your use cases
    • Gets the schema from your imports

Schema Location Independence

The provider does not care where your schema file lives. You:
  1. Define your schema file wherever makes sense (src/db/schema.ts, db/schema.ts, etc.)
  2. Import it in your app: import * as schema from "@/db/schema"
  3. Pass it to the factory: createDrizzleTursoProvider({ schema })
This keeps the provider flexible and your app in control of its structure.

Best Practices

Generate and run migrations using the Drizzle CLI to keep your database schema in sync.
Let Drizzle’s type system catch database schema errors at compile time.
Wrap multiple related database operations in transactions to maintain data consistency.
Organize your schema files by domain or feature for better maintainability.

Next Steps