Extending
Typed keys
Every engine accepts free-form names by default. Declare yours once and the whole framework checks them — jobs, events, templates, disks and database rows.
TypeScript declaration merging lets you add your own keys to the interfaces Yatta declares. This is the same mechanism bun-types uses; nothing is generated, so there is no build step and nothing to drift out of date.
Jobs
import { createJobs, SQLiteJobStore } from "yatta/jobs"; export interface AppJobs { "send-email": { to: string; subject: string; body: string }; "transcode-video": { videoId: string; quality: "720p" | "1080p" }; "cleanup-stale-tokens": { maxAgeDays?: number };} declare module "yatta/jobs" { interface JobRegister extends AppJobs {}} export const jobs = createJobs({ store: new SQLiteJobStore("Database/jobs.db"),});Every call site is now checked:
await jobs.enqueue("send-email", { to: "ada@example.com", subject: "Hi", body: "Hello",}); // ✓ await jobs.enqueue("send-email", { to: "ada@example.com" }); // ✗ Property 'subject' is missing await jobs.enqueue("emial-send", {}); // ✗ not a known jobEvents
export interface AppEvents { "user.registered": { userId: string; email: string }; "order.completed": { orderId: string; amount: number };} declare module "yatta/jobs" { interface EventRegister extends AppEvents {}}events.on("user.registered", (data) => { data.email; // string — typed}); await events.emit("order.completed", { orderId: "o_1", amount: 42 });Mail templates
export interface AppTemplates { welcome: { name: string; verifyUrl: string }; receipt: { total: number; currency: string };} declare module "yatta/mail" { interface MailRegister { templates: AppTemplates; }}await mailer.send({ to, template: "welcome", data: { name: "Ada", verifyUrl: "https://…" }, // ✓}); await mailer.send({ to, template: "welcome", data: { name: "Ada" }, // ✗ verifyUrl missing});Storage disks
declare module "yatta/storage" { interface StorageRegister { disks: "local" | "s3" | "backups"; }}await storage.disk("local").upload("a.png", buffer);await storage.disk("s3").upload("a.png", buffer);await storage.disk("gcs").upload("a.png", buffer); // ✗ unknown diskDatabase rows
export const schema = { users: { id: col.uuid(), email: col.text().unique(), roles: col.json<string[]>().default(["user"]), createdAt: col.createdAt(), },}; declare module "yatta/db" { interface Register { schema: typeof schema; }}const user = await db.users.findById("u_1"); user.email; // stringuser.roles; // string[]user.nope; // ✗ not a columnRealtime events
declare module "yatta/realtime" { interface RealtimeRegister { events: { "chat.message": { user: string; text: string }; "order.status": { orderId: string; status: string }; }; }}Keeping types tidy
- Put each augmentation in the file that owns the engine — that keeps the declaration next to the code that uses it.
- Use a named interface per app and
extendsit, rather than merging straight intoJobRegister. - Everything lives in
yatta/func/, so the shape of your system is readable from one folder.