Event Ledger

Append-only event ledger for Convex with per-subject activity feeds, cursor pagination, type/since filters, and retention cron — no external SaaS required.

Installation

npm install @vllnt/convex-events

About Event Ledger

Event Ledger is an append-only per-subject activity feed and event ledger as a Convex component: `record` stamps `createdAt` and inserts; events are never mutated. Feeds are queried newest-first by opaque `subjectRef`, with type/since filters, cursor pagination via `paginate`, and bounded counts that scan at most `maxCount + 1` rows and return `{ count, isExact }`. Retention runs automatically via an idempotent daily cron pruning past `retentionMs` and self-rescheduling batched purge; tables are sandboxed, `subjectRef`/`actorRef` are opaque strings the component never inspects, and auth stays with the host.

Benefits

Use cases

how to build an activity feed in Convex

The @vllnt/convex-events component provides a per-subject activity feed backed by an append-only ledger. Call events.record(ctx, subjectRef, type, opts) in a mutation to append an event, then call events.list or events.paginate in a query to read events back newest-first. The subjectRef is an opaque string the component never inspects, so it can be a user ID, document ID, or any host-defined identifier.

append-only event log with cursor pagination in Convex

The @vllnt/convex-events component implements an append-only ledger where events are stamped with createdAt at insert time and never mutated. The events.paginate method accepts Convex's paginationOptsValidator and returns the standard { page, isDone, continueCursor } shape, making it compatible with usePaginatedQuery. You can also narrow pages to a single event type or start from a given timestamp using the type and since options.

automatic event retention and expiry in Convex

The @vllnt/convex-events component includes a retention system configurable via events.configure(ctx, retentionMs). Once set, an idempotent daily cron prunes events older than the retention window. Large feeds are drained in self-rescheduling batches to avoid hitting Convex mutation time limits. You can also call events.pruneExpired manually for on-demand sweeps.

typed metadata on Convex events with validation

The Events class in @vllnt/convex-events is generic over a host-defined metadata type: new Events<Meta>(components.events, opts). You can pass an allowedTypes array, a metadataValidator, maxTypeLength, and maxMetadataBytes as client-boundary guards. Invalid input is rejected with a code-tagged EventValidationError before it reaches the database.

Frequently asked questions

How do I install and configure the @vllnt/convex-events component?

Install with npm install @vllnt/convex-events (or pnpm add @vllnt/convex-events). The peer dependency is convex@^1.41.0. Register the component in convex/convex.config.ts by calling app.use(events) where events is imported from @vllnt/convex-events/convex.config. Then instantiate the Events class in your host functions file with new Events(components.events) and call its methods inside your own mutations and queries.

Does @vllnt/convex-events handle authentication?

No. The @vllnt/convex-events component is auth-agnostic by design. The host application resolves identity, enforces authorization, and passes opaque subjectRef and actorRef strings into the component's methods. The component never inspects these values, so it works with any auth system the host uses.

What is the difference between events.list and events.paginate?

events.list returns a plain EventDoc array up to a configurable limit, suitable for simple feeds where you want the most recent N events. events.paginate accepts Convex's paginationOptsValidator and returns { page, isDone, continueCursor }, which is the correct shape for use with usePaginatedQuery and the useActivityFeed React hook from @vllnt/convex-events/react. Both methods support type and since filters.

How does the bounded count work in @vllnt/convex-events?

The events.count method scans at most maxCount + 1 rows and returns { count, isExact }. If the actual number of events exceeds maxCount, isExact is false and count reflects the scan limit rather than the true total. This avoids full-table scans on large feeds while still giving useful cardinality information for UI badges and indicators.

Can I use @vllnt/convex-events React hooks without coupling to the component's internal API?

Yes. The React hooks at @vllnt/convex-events/react accept the host's own re-exported query references rather than importing the component's internal API directly. You pass your api.events.paginate or api.events.count refs to useActivityFeed and useEventCount respectively. Both react and convex are optional peer dependencies, and the hooks are tree-shakeable.

Links