A complete working example of filesystem-free migrations for Drizzle ORM.
Five vals that work together:
- @nbbaier/drizzleMigrations - Core migration library
- @nbbaier/drizzleSchema - Example schema definitions
- @nbbaier/runDrizzleMigrations - HTTP endpoint to run migrations
- @nbbaier/inspectDrizzleSchema - View generated SQL
- @nbbaier/drizzleExampleApp - Working API example
Visit: https://nbbaier-runDrizzleMigrations.web.val.run
You'll see:
{ "success": true, "version": 1, "applied": ["users", "posts"], "skipped": [] }
This created the users and posts tables in your SQLite database.
Visit: https://nbbaier-drizzleExampleApp.web.val.run/users
Create a user:
curl -X POST https://nbbaier-drizzleExampleApp.web.val.run/users \ -H "Content-Type: application/json" \ -d '{"name": "Alice", "email": "alice@example.com"}'
Visit: https://nbbaier-inspectDrizzleSchema.web.val.run
You'll see the actual CREATE TABLE statements that were generated.
import {
migrate,
generateCreateTableSQL,
} from "https://esm.town/v/nbbaier/drizzleMigrations";
Or fork @nbbaier/drizzleMigrations to your own account.
Create a val with your tables:
import { sqliteTable, text, integer } from "npm:drizzle-orm/sqlite-core";
export const myTable = sqliteTable("my_table", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
});
import { sqlite } from "https://esm.town/v/std/sqlite";
import { migrate } from "https://esm.town/v/nbbaier/drizzleMigrations";
import * as schema from "https://esm.town/v/YOUR_USERNAME/yourSchema";
export default async function (req: Request) {
const result = await migrate(sqlite, schema);
return Response.json(result);
}
import { drizzle } from "npm:drizzle-orm/better-sqlite3";
import { sqlite } from "https://esm.town/v/std/sqlite";
import * as schema from "https://esm.town/v/YOUR_USERNAME/yourSchema";
const db = drizzle(sqlite, { schema });
// Now you have full type safety!
const results = await db.select().from(schema.myTable);
- ✅ Type-safe - Full TypeScript inference from your schema
- ✅ No filesystem - Works in Val Town's sandboxed environment
- ✅ Auto migrations - Creates new tables automatically
- ✅ Change detection - Warns when schemas change
- ✅ Foreign keys - Handles relationships correctly
- ✅ Lightweight - ~300 lines, zero dependencies beyond drizzle-orm
Run migrations against your database.
Parameters:
db- Val Town SQLite instancetables- Object with table definitionsoptions.dryRun- (optional) Preview without executing
Returns:
{
applied: string[], // New tables created
skipped: string[], // Unchanged tables
version: number // Migration version
}
Generate CREATE TABLE SQL for a single table.
const sql = generateCreateTableSQL(myTable);
console.log(sql);
// CREATE TABLE IF NOT EXISTS "my_table" (
// "id" INTEGER PRIMARY KEY AUTOINCREMENT,
// ...
// );
Generate SQL for all tables and indexes.
const sql = generateSchemaSQL({ users, posts });
- Reads Drizzle metadata - Uses Symbol keys to introspect schema
- Generates SQL - Walks column definitions to build CREATE TABLE
- Tracks state in SQLite - Stores migration versions in
__drizzle_migrationstable - Detects changes - Compares SQL hashes to previous version
- Safe updates - Only creates new tables, warns about changes
When you modify an existing table:
- Update your schema val
- Run migrations again
- You'll see:
[CHANGED] Table schema changed: users - Write manual ALTER TABLE or recreate the table
Example manual migration:
await sqlite.execute(`
ALTER TABLE users ADD COLUMN bio TEXT;
`);
| Feature | Drizzle Kit | This Solution |
|---|---|---|
| Filesystem | Required | None |
| Val Town | No | Yes |
| Size | ~5MB | ~10KB |
| Auto migrations | Yes | New tables only |
| Type safety | Yes | Yes |
export default async function (req: Request) {
const auth = req.headers.get("authorization");
if (auth !== `Bearer ${Deno.env.get("MIGRATION_TOKEN")}`) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const result = await migrate(sqlite, schema);
return Response.json(result);
}
Set MIGRATION_TOKEN in Val Town secrets.
See what would happen without executing:
const result = await migrate(sqlite, schema, { dryRun: true });
Each Val Town user has their own SQLite database, so you can test changes before deploying.
Check out:
- @nbbaier/drizzleSchema - Schema definitions
- @nbbaier/runDrizzleMigrations - Migration runner
- @nbbaier/drizzleExampleApp - Working API
MIT - Use freely!
Open an issue or ask in Val Town Discord.