Herman Stander
Core team developer and marketing
2025-09-08
Need to peek inside your Durable Object SQLite database? This mini app lets you browse tables, view and edit rows, insert new records, and even seed data with AI — all in one coherent RedwoodSDK project.
When you’re iterating on a Durable Object (DO) backing a SQLite database, it’s cumbersome to:
You want this without spinning up extra services, and you want it to run the same on localhost and in production.
Using RedwoodSDK’s full-stack primitives, we built a simple explorer that:
sqlite_master
All in a single Worker, with straightforward routes and React Server Components.
Full source code: redwoodjs/sdk-experiments › do-db-explorer
We keep routing minimal: the homepage and a table slug, and add a middleware to interrupt requests in non-local environments to avoid exposing the explorer.
// src/worker.tsx
export default defineApp([
setCommonHeaders(),
({ ctx }) => {
// middleware interruptor to stop the request if not localhost
isLocalhost();
ctx;
},
render(Document, [
route("/", Home),
route("/:table", Home),
]),
]);
The isLocalhost
middleware short-circuits non-local requests so the table viewer only appears on localhost.
:table
param in Home
We read the :table
route param from params
and validate it against discovered tables. If the param is missing or invalid, we fallback to the first available table.
// src/app/pages/Home.tsx (excerpt)
"use server";
export async function Home({ params }: { params: { table: string } }) {
const table = params.table;
const selectedTable: string | undefined = table;
// Discover tables
const masterRows = await (db as any)
.selectFrom("sqlite_master")
.selectAll()
.execute();
const tableNames = masterRows
.filter((r: any) => r.type === "table" && typeof r.name === "string" && !r.name.startsWith("sqlite_"))
.map((r: any) => r.name as string)
.sort((a: string, b: string) => a.localeCompare(b));
const activeTable =
selectedTable && tableNames.includes(selectedTable) ? selectedTable : tableNames[0];
// ...
}
The explorer uses a single Durable Object binding and database key defined in src/db/db.ts
for simplicity.
// src/db/db.ts
import { env } from "cloudflare:workers";
import { type Database, createDb } from "rwsdk/db";
import { type migrations } from "@/db/migrations";
export type AppDatabase = Database<typeof migrations>;
export const db = createDb<AppDatabase>(env.APP_DURABLE_OBJECT, "main-database");
We define a Durable Object class that applies migrations, and the app connects via the db
instance above.
// src/db/durableObject.ts
import { SqliteDurableObject } from "rwsdk/db";
import { migrations } from "@/db/migrations";
export class AppDurableObject extends SqliteDurableObject {
migrations = migrations;
}
For background and API details, see the RedwoodSDK Database (Durable Objects) docs: Database (Durable Objects).
And the DO is declared in wrangler.jsonc
:
{
"durable_objects": {
"bindings": [
{ "name": "APP_DURABLE_OBJECT", "class_name": "AppDurableObject" }
]
},
"ai": { "binding": "AI" },
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["AppDurableObject"] }
]
}
Refer to Cloudflare’s docs for configuration and schema changes: Wrangler configuration and Migrations.
We introspect sqlite_master
, parse a table’s CREATE statement for columns, and normalize values to strings for reliable rendering.
// src/app/pages/Home.tsx (excerpt)
const masterRows = await (db as any)
.selectFrom("sqlite_master")
.selectAll()
.execute();
const tableNames = masterRows
.filter((r: any) => r.type === "table" && typeof r.name === "string" && !r.name.startsWith("sqlite_"))
.map((r: any) => r.name as string)
.sort((a, b) => a.localeCompare(b));
// Normalize row values
if (rows.length > 0 && columns.length > 0) {
rows = rows.map((row: any) => {
const out: Record<string, string> = {};
for (const key of columns) {
const value = row[key];
out[key] = value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
}
return out;
});
}
Server actions power insert/update/delete with basic table checks:
// src/app/pages/functions.ts (excerpt)
export async function insertRow(tableName: string, row: Record<string, string>) {
const adb: any = db;
const masterRows = await adb.selectFrom("sqlite_master").selectAll().execute();
const table = masterRows.find((r: any) => r.name === tableName);
if (!table) throw new Error(`Table ${tableName} not found`);
await adb.insertInto(tableName as any).values(row).execute();
}
We parse the CREATE statement to derive simple reference options for foreign-key-like columns and show picklists.
// src/app/pages/functions.ts (excerpt)
export async function listRefOptions(tableName: string, columnName: string) {
const adb: any = db;
const masterRows = await adb.selectFrom("sqlite_master").selectAll().execute();
const table = masterRows.find((r: any) => r.name === tableName);
if (!table) return [];
const { extractColumnsFromCreate } = await import("@/utils/schema");
const schema = extractColumnsFromCreate(table.sql || "");
const col = schema.find((c: any) => c.name === columnName);
const ref = col?.references;
if (!ref?.table) return [];
const targetColumn = ref.column || "id";
try {
const rows = await adb.selectFrom(ref.table).select([targetColumn]).execute();
return rows.map((r: any) => String(r[targetColumn]));
} catch {
return [];
}
}
For rapid iteration, we seed with Workers AI and then coerce types to match the SQLite schema.
// src/app/pages/functions.ts (excerpt)
// @ts-ignore
const ai = env.AI;
const response = await ai.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `Generate 10 rows for table ...`,
max_tokens: 4000,
temperature: 0.2,
});
// Coerce text columns to strings, validate NOT NULLs, then insert
await adb.deleteFrom(tableName as any).execute();
await adb.insertInto(tableName as any).values(coercedData).execute();
/:table
RedwoodSDK keeps everything — routes, server logic, durable objects, and UI — in one cohesive codebase. You get the full Request/Response control, dev ergonomics, and production readiness without juggling multiple services.
Learn more in the official docs: Database (Durable Objects). Also see Cloudflare Durable Objects docs: Overview, Storage, and Alarms.
This explorer is intentionally minimal so you can fork it and evolve it to fit your workflow:
It’s a solid foundation for internal tooling — clone it and make it yours.
Happy exploring!