Engines

Authentication

Authentication persisted through your own database rather than a vendor. Passwords use Argon2id, sessions are AES-256-GCM encrypted at rest, and tokens rotate on refresh.

Setup

createAuth takes a store, a signing secret, and optional email and passkey configuration. The scaffolded yatta/func/auth.ts already wires it to your database.

yatta/func/auth.tsts
import { createAuth, type AuthStore } from "yatta/auth";import { db } from "./db";import { mailer } from "./mail"; class SQLiteAuthStore implements AuthStore {  async findUserById(id: string) {    const row = db.users.findById(id);    return row ? this.toUser(row) : null;  }   async findUserByEmail(email: string) {    const row = db.users.findFirst({      where: { email: email.toLowerCase().trim() },    });    return row ? this.toUser(row) : null;  }   async createUser(data: any) {    const row = db.users.insert({      ...data,      email: data.email.toLowerCase().trim(),    });    return this.toUser(row);  }   private toUser(row: any) {    return {      ...row,      emailVerified: Boolean(row.emailVerified),      twoFactorEnabled: Boolean(row.twoFactorEnabled),      createdAt: new Date(row.createdAt),      updatedAt: new Date(row.updatedAt),    };  }  // …sessions, identities, tokens, passkeys, API keys} export const auth = createAuth({  secret: process.env.AUTH_SECRET || "change-me-32-chars-minimum",  store: new SQLiteAuthStore(),  email: { mailer, appUrl: process.env.APP_URL || "http://localhost:4000" },  passkeys: {    rpName: "Yatta App",    rpID: process.env.RP_ID || "localhost",    origin: process.env.APP_URL || "http://localhost:4000",  },});
Note
Store the secret in AUTH_SECRET. The fallback string above is a development convenience only.

Users

Signup and logints
// signUp returns one of two shapes — check before destructuring.const result = await auth.signUp({  email: "ada@example.com",  password: "CorrectHorseBatteryStaple123!",  req, // optional — used for rate limiting and audit}); if ("emailVerificationRequired" in result) {  // No session yet; the user must confirm their address first.} else {  const { user, session, tokens, cookies } = result;  // result.toResponse(body?, status?) builds the whole reply, cookies included.} // signIn returns { mfaRequired, userId } when a second factor is pending.const login = await auth.signIn({  email: "ada@example.com",  password: "CorrectHorseBatteryStaple123!",}); await auth.signOut(sessionToken); // Resolve the current session from a request, a bearer token, or a raw stringconst resolved = await auth.getSession(req);const fromHeader = await auth.getSession(authorizationHeader);const maybe = await auth.getSession(req, { autoRefresh: true });

Tokens

Signup and login return a short-lived access token and a longer refresh token. Refreshing rotates both, so a stolen refresh token is usable at most once.

Refreshingts
const { tokens } = await auth.refresh(refreshToken); // tokens.accessToken  — new// tokens.refreshToken — rotated; the old one is now invalid

Passkeys

FIDO2 / WebAuthn. Registration is two steps: send options to the browser, then verify what comes back.

Passkeysts
// 1. Registration — options first, then verify what came back.// The challenge is stored server-side and returned as a property on the options.const options = await auth.passkey.generateRegistrationOptions(userId);// → options.challenge  (persisted for 5 minutes) const verified = await auth.passkey.verifyRegistration(  userId,  response,        // from navigator.credentials.create()  options.challenge,); // 2. Authenticationconst authn = await auth.passkey.generateAuthenticationOptions(user.email); const result = await auth.passkey.verifyAuthentication(  response,        // from navigator.credentials.get()  authn.challenge,); // verifyAuthentication resolves to the same shape as signIn: an AuthResult, or// { mfaRequired: true, userId } when a second factor is still pending.

Two-factor authentication

TOTPts
// Start enrolment — returns a secret and otpauth URLconst { secret, recoveryCodes } = await auth.mfa.enable(userId, req); // Confirm the first code before it counts as enabledawait auth.mfa.verify(userId, "123456"); // Later, during login, when user.twoFactorEnabled is trueconst result = await auth.mfa.challenge(loginResult);

API keys

API keysts
const { apiKey, record } = await auth.apiKeys.create(user.id, {  name: "Production CLI",  scopes: ["read:data", "write:data"],}); // apiKey  → shown once, e.g. "yk_live_..."// record  → the stored row const principal = await auth.apiKeys.authenticate(rawKey);// checks the hash and scopes await auth.apiKeys.revoke(record.id);

Permissions

RBACts
const check = auth.permissions.check(user.roles, "delete", "posts"); // Inline, for templatescheck.ifCan(user, "edit", "post", { isOwner: post.authorId === user.id });check.denyUnless(user, "admin", "billing");

Define roles with accesscontrol and reuse them:

tsts
import { createAuth } from "yatta/auth";import * as ac from "accesscontrol"; const roles = {  user: { read: ["own"] },  admin: { read: ["any"], edit: ["any"], delete: ["any"] },}; export const auth = createAuth({  /* ... */  permissions: ac.define(roles),});

Rate limiting

Failed sign-ins are throttled per IP and per account. Exceeding the limit throws RateLimitError (HTTP 429).

tsts
import { auth, RateLimitError } from "yatta/auth"; try {  await auth.signIn({ email, password, req });} catch (err) {  if (err instanceof RateLimitError) {    return new Response("Too many attempts", { status: 429 });  }  throw err;}

Using it in a route

yatta/backend/me.tsts
import { API, createAPI } from "yatta/api";import { auth } from "../func/auth"; const api = createAPI(); api.get(async (ctx) => {  const token = ctx.req.headers.get("Authorization")?.replace("Bearer ", "");  const session = await auth.getSession(token);   if (!session) return API.json({ error: "Unauthorized" }, { status: 401 });   return API.json({ user: session.user });}); export default api;