or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authentication.mdclient-runtime.mddatabase.mde2e-testing.mdindex.mdrealtime.mdrouting.mdsynced-state.mdturnstile.mdvite-plugin.mdworker-runtime.md
tile.json

database.mddocs/

Database Access

Type-safe database access using Kysely ORM with SQLite on Cloudflare Durable Objects, including migration support and schema inference from migration definitions.

Capabilities

Create Database Connection

Creates a Kysely database instance connected to a Durable Object.

/**
 * Creates a Kysely database instance connected to a Durable Object
 * @param durableObjectBinding - Durable Object namespace binding
 * @param name - Database name (default: "main")
 * @returns Kysely instance with type-safe query builder
 */
function createDb<DatabaseType>(
  durableObjectBinding: DurableObjectNamespace,
  name?: string
): Kysely<DatabaseType>;

Usage Example:

import { createDb } from 'rwsdk/db';

interface Database {
  users: {
    id: number;
    name: string;
    email: string;
    createdAt: string;
  };
  posts: {
    id: number;
    userId: number;
    title: string;
    content: string;
  };
}

// In a route handler or server function
async function getUsers(env: any) {
  const db = createDb<Database>(env.DB_DO);

  const users = await db
    .selectFrom('users')
    .selectAll()
    .execute();

  return users;
}

SQL Template Tag

Kysely's SQL template tag for raw SQL queries.

/**
 * SQL template tag for raw SQL queries (re-exported from kysely)
 */
const sql: SqlTag;

Usage Example:

import { createDb, sql } from 'rwsdk/db';

const db = createDb<Database>(env.DB_DO);

// Raw SQL query
const result = await sql`SELECT * FROM users WHERE age > ${18}`.execute(db);

// SQL in query builder
const users = await db
  .selectFrom('users')
  .select('name')
  .where(sql`age > 18`)
  .execute();

SQLite Durable Object Base Class

Base class for Durable Objects that include a Kysely-backed SQLite database with automatic migrations.

/**
 * Base class for Durable Objects with Kysely database
 */
class SqliteDurableObject<T> {
  /**
   * Creates a new SQLite Durable Object
   * @param ctx - Durable Object state
   * @param env - Environment bindings
   * @param migrations - Migration definitions
   * @param migrationTableName - Migration tracking table name (default: "migrations")
   * @param plugins - Optional Kysely plugins
   */
  constructor(
    ctx: DurableObjectState,
    env: any,
    migrations: Record<string, Migration>,
    migrationTableName?: string,
    plugins?: KyselyPlugin[]
  );

  /**
   * Initializes the database and runs pending migrations
   */
  initialize(): Promise<void>;

  /**
   * RPC method for executing queries from external clients
   * @param compiledQuery - Compiled Kysely query
   * @returns Query result
   */
  kyselyExecuteQuery<R>(
    compiledQuery: CompiledQuery
  ): Promise<QueryResult<R>>;

  /**
   * Kysely instance for database operations
   */
  kysely: Kysely<T>;

  /**
   * Migration definitions
   */
  migrations: Record<string, Migration>;
}

Usage Example:

import { SqliteDurableObject, sql } from 'rwsdk/db';

// Define migrations
const migrations = {
  '001_initial': {
    async up(db) {
      await db.schema
        .createTable('users')
        .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
        .addColumn('name', 'text', (col) => col.notNull())
        .addColumn('email', 'text', (col) => col.notNull().unique())
        .addColumn('created_at', 'text', (col) =>
          col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
        )
        .execute();
    },
    async down(db) {
      await db.schema.dropTable('users').execute();
    },
  },
  '002_add_posts': {
    async up(db) {
      await db.schema
        .createTable('posts')
        .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
        .addColumn('user_id', 'integer', (col) =>
          col.notNull().references('users.id').onDelete('cascade')
        )
        .addColumn('title', 'text', (col) => col.notNull())
        .addColumn('content', 'text')
        .execute();
    },
    async down(db) {
      await db.schema.dropTable('posts').execute();
    },
  },
};

// Create Durable Object class
export class MyDatabase extends SqliteDurableObject<Database> {
  constructor(ctx: DurableObjectState, env: any) {
    super(ctx, env, migrations);
  }

  async fetch(request: Request) {
    // Ensure database is initialized
    await this.initialize();

    // Use kysely for queries
    const users = await this.kysely
      .selectFrom('users')
      .selectAll()
      .execute();

    return Response.json({ users });
  }
}

Migration Support

Functions for managing database migrations.

