Getting started

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.

terminalbash
# 1. Installbun add yatta # 2. Create yatta/ in the current projectyatta init # 3. Runbun run dev

That 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 migrations
  • Auth — Argon2id passwords, sessions, passkeys, 2FA, API keys
  • Jobs — durable queues, cron, and a typed event bus
  • Cache — in-memory LRU backed by SQLite
  • Storage — local disk and S3 with signed URLs
  • Mail — templates, layouts, and seven transports
  • Realtime — WebSockets and SSE in one interface
  • API — 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.

yatta/main.tsts
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:

yatta/backend/user/index.tsts
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.

yatta/func/jobs.tsts
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

PORTnumber
Defaults to 4000.
NODE_ENV"production" | "development" | "test"
Defaults to development.
STORAGE_SECRETstring
Signs storage URLs. Required in production; outside it a temporary secret is generated and a warning is logged.
DATABASE_URLstring
SQLite file path. Defaults to Database/app.db.
Note
Configuration is validated before anything boots. A malformed PORT or a missing production secret stops the process immediately instead of failing halfway. See Configuration.

Where to next