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.
Why migrate?
Section titled “Why migrate?”| 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 |
Step 1: Scaffold a Dulak project
Section titled “Step 1: Scaffold a Dulak project”bun create dulak@latest my-appcd my-appbun run dev # http://localhost:4000Pick 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.
Step 2: Export your D1 data
Section titled “Step 2: Export your D1 data”From your project, export the production database:
wrangler d1 execute <database-name> --remote --export ./backup.sqlReplace <database-name> with your D1 database name from wrangler.toml.
Step 3: Import into Dulak’s bun:sqlite
Section titled “Step 3: Import into Dulak’s bun:sqlite”Dulak uses bun:sqlite with the exact same schema as Kilat — the
migration files (0001_users.sql through 0004_uploads.sql) are identical.
# Apply schema migrations (creates tables)bun run dev # migrations run automatically on startup
# Import your databun -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');"Step 4: Move uploaded files
Section titled “Step 4: Move uploaded files”If your app has file uploads (avatars, etc.), copy them from production to Dulak’s upload directory:
# If uploads are in R2wrangler r2 object get <bucket-name>/<filename> ./data/uploads/<filename>
# Or download via HTTP from your appcurl -o ./data/uploads/avatar-123.png https://your-app.workers.dev/uploads/avatar-123.pngUpdate the path column in the uploads table to point to the new local
paths:
UPDATE uploads SET path = './data/uploads/' || id WHERE 1=1;Step 5: Move environment variables
Section titled “Step 5: Move environment variables”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) |
cp .env.example .env# Edit .env with your valuesStep 6: Deploy Dulak
Section titled “Step 6: Deploy Dulak”Option A: Docker (recommended)
Section titled “Option A: Docker (recommended)”docker compose up -d --buildDulak ships with a multi-stage Dockerfile (oven/bun:1.3-alpine).
The ./data volume keeps your SQLite database across restarts.
Option B: VPS with systemd
Section titled “Option B: VPS with systemd”Follow the Linux VPS guide — Bun + systemd + Cloudflare (origin rule or tunnel), no reverse proxy needed.
Option C: Single binary
Section titled “Option C: Single binary”bun build --compile src/index.ts --outfile my-app./my-appStep 7: Update Google OAuth redirect
Section titled “Step 7: Update Google OAuth redirect”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 over vs needs changes
Section titled “What ports over vs needs changes”| 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.