Skip to content

Philosophy

Dulak is the Banjar word for bored — and the name is the manifesto: code that is “boring” is the most valuable code there is. Boring here does not mean dull — it means predictable. No surprises, no clever tricks that require the next maintainer to reverse-engineer intent. Whoever comes later — human or AI agent — should be able to understand the code, change it, and not be afraid of breaking it.

Every choice trades “clever” for “obviously right”. When two ways of doing the same thing exist, only one is kept — the simpler one. For example, route handlers are written inline in their route file instead of being split into abstract controllers. Boring? Yes. Followable at a glance? Far more.

Every dependency is a liability: it must be upgraded, audited, and can break under you. When 60 lines of our own code are enough, we write them. In this repo: the rate limiter is hand-rolled (rate-limit.ts), the Google OAuth client is plain fetch (no SDK), CSS is vanilla by default, and the database layer is raw bun:sqlite prepared statements — no ORM.

Structure is standardized, on purpose: routes only live in routes/<feature>.routes.ts, all SQL lives in db.ts, environment variables are read only in config.ts. No “structural creativity” — that is the point. When everyone writes the same way, anyone can find anything.

Given a URL you can name the file that owns it: /loginroutes/auth.routes.ts, /uploadsroutes/uploads.routes.ts. Every URL lives in exactly one file, with its GET render and POST actions together. Paste a broken URL and you land in exactly one place — no guessing.

The guardrails a deployed app needs are wired from day one: CSRF, rate limiting, security headers, versioned migrations, graceful shutdown, Docker, CI. But the business features are not — that part is yours. You start from a skeleton that is shaped like a production app, not from zero.

A starter gets forked and maintained for years — its dependencies decide how much forced maintenance lands on the fork owner. Dulak pins current stable releases (Hono 4.x, Bun 1.3, Inertia v3) because:

  • No inherited rewrite churn. A dependency whose next major is a full rewrite strands the boilerplate on a legacy track with no upgrade path.
  • Breaking changes become schedulable. On a stable track, upgrades are deliberate, documented migrations you can plan and test — not emergency fixes when a beta dependency shifts under you.
  • A new major must prove itself. It ships when there is a concrete reason (security, ecosystem, maintenance), not because it is the newest thing.

Synchronous, explicitly typed, parameterized queries; fail-fast configuration; deterministic tests (bun test --isolate). Prefer the boring implementation that is obviously correct over the clever one that is hard to verify.

The “next maintainer” includes the agent writing the next feature — which, in this project, is the main way the code evolves. That is why conventions are codified where agents read them (AGENTS.md), validation errors have exact, documented shapes (TypeBox), mistakes fail at compile time (strict + noUncheckedIndexedAccess), and the test suite runs deterministically as the safety net. A codebase an agent can extend without inventing conventions is a codebase that stays coherent.