Claude Code Plugins

Community-maintained marketplace

Feedback

rwsdk-database-do

@kcc989/logoer
0
0

Use when working with rwsdk/db for SQLite Durable Objects - covers setup with migrations, type-safe queries with Kysely, CRUD operations, joins, seeding, and migration rollback handling in Cloudflare Workers environments

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name rwsdk-database-do
description Use when working with rwsdk/db for SQLite Durable Objects - covers setup with migrations, type-safe queries with Kysely, CRUD operations, joins, seeding, and migration rollback handling in Cloudflare Workers environments

rwsdk Database DO

Overview

rwsdk/db provides SQLite databases using Durable Objects with type-safe queries via Kysely. Each database instance runs in isolated Durable Objects with schema types inferred directly from migrations. No code generation needed - TypeScript understands your schema automatically.

When to Use

Use when:

  • Building Cloudflare Workers apps needing SQL databases
  • Want runtime database creation with minimal setup
  • Need isolated database instances (multi-tenant, modular components)
  • Prefer SQL query builder over ORMs
  • Working with rwsdk framework

Don't use when:

  • Need globally distributed low-latency reads (single location limitation)
  • Require production-grade backup features (preview feature)
  • Complex ORM requirements outweigh simplicity benefits

Quick Setup (5 Files)

1. Define Migrations (db/migrations.ts)

import { type Migrations } from 'rwsdk/db';

export const migrations = {
  '001_initial_schema': {
    async up(db) {
      await db.schema
        .createTable('todos')
        .addColumn('id', 'text', (col) => col.primaryKey())
        .addColumn('text', 'text', (col) => col.notNull())
        .addColumn('completed', 'integer', (col) => col.notNull().defaultTo(0))
        .addColumn('createdAt', 'text', (col) => col.notNull())
        .execute();
    },
    async down(db) {
      // CRITICAL: Use .ifExists() for SQLite's non-transactional DDL
      await db.schema.dropTable('todos').ifExists().execute();
    },
  },
} satisfies Migrations;

2. Create Database Instance (db/db.ts)

import { env } from 'cloudflare:workers';
import { type Database, createDb } from 'rwsdk/db';
import { type migrations } from '@/db/migrations';

// Export inferred types
export type AppDatabase = Database<typeof migrations>;
export type Todo = AppDatabase['todos'];

// Create database instance
export const db = createDb<AppDatabase>(
  env.APP_DURABLE_OBJECT,
  'todo-database' // unique key for this instance
);

3. Create Durable Object Class (db/durableObject.ts)

import { SqliteDurableObject } from 'rwsdk/db';
import { migrations } from '@/db/migrations';

export class AppDurableObject extends SqliteDurableObject {
  migrations = migrations;
}

4. Export from Worker (worker.ts)

export { AppDurableObject } from '@/db/durableObject';
// ... rest of worker code

5. Configure Wrangler (wrangler.toml)

[durable_objects]
bindings = [
  { name = "APP_DURABLE_OBJECT", class_name = "AppDurableObject" }
]

[[migrations]]
tag = "v1"
new_sqlite_classes = ["AppDurableObject"]

Type Inference

Types are automatically inferred from migrations - no code generation:

// TypeScript knows all columns and types
const user = await db.selectFrom('users').selectAll().executeTakeFirst();
// Type: { id: string, username: string } | undefined

CRUD Operations Reference

Operation Pattern Example
Insert insertInto().values().execute() await db.insertInto("todos").values(todo).execute()
Select selectFrom().selectAll().execute() await db.selectFrom("todos").selectAll().execute()
Select One selectFrom().executeTakeFirst() await db.selectFrom("todos").where("id", "=", id).executeTakeFirst()
Update updateTable().set().where().execute() await db.updateTable("todos").set({ completed: 1 }).where("id", "=", id).execute()
Delete deleteFrom().where().execute() await db.deleteFrom("todos").where("id", "=", id).execute()

Joins and Nested Data

Use Kysely helpers for ORM-like nested results:

import { jsonObjectFrom } from 'kysely/helpers/sqlite';

const posts = await db
  .selectFrom('posts')
  .selectAll('posts')
  .select((eb) => [
    jsonObjectFrom(
      eb
        .selectFrom('users')
        .select(['id', 'username'])
        .whereRef('users.id', '=', 'posts.userId')
    ).as('author'),
  ])
  .execute();

// Result: [{ id: "post-123", title: "...", author: { id: "...", username: "..." } }]

Database Seeding

Create seed script with default export:

// scripts/seed.ts
import { db } from '@/db/db';

export default async () => {
  console.log('🌱 Seeding...');
  await db.deleteFrom('todos').execute();

  await db
    .insertInto('todos')
    .values([
      {
        id: crypto.randomUUID(),
        text: 'First todo',
        completed: 0,
        createdAt: new Date().toISOString(),
      },
    ])
    .execute();

  console.log('✓ Seeded');
};

Add to package.json:

{
  "scripts": {
    "seed": "rwsdk worker-run ./src/scripts/seed.ts"
  }
}

Run: npm run seed

Migration Timing

Migrations run when createDb() is called:

  • Development: On dev server start
  • Production: During deployment (initial request triggers migration)

Migration Failures and Rollback

CRITICAL: SQLite doesn't support transactional DDL. Failed migrations can leave partial state.

Automatic behavior: Failed up() triggers down() automatically

Write defensive down() functions:

async down(db) {
  // Always use .ifExists() in case up() partially failed
  await db.schema.dropTable("posts").ifExists().execute();
  await db.schema.dropTable("users").ifExists().execute();
}

Why auto-rollback: Returns database to known-good state since manual intervention isn't feasible in isolated runtime environments.

Common Mistakes

Mistake Fix
Missing .ifExists() in down() Always use .ifExists() for rollback safety
Forgetting to export DO from worker Add export { AppDurableObject } to worker file
Wrong wrangler.toml binding name Match env.APP_DURABLE_OBJECT to wrangler binding name
Multiple DB instances without unique keys Each createDb() call needs unique key parameter
Not awaiting .execute() All Kysely queries must end with .execute()
Accessing tables before migrations run Module-level createDb() ensures migrations run on startup

API Quick Reference

createDb()

createDb<T>(
  durableObjectNamespace: DurableObjectNamespace,
  key: string
): Database<T>

Migration structure

{
  "001_migration_name": {
    async up(db) { /* create schema */ },
    async down(db) { /* undo with .ifExists() */ }
  }
} satisfies Migrations

Type exports

export type AppDatabase = Database<typeof migrations>;
export type TableName = AppDatabase['tableName'];

Performance Notes

  • Latency: Single-location execution (not globally distributed)
  • Isolation: Perfect for modular components, multi-tenant apps
  • Scale: Can create multiple instances with different keys for geographic distribution
  • Backups: No built-in backup features (implement external replication for critical data)

Complete Kysely Documentation

For full query builder capabilities, see Kysely docs. Everything in Kysely works with rwsdk/db.

Real-World Patterns

Multiple database instances:

const userDb = createDb(env.DO, 'users');
const analyticsDb = createDb(env.DO, 'analytics'); // isolated

Type-safe table access:

type User = AppDatabase['users']; // Inferred from migrations
const users: User[] = await db.selectFrom('users').selectAll().execute();

Conditional queries:

let query = db.selectFrom('posts').selectAll();
if (userId) {
  query = query.where('userId', '=', userId);
}
const posts = await query.execute();