Ecto-inspired database toolkit for Bun. Functional, type-safe, zero external dependencies.
| Package | Description |
|---|---|
| Verb | Fast web framework for Bun |
| Hull | Ecto-inspired database toolkit (this repo) |
| Allow | Authentication library |
| Hoist | Deployment platform |
bun add @verb-js/hullUses Bun's built-in Bun.sql for PostgreSQL, MySQL, and SQLite.
import {
schema,
changeset, cast, validateRequired, validateLength,
from, whereEq, orderBy, limit,
connect, all, one, insert
} from "@verb-js/hull"
// Define schema (fluent builder - like Ecto)
const User = schema("users")
.uuid("id", { primaryKey: true })
.string("email", 255, { unique: true })
.string("name", 100)
.integer("age", { nullable: true })
.boolean("active", { default: true })
.timestamps()
const Post = schema("posts")
.uuid("id", { primaryKey: true })
.references("userId", "users")
.string("title", 200)
.text("body")
.boolean("published", { default: false })
.timestamps()
// Type inference
type User = InferRow<typeof User>
// => { id: string, email: string, name: string, age: number | null, ... }
// Changeset (validate + cast)
const cs = validateLength(
validateRequired(
cast(changeset(User, {}), params, ["email", "name", "age"]),
["email", "name"]
),
"email", { min: 5 }
)
// Query (composable)
const query = limit(
orderBy(
whereEq(from(User), "active", true),
"createdAt", "desc"
),
10
)
// Execute
const repo = connect({ url: process.env.DATABASE_URL! })
const users = await all(repo, query)
const user = await one(repo, whereEq(from(User), "id", "123"))
const newUser = await insert(repo, cs)import { defineMigration, createTable, dropTable } from "@verb-js/hull"
export default defineMigration(
"20241222_create_users",
"Create users table",
async (repo) => {
await createTable(repo, "users", {
id: { type: "UUID", primaryKey: true, default: "gen_random_uuid()" },
email: { type: "VARCHAR(255)", unique: true },
name: { type: "VARCHAR(100)" },
createdAt: { type: "TIMESTAMPTZ", default: "now()" },
})
},
async (repo) => {
await dropTable(repo, "users")
}
)# Ecto (Elixir)
schema "users" do
field :email, :string
field :name, :string
field :age, :integer
timestamps()
end// Hull (TypeScript)
const User = schema("users")
.string("email", 255)
.string("name", 100)
.integer("age")
.timestamps()Hull supports PostgreSQL and SQLite through Bun's built-in Bun.sql:
// PostgreSQL (production)
const repo = connect({ url: "postgres://user:pass@localhost:5432/myapp" })
// SQLite (local development)
const repo = connect({ url: "sqlite:///path/to/dev.db" })The dialect is auto-detected from the connection URL. SQLite is great for local development - same schema definitions work with both databases.
Sync your schemas directly to the database without writing migration files:
import { connect, sync } from "@verb-js/hull"
import { User, Post } from "./schema"
const repo = connect({ url: process.env.DATABASE_URL! })
// Creates tables and adds missing columns automatically
await sync(repo, [User, Post])- schema - Define tables with fluent builder
- changeset - Validate and cast data
- query - Composable query builder
- repo - Execute queries with Bun.sql
- migration - Schema versioning + auto-sync