/**
 * Creates a Kysely migrator
 * @param db - Kysely database instance
 * @param migrations - Migration definitions
 * @param migrationTableName - Migration tracking table name
 * @returns Kysely Migrator instance
 */
function createMigrator(
  db: Kysely<any>,
  migrations: Record<string, Migration>,
  migrationTableName?: string
): Migrator;

/**
 * In-memory migration provider
 */
class InMemoryMigrationProvider implements MigrationProvider {
  constructor(migrations: Record<string, Migration>);
}

Type Inference from Migrations

Utilities for inferring database schema types from migration definitions.

/**
 * Migration interface
 */
interface Migration<TUpReturn = void> {
  up(db: Kysely<any>): Promise<TUpReturn>;
  down?(db: Kysely<any>): Promise<void>;
}

/**
 * Builder for migrations with type inference
 */
type MigrationBuilder = {
  up(fn: (db: Kysely<any>) => Promise<void>): MigrationBuilder;
  down(fn: (db: Kysely<any>) => Promise<void>): MigrationBuilder;
};

/**
 * Record of migration definitions
 */
type Migrations = Record<string, Migration>;

/**
 * Infers database schema type from migrations
 */
type Database<TMigrations extends Migrations> = InferDatabaseFromMigrations<TMigrations>;

/**
 * Builder with schema inference
 */
type InferenceBuilder = {
  createTable(name: string): TableBuilder;
  alterTable(name: string): TableBuilder;
};

Complete Database Example

import { SqliteDurableObject, sql } from 'rwsdk/db';
import { createDb } from 'rwsdk/db';

// Define database schema
interface Database {
  users: {
    id: number;
    name: string;
    email: string;
    password_hash: string;
    created_at: string;
  };
  posts: {
    id: number;
    user_id: number;
    title: string;
    content: string;
    published: number;
    created_at: string;
  };
}

// Define migrations
const migrations = {
  '001_initial_schema': {
    async up(db) {
      await db.schema
        .createTable('users')
        .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
        .addColumn('name', 'text', (col) => col.notNull())
        .addColumn('email', 'text', (col) => col.notNull().unique())
        .addColumn('password_hash', 'text', (col) => col.notNull())
        .addColumn('created_at', 'text', (col) =>
          col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
        )
        .execute();

      await db.schema
        .createTable('posts')
        .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
        .addColumn('user_id', 'integer', (col) =>
          col.notNull().references('users.id').onDelete('cascade')
        )
        .addColumn('title', 'text', (col) => col.notNull())
        .addColumn('content', 'text')
        .addColumn('published', 'integer', (col) => col.notNull().defaultTo(0))
        .addColumn('created_at', 'text', (col) =>
          col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
        )
        .execute();

      await db.schema
        .createIndex('posts_user_id_idx')
        .on('posts')
        .column('user_id')
        .execute();
    },
  },
};

// Durable Object implementation
export class AppDatabase extends SqliteDurableObject<Database> {
  constructor(ctx: DurableObjectState, env: any) {
    super(ctx, env, migrations);
  }

  async fetch(request: Request) {
    await this.initialize();

    const url = new URL(request.url);

    if (url.pathname === '/users' && request.method === 'GET') {
      const users = await this.kysely
        .selectFrom('users')
        .select(['id', 'name', 'email', 'created_at'])
        .execute();

      return Response.json({ users });
    }

    if (url.pathname === '/posts' && request.method === 'GET') {
      const posts = await this.kysely
        .selectFrom('posts')
        .innerJoin('users', 'users.id', 'posts.user_id')
        .select([
          'posts.id',
          'posts.title',
          'posts.content',
          'posts.published',
          'users.name as author_name',
        ])
        .where('posts.published', '=', 1)
        .orderBy('posts.created_at', 'desc')
        .execute();

      return Response.json({ posts });
    }

    return new Response('Not Found', { status: 404 });
  }
}

// Using the database from worker
import { createDb } from 'rwsdk/db';

async function getPosts(env: any) {
  const db = createDb<Database>(env.APP_DB);

  const posts = await db
    .selectFrom('posts')
    .innerJoin('users', 'users.id', 'posts.user_id')
    .select([
      'posts.id',
      'posts.title',
      'posts.content',
      'users.name as author',
    ])
    .where('posts.published', '=', 1)
    .execute();

  return posts;
}

Migration Best Practices

  1. Naming: Use sequential numbering with descriptive names (e.g., 001_initial_schema, 002_add_users)
  2. Reversibility: Include down migrations for rollback capability
  3. Atomicity: Keep migrations focused on single logical changes
  4. Data Safety: Never delete data in up migrations without proper backup
  5. Testing: Test migrations in development before production deployment