npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@feedclip/activity

v0.9.0

Published

Drop-in activity history and audit trail for React applications.

Downloads

1,097

Readme

Activity

npm provenance license

Live demo · Documentation · Production starter · StackBlitz · npm

Drop-in activity history for React applications, backed by a framework-independent engine and your own storage. Ship searchable, accessible audit trails for invoices, customers, tickets, orders, and other business resources without rebuilding the UI.

Activity History is the product; the framework-independent Activity Engine is the architecture underneath it.

Use the documentation hub for authentication, tenant isolation, framework recipes, troubleshooting, customization, performance and onboarding feedback.

Activity demo with the live configurator

The demo is the real package UI, not a static mock. Use the configurator to try themes, density, locale and accent color, then copy the generated JSX and CSS.

Install

npm install @feedclip/activity

Or generate a working integration inside an existing application:

npx @feedclip/activity init . --template react-memory
# production Next.js + PostgreSQL files
npx @feedclip/activity init . --template next-postgres

The command creates an ACTIVITY_SETUP.md checklist and refuses to overwrite existing generated files unless --force is provided.

React 18 or 19 is required only when using the React entrypoint.

Try the package in a clean browser environment with the StackBlitz installation example. The example installs the public npm release rather than importing SDK source files from this repository.

Five-minute quick start

Start with memory storage to add the UI without infrastructure. It is ideal for prototyping and tests; switch to PostgreSQL before deploying data that must survive a process restart.

import {
  createActivity,
  createMemoryStorageAdapter,
} from "@feedclip/activity";

const activity = createActivity({
  adapter: createMemoryStorageAdapter(),
});

await activity.track({
  resource: {
    type: "invoice",
    id: "inv_123",
    title: "Invoice INV-123",
  },
  actor: {
    type: "user",
    id: "user_1",
    name: "John Smith",
  },
  action: "update",
  changes: [
    {
      field: "status",
      label: "Status",
      before: "Draft",
      after: "Approved",
      valueType: "enum",
    },
  ],
});

const entries = await activity.query({
  resource: { type: "invoice", id: "inv_123" },
});

Render the same Activity instance in React:

import { ActivityPanel } from "@feedclip/activity/react";
import "@feedclip/activity/styles.css";

export function InvoiceActivity() {
  return (
    <ActivityPanel
      activity={activity}
      resource={{ type: "invoice", id: "inv_123" }}
    />
  );
}

That is the complete in-memory integration. See the executable memory example, or continue with PostgreSQL for persistence and the HTTP adapter when the panel runs in a browser.

Production starter

For a complete integration, use the Next.js + PostgreSQL production starter. It demonstrates signed sessions, resource authorization, tenant-isolated storage, server-derived actors, schema migration, and a browser panel that cannot forge activity records. It installs the public npm package and includes an executable five-minute setup, not pseudocode.

git clone https://github.com/andreyshedko/activity.git
cd activity/examples/nextjs
createdb activity_starter
cp .env.example .env.local
npm install
npm run db:migrate
npm run dev

SQLite

Use SQLite for desktop applications, local-first services, single-node deployments, and development environments. The adapter has no runtime driver dependency and accepts the synchronous prepare/run/get/all contract used by Node's built-in SQLite database:

import { DatabaseSync } from "node:sqlite";
import { readFileSync } from "node:fs";
import { createActivity } from "@feedclip/activity";
import { sqliteAdapter } from "@feedclip/activity/adapters/sqlite";

const database = new DatabaseSync("activity.db");
database.exec("pragma foreign_keys = on");
database.exec(
  readFileSync(new URL(import.meta.resolve("@feedclip/activity/sqlite-migration.sql")), "utf8"),
);

const activity = createActivity({ adapter: sqliteAdapter(database) });

The included schema stores timestamps as ISO 8601 text, JSON values as text, and identifiers as text. Cursor and offset pagination, filters, search, transactions, content, metadata, and changes have the same behavior as the PostgreSQL adapter. See the executable SQLite quick start.

MySQL

Use the MySQL adapter with a mysql2/promise pool. The database driver remains an application dependency, so Activity does not add a runtime dependency:

import mysql from "mysql2/promise";
import { createActivity } from "@feedclip/activity";
import { mysqlAdapter } from "@feedclip/activity/adapters/mysql";

const pool = mysql.createPool(process.env.MYSQL_URL!);
const activity = createActivity({ adapter: mysqlAdapter(pool) });

Apply @feedclip/activity/mysql-migration.sql with multiple statements enabled before tracking data. The adapter supports atomic inserts, search, filters, offset pagination, and stable cursor pagination. See the executable MySQL quick start.

Headless and reliability entrypoints

