Skip to content

Migrating from Kilat to Dulak

Kilat runs on Cloudflare Workers + D1. Dulak is its sister project — same philosophy, same stack (Hono + Inertia v3 + SSR, React/Svelte/Vue templates), but runs on Bun + bun:sqlite with no platform limits.

When you hit Cloudflare’s free tier limits (10 ms CPU, 100K requests/day, 500 MB D1), migrating to Dulak gives you unlimited everything on your own VPS. The good news: the database schema is identical.

Kilat (Workers) Dulak (Bun)
CPU time 10 ms (free) / 5 min (paid) Unlimited
Requests 100K/day (free) Unlimited
Database D1 (500 MB free) bun:sqlite (disk-bound)
Rate limiting No-op stub In-memory fixed-window
Password hashing PBKDF2 (Web Crypto) argon2id (Bun.password)
Uploads Manual tus resumable, built-in
Deploy wrangler deploy Docker / VPS
Terminal window
bun create dulak@latest my-app
cd my-app
bun run dev # http://localhost:4000

Pick the same template you used with Kilat (React/Svelte/Vue, vanilla/Tailwind). The client-side code is compatible — your pages and components port over with minimal changes.

From your project, export the production database:

Terminal window
wrangler d1 execute <database-name> --remote --export ./backup.sql

Replace <database-name> with your D1 database name from wrangler.toml.

Dulak uses bun:sqlite with the exact same schema as Kilat — the migration files (0001_users.sql through 0004_uploads.sql) are identical.

Terminal window
# Apply schema migrations (creates tables)
bun run dev # migrations run automatically on startup
# Import your data
bun -e "
import { Database } from 'bun:sqlite';
const db = new Database('./data/app.sqlite');
const sql = await Bun.file('./backup.sql').text();
db.exec(sql);
db.close();
console.log('Import complete');
"

If your app has file uploads (avatars, etc.), copy them from production to Dulak’s upload directory:

Terminal window
# If uploads are in R2
wrangler r2 object get <bucket-name>/<filename> ./data/uploads/<filename>
# Or download via HTTP from your app
curl -o ./data/uploads/avatar-123.png https://your-app.workers.dev/uploads/avatar-123.png

Update the path column in the uploads table to point to the new local paths:

UPDATE uploads SET path = './data/uploads/' || id WHERE 1=1;

Dulak uses a .env file instead of Wrangler secrets. Map your config:

wrangler.toml / secrets Dulak (.env)
APP_URL APP_URL
GOOGLE_CLIENT_ID GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET GOOGLE_CLIENT_SECRET
RESEND_API_KEY RESEND_API_KEY
MAIL_DRIVER MAIL_DRIVER
DATABASE_PATH DATABASE_PATH (default: ./data/app.sqlite)
PORT PORT (default: 4000)
Terminal window
cp .env.example .env
# Edit .env with your values
Terminal window
docker compose up -d --build

Dulak ships with a multi-stage Dockerfile (oven/bun:1.3-alpine). The ./data volume keeps your SQLite database across restarts.

Follow the Linux VPS guide — Bun + systemd + Cloudflare (origin rule or tunnel), no reverse proxy needed.

Terminal window
bun build --compile src/index.ts --outfile my-app
./my-app

Google OAuth redirect URIs are domain-specific. Update your Google Cloud Console credentials:

  • Old: https://your-app.workers.dev/auth/google/callback
  • New: https://your-vps-domain.com/auth/google/callback
What Ports directly? Notes
Client pages (React/Svelte/Vue) Same Inertia v3 components
Route handlers Same Hono routes, same SQL
db.ts query functions ⚠️ Change d1.prepare().bind().first()db.prepare().get()
config.ts Same env vars, different reader
Auth (sessions, CSRF, guards) Same logic, different password hash
Migrations Identical SQL files
Tests ⚠️ Dulak uses in-memory SQLite directly, no D1 mock needed

The main code change is in db.ts: D1’s async prepare().bind().first() becomes bun:sqlite’s sync prepare().get(). Dulak’s db.ts already has this — use it as reference.