Deliberately boring.
A full-stack starter that actually works.
One Bun process: Hono + bun:sqlite +Inertia v3 with in-process SSR. React, Svelte, or Vue — your choice. Auth, uploads, migrations, tests, Docker — wired end to end, not left for you to glue.
Benchmarks
Numbers from real hardware
bun:sqlite + Hono on Apple M4, measured — not estimated.Full benchmark →
vs Laravel 13 + FrankenPHP · 8 workers · same SQLite WAL · same machine
Everything wired
Working code, not wiring homework
Auth, uploads, migrations, SSR, tests, Docker — running end to end, not left for you to wire.
Auth, complete
Register, login, logout, forgot/reset password and Google OAuth — argon2id, DB-backed sessions, CSRF, rate-limited.
Inertia v3 + SSR
Full HTML on first load, SPA after. In-process SSR, 409 version negotiation, partial reloads, one-shot flash.
Resumable uploads
tus protocol v1: creation, resume, checksum, termination, expiration — proven by the avatar upload.
One server, three clients
React, Svelte 5, and Vue 3 templates with Tailwind CSS v4 — same auth, SSR, and test suite.
Migrations & SQL
Versioned SQL applied at startup in transactions. Zero-ORM bun:sqlite, prepared statements only.
Production-grade ops
Batched logging, gzip, security headers, /health, graceful shutdown, multi-stage Docker, CI.
Philosophy
Why Dulak exists
Because your first week should be business logic, not infrastructure.
Boring means predictable
Routes in one place, SQL in one file, env in one module — no guessing where things live. No clever tricks to figure out. Whoever comes next — human or AI — can read it and change it without fear of breaking it.
Everything in one runtime
No separate language, package manager, web server, and database to install and configure. Bun does all of it — one install, one runtime, and your app is running with auth, SSR, and tests already working.
Built for AI agents
Conventions codified in AGENTS.md, validation errors with exact shapes, strict TypeScript that fails at compile time, deterministic tests as the safety net. A codebase an agent can extend without inventing conventions stays coherent.
For the full reasoning, read the philosophy page →.
Templates
One server. Three clients.
import { Head, Link, useForm } from "@inertiajs/react";import AuthLayout from "../components/AuthLayout";import Field from "../components/Field";import "../components/AuthLayout.css";
export default function Login() { const form = useForm({ email: "", password: "" });
const submit = (e: React.FormEvent) => { e.preventDefault(); form.post("/login"); };
return ( <AuthLayout> <Head title="Login" /> <h1 className="auth-sub">Welcome back</h1> <form onSubmit={submit} noValidate> <Field id="email" label="Email" error={form.errors.email}> <input id="email" type="email" autoComplete="email" value={form.data.email} onChange={(e) => form.setData("email", e.target.value)} /> </Field> <button className="btn btn-primary btn-block" type="submit" disabled={form.processing}> {form.processing ? "Signing in…" : "Sign in"} </button> </form> </AuthLayout> );}<script lang="ts"> import { Link, useForm } from '@inertiajs/svelte' import AuthLayout from '../components/AuthLayout.svelte' import Field from '../components/Field.svelte'
let { googleEnabled = false, notice = null } = $props()
const form = useForm({ email: '', password: '' })
function submit(e: SubmitEvent) { e.preventDefault() form.post('/login') }</script>
<AuthLayout> <h1 class="auth-sub">Welcome back</h1> <form onsubmit={submit} novalidate> <Field id="email" label="Email" error={form.errors.email}> <input id="email" type="email" bind:value={form.email} onchange={() => form.clearErrors('email')} /> </Field> <button class="btn btn-primary btn-block" type="submit" disabled={form.processing}> {form.processing ? 'Signing in…' : 'Sign in'} </button> </form></AuthLayout><script setup lang="ts">import { Head, Link, useForm } from "@inertiajs/vue3";import AuthLayout from "../components/AuthLayout.vue";import Field from "../components/Field.vue";
defineProps<{ googleEnabled?: boolean; notice?: string | null }>();
const form = useForm({ email: "", password: "" });
function submit() { form.post("/login");}</script>
<template> <Head><title>Login</title></Head> <AuthLayout> <h1 class="auth-sub">Welcome back</h1> <form @submit.prevent="submit" novalidate> <Field id="email" label="Email" :error="form.errors.email"> <input id="email" type="email" v-model="form.email" @change="form.clearErrors('email')" /> </Field> <button class="btn btn-primary btn-block" type="submit" :disabled="form.processing"> {{ form.processing ? "Signing in…" : "Sign in" }} </button> </form> </AuthLayout></template>FAQ
The short version. For the long version, read the comparison article.
How is Dulak different from Next.js, Nuxt, or SvelteKit?
Those are meta-frameworks that couple a server model to one client framework (React, Vue, or Svelte) and leave auth, uploads, and persistence to you. Dulak is a single Bun process with Hono + bun:sqlite + Inertia v3, auth and uploads already wired, raw SQL instead of an ORM, and React, Svelte, or Vue as a swappable template. Pick a meta-framework for RSC or edge deploy; pick Dulak for a wired, ownable, single-runtime stack.
How is Dulak different from Laravel?
One runtime vs four. Dulak is Bun end to end — HTTP server, database, bundler, test runner, package manager. Laravel is still gluing together PHP, Composer, a web server, and a database driver in 2026 — plus a separate Node process if you want Inertia SSR. Dulak serves 52K reads/s and 95K writes/s on a single process; Laravel on FrankenPHP (8 workers) hits 6K — 8× slower on reads, 5× on writes. No ORM means no abstraction tax. Pick Dulak for TypeScript everywhere and one runtime; pick Laravel if four toolchains sounds like a feature, not a tax.
Why Bun and not Node?
One runtime for everything: Bun.serve is the HTTP server, bun:sqlite is the database, Bun.build is the bundler, bun test is the test runner. No separate language, package manager, web server, and database to wire together before your first running app.
Do I have to use React?
No. Dulak ships templates for React 19, Svelte 5, and Vue 3 — each with vanilla CSS or Tailwind CSS v4. The server side (Hono, SQL, auth) is identical across all of them.
Why no ORM?
ORMs were built to help humans avoid writing SQL. In 2026, AI generates correct, optimized raw SQL on demand — the human-friction problem ORMs solved is gone. What remains is the cost: an ORM is a dependency you upgrade, audit, and debug, and it adds a layer between you and the database that hides the actual query plan. bun:sqlite with prepared statements is synchronous, explicit, and zero-dependency. You see the exact SQL that runs, and the AI writing your next feature writes it at peak performance — no abstraction tax.
Why SQLite and not Postgres?
One file for the whole database: bun:sqlite is built into Bun, no server to install, no connection pool, no credentials. WAL mode serves 52K reads/s and 95K writes/s on a single process — enough for most apps. Back up by copying a file, or stream replicas to S3 with Litestream. When you outgrow it, Postgres is a deliberate swap point, not an emergency.
Is it production-ready?
Yes — every guardrail a deployed app needs is wired and tested, not scaffolded: argon2id passwords, DB-backed sessions (not JWT), CSRF origin checks, two-layer rate limiting (global DDoS baseline + auth brute-force), security headers (CSP, nosniff, frame denial), versioned migrations in transactions, gzip compression, graceful shutdown (SIGTERM), multi-stage Docker, and a 63-test E2E suite that boots the full app against an in-memory DB. The server side is not a demo — it is the same code path that runs in production. What is missing is your business logic, and that is the point: you start from a skeleton that already works, not one you have to harden.
Will this still work in 5–10 years?
Yes, and here is why: Dulak is a boilerplate you fork and own, not a framework that can be deprecated out from under you. The stack is chosen for longevity — Bun, Hono, SQLite, and Inertia are stable, pinned versions with no pending rewrites. SQLite is the most deployed database in the world and has maintained backward compatibility for decades. When a dependency does release a new major, it is a deliberate migration you schedule and test — not an emergency. And because there is no ORM, no SDK, no proprietary abstraction layer, the code you own is just TypeScript, SQL, and HTTP — readable and maintainable by any developer or AI agent, now and in 2036.