Introduction
A fully-typed activity and audit log library for TypeScript.
better-activity is a framework-agnostic, multi-database activity / audit
log library for TypeScript. You declare your entities and the actions they
support once, and the SDK gives you a type-safe save() / list() /
paginate() API on top of the database you already use.
It's modeled on the architecture of
better-auth: a small core,
adapters for every popular database, and zero magic between you and the
data.
Why better-activity?
Audit logs almost always start as a one-off table glued together with a
custom save_event(...) helper. As soon as you have more than a couple of
event types, that helper starts collecting bugs: typos in action names,
missing fields, inconsistent metadata shapes, and queries that scan the
whole table.
better-activity solves that with three ideas:
- A declared entity registry. Every entity gets a fixed set of allowed actions and an optional metadata type. Misuses are caught at compile time.
- One core, many backends. Postgres, MySQL, SQLite, MongoDB, Drizzle, Prisma, Kysely, and an in-memory adapter for tests — all behind the same API.
- Boring, indexed schema. The
activitytable is intentionally narrow and ships with the indexes you actually need.
Features
Type-safe per entity
Per-entity action and metadata constraints, checked at compile time.
Eight adapters
Postgres, MySQL, SQLite, MongoDB, Drizzle, Prisma, Kysely, memory.
Cursor pagination
Stable cursors that don't drift under concurrent inserts.
Hooks & subscribers
beforeSave / afterSave and in-process subscribers.
PII redaction
Scrub fields by dot-path before they hit the database.
CLI
Generate or apply the schema for the configured adapter.
A 30-second taste
import { betterActivity } from "better-activity";
import { postgresAdapter } from "better-activity/adapters/postgres";
import { Pool } from "pg";
export const activity = betterActivity({
database: postgresAdapter({ pool: new Pool() }),
entities: {
user: {
actions: ["created", "updated", "logged_in", "logged_out"],
metadata: {} as { ip?: string; userAgent?: string },
},
project: {
actions: ["created", "archived", "member_added"],
},
},
});
await activity.save({
entity: "user",
entityId: "usr_123",
action: "logged_in",
actorId: "usr_123",
metadata: { ip: "1.2.3.4" },
});
const events = await activity.list({
entity: "project",
entityId: "prj_456",
limit: 50,
});