Build a custom interface with @feedclip/activity/headless: day grouping, adjacent-event collapsing, retention filtering, JSON/CSV export and a subscribable feed for optimistic or real-time host updates. Add redaction, lifecycle telemetry, bounded batch tracking and process-local idempotency from @feedclip/activity/reliability. See the headless and reliability guides.

Middleware

Use middleware for application policies and server-side enrichment before an activity record reaches storage:

import { ActivityError, createActivity } from "@feedclip/activity";

const activity = createActivity({
  adapter,
  middleware: [
    (entry) => ({
      ...entry,
      metadata: { ...entry.metadata, tenantId: currentTenant.id },
    }),
    (entry, context) => {
      if (!canTrack(entry.resource)) {
        throw new ActivityError("POLICY_DENIED", "Activity tracking is not allowed");
      }
      console.info(context.operation, entry.id);
      return entry;
    },
  ],
});

Middleware runs sequentially after validation and normalization but before the storage adapter. Each handler receives an immutable ActivityRecord and must return that record or a new valid record, synchronously or asynchronously. A failure stops the pipeline, so the adapter does not persist a partial event.

Lifecycle events

Attach observers for logging, metrics, or telemetry without coupling them to a storage adapter:

const activity = createActivity({
  adapter,
  listeners: [
    async (event) => {
      if (event.type === "afterTrack") {
        await metrics.increment("activity.tracked", {
          action: event.record.action,
        });
      }
      if (event.type === "trackFailed") {
        logger.error(event.error, "Activity tracking failed");
      }
    },
  ],
});

beforeTrack receives the final record after middleware and before persistence. afterTrack runs after a successful insert. trackFailed reports validation, middleware, and storage failures and includes a record when one was constructed. Events and listener arrays are immutable snapshots. Listener failures are isolated: every listener still runs, tracking keeps its original outcome, and a persisted record is never rolled back because an observer failed.

React

The Activity instance is passed explicitly. No provider is required.

Use messages to replace UI copy and locale to format dates and time:

<ActivityPanel
  activity={activity}
  locale="cs-CZ"
  messages={{ title: "Aktivita", searchPlaceholder: "Hledat..." }}
  resource={{ type: "invoice", id: "inv_123" }}
/>

Custom entry actions

Add product-specific actions without teaching Activity about your domain. Each callback receives the immutable ActivityRecord for that row:

<ActivityPanel
  activity={activity}
  entryActions={[
    {
      id: "open-invoice",
      label: "Open invoice",
      onSelect: (entry) => openInvoice(entry.resource.id),
    },
    {
      id: "restore",
      label: "Restore",
      isVisible: (entry) => entry.action === "archive",
      isDisabled: (entry) => entry.actor.type === "system",
      onSelect: restoreFromEntry,
    },
  ]}
  resource={{ type: "invoice", id: "inv_123" }}
/>

Actions are keyboard-accessible. Supply icon with any React node when an icon is preferred; label remains the accessible name and tooltip.

Detail views and deep links

Use expandedEntryId when the selected event belongs in the URL. Activity keeps the detail view accessible and inline; the host application remains in control of routing:

const [expandedEntryId, setExpandedEntryId] = useState<string | null>(
  new URLSearchParams(location.search).get("activity"),
);

<ActivityPanel
  activity={activity}
  expandedEntryId={expandedEntryId}
  onExpandedEntryChange={(entryId) => {
    setExpandedEntryId(entryId);
    const url = new URL(location.href);
    entryId ? url.searchParams.set("activity", entryId) : url.searchParams.delete("activity");
    history.replaceState(null, "", url);
  }}
  resource={{ type: "invoice", id: "inv_123" }}
/>

Every rendered record also receives a stable DOM target in the form activity-entry-{record.id}. Without expandedEntryId, expansion remains local and multiple records can be opened independently, preserving existing behavior.

Browser-to-server HTTP adapter

Keep PostgreSQL credentials on the server. The browser uses the HTTP adapter:

import { createActivity } from "@feedclip/activity";
import { httpAdapter } from "@feedclip/activity/adapters/http";

const activity = createActivity({
  adapter: httpAdapter({
    endpoint: "/api/activity",
    headers: () => ({ authorization: `Bearer ${getSessionToken()}` }),
  }),
});

<ActivityPanel activity={activity} pageSize={20} resource={resource} />;

On a server with the standard Fetch API, connect the same endpoint to storage:

import { postgresAdapter } from "@feedclip/activity/adapters/postgres";
import { createActivityHttpHandler } from "@feedclip/activity/http";

const handleActivity = createActivityHttpHandler({
  adapter: postgresAdapter(db),
  authorize: async ({ request, operation, resource }) => {
    const session = await requireSession(request);
    return canAccessResource(session, operation, resource);
  },
});

