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.