Better Activity

API Reference

Every method on the activity instance.

The betterActivity() call returns an instance with the methods below. All methods are async and fully typed against your entities map.

save

Persist a single event.

await activity.save({
  entity: "user",
  entityId: "usr_123",
  action: "logged_in",
  actorId: "usr_123",
  metadata: { ip: "1.2.3.4" },
});

Input

FieldTypeRequiredNotes
entitydeclared entity nameyesType-checked against entities.
entityIdstringyesID of the thing being acted on.
actiondeclared actionyesMust be in entities[entity].actions.
actorIdstring | nullnoWho performed the action.
actorTypestring | nullnoFree-form actor classifier (e.g. "user", "system").
metadataper-entity metadatanoTyped by entities[entity].metadata.
ipstring | nullnoRequest IP.
userAgentstring | nullnoRequest user agent.
requestIdstring | nullnoTrace correlation id.
idstringnoOverride the auto-generated id.
createdAtDatenoOverride the auto-generated timestamp.

Returns the persisted ActivityRecord.

saveMany

Bulk-insert events in a single round-trip. Accepts an array of the same inputs as save. All beforeSave hooks fire in parallel before the batch is committed.

await activity.saveMany([
  { entity: "user", entityId: "u1", action: "logged_in" },
  { entity: "user", entityId: "u2", action: "logged_in" },
]);

list

Filter + offset pagination. The default limit is 100 and the default sort order is descending by createdAt.

const events = await activity.list({
  entity: "project",
  entityId: "prj_456",
  after: "2026-01-01",
  before: new Date(),
  limit: 50,
  offset: 0,
  sortBy: "desc",
});
FieldTypeDefaultNotes
entityentity name
entityIdstring
actiondeclared actionFiltered by action.
actorIdstring
afterDate | ISO stringInclusive lower bound on createdAt.
beforeDate | ISO stringExclusive upper bound on createdAt.
limitnumber100
offsetnumber0
sortBy"asc" | "desc""desc"

Use list for ad-hoc queries and for the first page of a feed. For deep scrolling, prefer paginate.

paginate

Cursor-based pagination. The cursor is opaque and stable under concurrent inserts — paginate won't yield the same row twice even if new rows arrive between pages.

let cursor: string | undefined;
do {
  const page = await activity.paginate({
    entity: "user",
    cursor,
    limit: 100,
  });
  for (const event of page.items) handle(event);
  cursor = page.nextCursor ?? undefined;
} while (cursor);

Input

Same filter fields as list (minus offset), plus:

FieldTypeNotes
cursorstringOpaque cursor from a previous paginate call's nextCursor.

Return value

{
  items: ActivityRecord[];
  nextCursor: string | null;  // null when there's no more data
  hasMore: boolean;
}

byActor

Convenience wrapper for "all events authored by an actor". Sorted by createdAt descending.

const events = await activity.byActor({
  actorId: "usr_123",
  limit: 100,
});

between

Inclusive from, exclusive to. Returns events sorted by createdAt ascending, intended for chronological exports / reports.

const today = await activity.between({
  entity: "project",
  from: "2026-05-14T00:00:00Z",
  to:   "2026-05-15T00:00:00Z",
  limit: 1000,
});

count

Same filters as list, returns a single integer.

const total = await activity.count({ entity: "user", action: "logged_in" });

purge

Hard-delete events matching a filter. At least one filter is required — the SDK refuses to wipe the whole table. Audit logs are usually append-only; use this only for retention windows or test fixtures.

await activity.purge({
  entity: "user",
  before: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
});

Returns the number of rows deleted.

subscribe

Register an in-process listener that fires after every successful save(). Returns an unsubscribe function.

const off = activity.subscribe(async (event) => {
  await sendToWebhook(event);
});

off(); // detach

Subscriber errors are caught and logged (when debugLogs: true) so a single bad listener can't break save().

See Hooks & Subscribers for the full hook model.

use

Append a beforeSave hook at runtime. Returns a removal function.

const off = activity.use(async (ctx) => {
  if (ctx.input.entity === "user" && !ctx.input.actorId) {
    ctx.abort = { reason: "user events require an actorId" };
  }
});

adapter

The raw DBAdapter instance. Escape hatch — useful for adapter-specific behavior the SDK doesn't expose.

options

The fully-resolved options object (after defaults like tableName and strict are applied). Read-only.

$Infer

Type-only namespace exposing inferred record / input shapes:

type Event = typeof activity.$Infer.Record;
type Input = typeof activity.$Infer.SaveInput;

On this page