Keel's ORM is a compact active record over the query builder: a model is a class pointed at a table, and its rows come back as typed objects with methods. There's no mapper to configure and no separate schema layer — a model is the row plus behaviour. It runs on whatever connection you registered, so the same code works on Node and the edge.
import { Model } from "@shaferllc/keel/core";
class User extends Model {
static table = "users";
declare id: number;
declare email: string;
posts() { return this.hasMany(Post); }
}
const user = await User.find(1);
await user.posts(); // relations are awaitable
if (await user.subscribed()) { /* … */ }
This page is the map; each capability has a deep-dive in Models.
What the ORM gives you
| Area | What you get | Guide |
|---|---|---|
| CRUD | find / all / create / save / update / delete, firstOrCreate, updateOrCreate |
Models → Reading/Writing |
| Casts | boolean / int / json / date … columns round-trip as real JS types |
Models → Attribute casts |
| Mass assignment | fillable / guarded allow/deny lists guard untrusted input |
Models → Mass assignment |
| Serialization | hidden / visible / appends shape toJSON() |
Models → Serializing |
| Relationships | hasOne / hasMany / belongsTo / belongsToMany + polymorphic morphOne / morphMany / morphTo |
Models → Relationships |
| Eager loading | with("posts.comments") (nested), withCount, Model.load — no N+1 |
Models → Eager loading |
| Relationship queries | whereHas / has / doesntHave |
Models → Querying relationships |
| Lifecycle events | creating/saved/deleting/… hooks and observers, inherited by subclasses |
Models → Lifecycle events |
| Scopes | global scopes (tenancy, published-only) + local scope methods | Models → Query scopes |
| Soft deletes | deleted_at, withTrashed / onlyTrashed / restore / forceDelete |
Models → Soft deletes |
How it relates to the rest
- The query builder is the layer underneath.
Model.query()returns a model-aware builder, and everything an ORM query can't express (raw joins, aggregates, bulk writes) is onedb()call away. - Migrations define the tables models read and write.
- Factories & seeders generate model rows for tests and demos.
- API resources turn models into a REST API; transformers control their serialized shape at the boundary.
When to drop down
The ORM is deliberately small — enough for CRUD, relationships, and the common
query shapes without an ORM dependency. For a gnarly one-off report, reach for
the query builder or a raw connection().select(sql); the
model layer never gets in the way.
A worked example
A blog with authors and posts — enough to see CRUD, a relation, and eager loading together:
import { Model } from "@shaferllc/keel/core";
class Post extends Model {
static table = "posts";
static fillable = ["title", "body"];
declare id: number;
declare title: string;
declare user_id: number;
author() { return this.belongsTo(User); }
}
class User extends Model {
static table = "users";
declare id: number;
declare email: string;
posts() { return this.hasMany(Post); }
}
const ada = await User.create({ email: "ada@example.com" });
await ada.posts().create({ title: "Notes on engines", body: "…" });
const withPosts = await User.with("posts").where("id", ada.id).first();
for (const post of (withPosts as User & { posts: Post[] }).posts) {
console.log(post.title);
}
For the full surface — casts, soft deletes, scopes, polymorphic relations — see Models.