export const GET = handleActivity;
export const POST = handleActivity;

authorize is required and runs before every query or insert. The handler revalidates incoming records rather than trusting browser payloads. See the examples/nextjs browser → route handler → PostgreSQL flow and the smaller executable HTTP example.

Pagination

activity.query() remains compatible and returns an array. Use queryPage() when totals and continuation state are required:

const page = await activity.queryPage?.({ resource, limit: 20, offset: 0 });
// { entries, total, hasMore }

ActivityPanel pageSize={20} renders an accessible Load more action and appends subsequent pages without replacing entries already on screen.

For streams that can receive new events while a user is paging, opt into cursor pagination so newly inserted rows cannot shift page boundaries:

<ActivityPanel
  activity={activity}
  pageSize={20}
  paginationMode="cursor"
  resource={{ type: "invoice", id: "inv_123" }}
/>

The same opaque cursor can be used without React:

const first = await activity.queryPage?.({ resource, limit: 20 });
const second = await activity.queryPage?.({
  resource,
  limit: 20,
  cursor: first?.nextCursor,
});

cursor and offset are mutually exclusive. Cursors are transport-safe and opaque to consumers; pass nextCursor back unchanged.

Attachments

Activity records attachment metadata; file upload, malware scanning, access control, and download delivery stay in the host application. Configure validation when creating the engine:

const activity = createActivity({
  adapter,
  attachmentPolicy: {
    maxSizeBytes: 25 * 1024 * 1024,
    allowedMimeTypes: ["image/*", "application/pdf"],
    allowedUrlProtocols: ["https:"],
  },
});

ActivityPanel does not navigate to stored URLs. Handle attachment access in the application, reauthorize the user, and generate a fresh short-lived URL:

<ActivityPanel
  activity={activity}
  onAttachmentOpen={(attachment, entry) => {
    openAuthorizedDownload(entry.resource, attachment);
  }}
  resource={{ type: "invoice", id: "inv_123" }}
/>

See the public security guidance, runtime compatibility, and API stability policy on GitHub. Release history is maintained in the bundled changelog.

Themes

Use the theme prop to select the built-in light or dark theme, or follow the operating-system preference:

<ActivityPanel
  activity={activity}
  resource={{ type: "invoice", id: "inv_123" }}
  theme="system"
/>

Accepted values are "light" (the default), "dark", and "system". Themes are implemented entirely with CSS custom properties. Override them after the package stylesheet to match your product:

.activity-panel[data-activity-theme="dark"] {
  --activity-color-surface: #09090b;
  --activity-color-text: #e4e4e7;
  --activity-color-accent: #34d399;
  --activity-color-border: #3f3f46;
}

Loading, empty, and error states

The panel keeps existing entries visible while a changed search or filter is refreshing. Initial loading uses a skeleton sized for the selected variant. The default error state includes a retry button.

Applications can replace empty and error content while retaining the panel's query lifecycle:

<ActivityPanel
  activity={activity}
  renderEmpty={({ hasQuery }) => (
    <MyEmptyState filtered={hasQuery} />
  )}
  renderError={({ error, retry }) => (
    <MyErrorState error={error} onRetry={retry} />
  )}
  resource={{ type: "invoice", id: "inv_123" }}
/>

PostgreSQL

Install a PostgreSQL client in the server application:

npm install pg

Apply the bundled @feedclip/activity/migration.sql with your migration tool, then create an adapter around the pool. Database credentials must remain on the server:

import pg from "pg";
import { createActivity } from "@feedclip/activity";
import { postgresAdapter } from "@feedclip/activity/adapters/postgres";

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const activity = createActivity({
  adapter: postgresAdapter(pool),
});

The migration is exported as @feedclip/activity/migration.sql and also lives at migrations/001_activity_schema.sql. The executable PostgreSQL quick start shows migration loading, writing, and paginated reading end to end. Review the public migration guidance before applying database changes.

Public entrypoints

  • @feedclip/activity — engine, types, and memory adapter
  • @feedclip/activity/reactActivityPanel
  • @feedclip/activity/adapters/memory
  • @feedclip/activity/adapters/postgres
  • @feedclip/activity/adapters/sqlite
  • @feedclip/activity/adapters/mysql
  • @feedclip/activity/adapters/http
  • @feedclip/activity/http — Fetch-compatible server handler
  • @feedclip/activity/styles.css
  • @feedclip/activity/migration.sql
  • @feedclip/activity/sqlite-migration.sql
  • @feedclip/activity/mysql-migration.sql

Status

Activity is still pre-1.0. The public contracts are usable, but the package has not yet reached a stable 1.0 compatibility guarantee.

License

MIT