Releases: shaferllc/keel
Release list
v0.36.0 — notifications
Send a message to one or many recipients over pluggable channels: notify(user, new InvoicePaid(4200)). A Notification declares via() (channels) and per-channel content (toMail, toArray).
Built-in channels: MailChannel (via the mailer, routed by email or routeNotificationFor), DatabaseChannel (inserts toArray into a table), and ArrayChannel (for tests). Set shouldQueue = true to deliver from a queued job — this is where the mail and queue layers compose. A custom channel is one send method. New generator keel make:notification.
See docs/notifications.md.
v0.35.0 — queues & jobs
Move slow work off the request path: dispatch(new SendWelcome(id)) places a Job (or a plain function) on a queue, and a pluggable QueueDriver decides when it runs. Built-in drivers: SyncDriver (runs immediately — the default), MemoryDriver (defers; work() drains FIFO, inspect .jobs). dispatch takes { delay, queue }; a push-only custom driver is the seam for a real broker (e.g. Cloudflare Queues). New generator keel make:job. Core imports no broker, edge-safe.
See docs/queues.md.
v0.34.0 — mail
A fluent, edge-safe mailer: mail().to().subject().html().send() over a pluggable Transport (like the database Connection). setMailer(transport, { from }) registers the default. Built-in transports: ArrayTransport (collects to .sent, the default and ideal for tests), LogTransport (logs instead of delivering), and fetchTransport({ url, headers, body }) for provider HTTP APIs (Resend/Postmark/Mailgun) over fetch — the core imports no SDK. send() validates recipient/subject/body/from.
See docs/mail.md.
v0.33.0 — model casts + mass-assignment guarding
Attribute casts: static casts = { active: "boolean", meta: "json", joined_at: "date" } round-trips columns as real JS types — cast on read, back to storable primitives on write, so boolean/json bind cleanly on drivers that reject JS booleans/objects. Types: int, float, boolean, string, json/array, date.
Mass-assignment guarding: static fillable (allowlist) / static guarded (denylist) filter what create() and fill() accept; forceFill() bypasses. With neither declared, behavior is unchanged (backward compatible).
See docs/models.md#attribute-casts.
v0.32.0 — factories & seeders
factory(Model, (f, i) => ({ ... })) builds model attributes with a built-in, dependency-free Faker (seedable for deterministic runs). .make() (unsaved) / .create() (persisted), .count(n) for batches, inline overrides. Seeder classes with run() + call([...]) composition; seed(DatabaseSeeder) runs one. New generators keel make:factory and keel make:seeder. Edge-safe (no external faker library).
See docs/factories.md.
v0.31.0 — model relationships
Model relationships over the query builder: hasMany / hasOne / belongsTo / belongsToMany, with conventional foreign keys you can override. Relations are awaitable and expose .query(); Model.load(models, "posts") eager-loads with one whereIn per relation (fixes N+1). belongsToMany reads a pivot and offers attach / detach / sync. Loaded relations stay out of save() and serialize via toJSON(). No JOINs, edge-safe.
See docs/models.md#relationships.
v0.30.0 — migrations
Version your schema with a fluent builder + migrator:
const migrations: Migration[] = [{
name: "01_create_users",
up: (s) => s.createTable("users", (t) => {
t.id();
t.string("email").unique();
t.boolean("active").default(true);
t.timestamps();
}),
down: (s) => s.dropTable("users"),
}];
await new Migrator(connection, "postgres").up(migrations); // idempotent
await new Migrator(connection, "postgres").down(migrations); // roll back last batchApplied migrations are batch-tracked in a migrations table; SQL is dialect-aware (sqlite/mysql/postgres). See docs/migrations.md.
v0.29.0 — active-record models
A tiny active-record Model over the query builder:
class User extends Model {
static table = "users";
declare id: number;
declare email: string;
}
const user = await User.find(1); // User | null
const created = await User.create({ email });
user.email = "new@x.com";
await user.save(); // insert or update
await user.delete();Static find/findOrFail/all/first/where/create; instance save/delete/fill/toJSON. Model.query() drops to the raw builder. Runs on any registered connection — edge-safe. See docs/models.md.
v0.28.0 — database query builder
A driver-agnostic query builder — the foundation of Keel's data layer:
setConnection(myConnection, "postgres"); // any driver, two methods
await db("users").where("active", true).orderBy("name").get();
await db("users").where("id", 1).first();
await db("posts").whereIn("id", [1, 2, 3]).count();
const id = await db("users").insertGetId({ email, name });Parameterized SQL (injection-safe) through a two-method Connection — works with D1, Neon/Postgres, PlanetScale, Turso, better-sqlite3, and pg. The core imports no driver, so it stays edge-safe. An active-record Model layer + migrations build on this next. See docs/database.md.
v0.27.0 — authentication
Session-based auth, tying together the session + hash primitives:
setUserProvider((id) => db.users.find(id)); // once, in a provider
if (user && await hash.verify(user.password, password)) {
auth().login(user.id);
}
auth().check(); auth().id(); await auth().user(); auth().logout();Protect routes with the authGuard({ redirectTo? }) middleware (401 for APIs, redirect for web). See docs/authentication.md.