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 @futuregensystems/softechlog, put your secret key in .env.local as SOFTECHLOG_SECRET_KEY, and create one shared instance:
// lib/softechlog.ts — one instance, imported everywhere server-side
import { Softechlog } from '@futuregensystems/softechlog';
export const log = new Softechlog({
secretKey: process.env.SOFTECHLOG_SECRET_KEY!, // stl_sk_… — server only
});2. Track from route handlers & server actions
// 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.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:
// 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.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.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" />
{/* custom element — see softechlog-feed.d.ts below for the TypeScript types */}
<softechlog-feed ref={ref} />
</>
);
}Next.js type-checks during next build, and a custom element is unknown to JSX until you declare it — without the file below the build fails with Property 'softechlog-feed' does not exist on type 'JSX.IntrinsicElements':
// softechlog-feed.d.ts (anywhere in your tsconfig `include`)
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
type SoftechlogFeedProps = DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & {
'feed-token'?: string; 'target-type'?: string; 'target-id'?: string;
theme?: 'light' | 'dark' | 'auto'; limit?: number | string; locale?: string;
'empty-text'?: string; 'api-url'?: string;
};
declare global { // React 18
namespace JSX { interface IntrinsicElements { 'softechlog-feed': SoftechlogFeedProps } }
}
declare module 'react' { // React 19
namespace JSX { interface IntrinsicElements { 'softechlog-feed': SoftechlogFeedProps } }
}Attributes, theming, and framework notes are in the Feed Component guide.