Docs

Next.js

App Router recipes: a shared server-side client, track() from route handlers and server actions, the serverless flush() gotcha, and the feed component in a React page.

1. Install and initialize

npm install @softechlog/node, put your secret key in .env.local as SOFTECHLOG_SECRET_KEY, and create one shared instance:

lib/softechlog.tsserver only
// lib/softechlog.ts — one instance, imported everywhere server-side
import { Softechlog } from '@softechlog/node';

export const log = new Softechlog({
  secretKey: process.env.SOFTECHLOG_SECRET_KEY!, // stl_sk_… — server only
});
Never import this module from a client component — the secret key must stay on the server. If you need browser events, use auto-capture with the public key instead.

2. Track from route handlers & server actions

app/api/members/route.tsroute handler
// app/api/members/route.ts
import { log } from '@/lib/softechlog';

export async function POST(req: Request) {
  const { user, invitee, workspace } = await parseAndAuthorize(req);

  await inviteMember(workspace, invitee);

  // Fire-and-forget: track() never throws
  log.track({
    actor: { id: user.id, name: user.name, email: user.email },
    action: 'workspace.member.invited',
    target: { type: 'workspace', id: workspace.id, name: workspace.name },
    metadata: { invited_email: invitee.email },
  });

  return Response.json({ ok: true });
}
app/settings/actions.tsserver action
// app/settings/actions.ts — server actions work the same way
'use server';
import { log } from '@/lib/softechlog';

export async function rotateApiKey(formData: FormData) {
  const user = await requireUser();
  const key = await doRotation(user);

  log.track({
    actor: { id: user.id, name: user.name, email: user.email },
    action: 'api_key.rotated',
    target: { type: 'api_key', id: key.id },
  });
}

3. The serverless gotcha: flush()

track() is fire-and-forget, which is what you want on a long-running server. On serverless runtimes the function can be frozen the moment you return — before the event leaves the box. Await flush() at the end of short-lived handlers:

serverless handlersflush()
// On serverless (Vercel/Lambda), the runtime can freeze before a
// fire-and-forget request finishes. Await flush() before returning
// from short-lived handlers:
log.track({ ... });
await log.flush();
return Response.json({ ok: true });

4. Feed token endpoint + feed component

Mint a short-lived, single-user token on the server:

app/api/activity-token/route.tsserver
// app/api/activity-token/route.ts
import { log } from '@/lib/softechlog';

export async function GET() {
  const user = await requireUser(); // your auth
  const { token, expires_at } = await log.feedToken({
    actorId: user.id,
    ttlSeconds: 3600,
  });
  return Response.json({ token, expires_at });
}

Then render the feed — <softechlog-feed> is a Web Component, so it drops into JSX directly:

app/settings/activity/page.tsxclient component
// app/settings/activity/page.tsx — feed in a client component
'use client';
import { useEffect, useRef } from 'react';
import Script from 'next/script';

export default function ActivityPage() {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    fetch('/api/activity-token')
      .then((r) => r.json())
      .then(({ token }) => ref.current?.setAttribute('feed-token', token));
  }, []);

  return (
    <>
      <Script src="https://softechlog.com/feed.js" strategy="afterInteractive" />
      {/* Web Components need the ignore flag in React <19 */}
      <softechlog-feed ref={ref} />
    </>
  );
}

Attributes, theming, and framework notes are in the Feed Component guide.