Introduction
A backend framework for Bun. One install replaces the database, auth, queue, cache, storage, mail and realtime services you would otherwise assemble and operate yourself.
Most Bun backends are a thin HTTP layer. Yatta is the part underneath it as well: a worker scheduler that keeps CPU work off your event loop, and a set of engines that run in-process, sharing one database and one type registry.
# 1. Installbun add yatta # 2. Create yatta/ in the current projectyatta init # 3. Runbun run devThat is the whole setup. yatta init writes a yatta/ folder containing a server entrypoint, a backend router, and one file per engine — all wired together and ready to edit. See Install & CLI.
What you get
Eight engines, each importable on its own:
Database— typed SQLite ORM with relations, transactions and migrationsAuth— Argon2id passwords, sessions, passkeys, 2FA, API keysJobs— durable queues, cron, and a typed event busCache— in-memory LRU backed by SQLiteStorage— local disk and S3 with signed URLsMail— templates, layouts, and seven transportsRealtime— WebSockets and SSE in one interfaceAPI— file-based routing and middleware
How it fits together
The runtime splits work across two pools. Anything marked io runs concurrently; anything marked cpu runs one task at a time on a dedicated thread. Password hashing and crypto never block a request.
import { createRuntime, defineSubsystem } from "yatta/runtime";import routers from "./func/routerHelper"; const runtime = createRuntime({ taskTimeoutMs: 30_000 });await runtime.start(); // CPU work is serialized; I/O work runs concurrently.await runtime.registerSubsystem( defineSubsystem({ name: "auth", entrypoint: new URL("./func/auth.ts", import.meta.url), workload: "cpu", }),); Bun.serve({ port: Number(process.env.PORT) || 4000, fetch: (req, server) => routers(req, server),});Your first route
Routes are files. Anything in yatta/backend/ is matched with a Next.js-style convention:
import { API, createAPI } from "yatta/api"; const api = createAPI(); api.get(async (ctx) => { return API.json({ id: ctx.params.id });}); export default api;yatta/backend/user/index.ts serves /user, and ctx.params.id is typed from the filename. See HTTP API.
Typed keys
Engine keys are typed through declaration merging. Add your job names once and every call site gets autocompletion and payload checking — with no code generation step.
import { createJobs, SQLiteJobStore } from "yatta/jobs"; export interface AppJobs { "send-email": { to: string; subject: string; body: string };} declare module "yatta/jobs" { interface JobRegister extends AppJobs {}} export const jobs = createJobs({ store: new SQLiteJobStore("Database/jobs.db"),});jobs.enqueue("send-email") is now checked against that shape. Full guide in Typed keys.
Configuration
PORTnumberNODE_ENV"production" | "development" | "test"STORAGE_SECRETstringDATABASE_URLstringPORT or a missing production secret stops the process immediately instead of failing halfway. See Configuration.Where to next
- Project layout — what each generated file does
- Database — the ORM most projects start with
- Authentication — users, sessions and passkeys
- Reliability — supervision, deadlines, draining