Docs

Manual Tracking

Emit events from your backend wherever meaningful actions happen — invites, uploads, plan changes, deletions. One consistent shape, two SDKs, zero risk to your app.

The event shape

Every event answers four questions, plus a few optional extras:

FieldRequiredDescription
actoryesWho did it — id (required, ≤ 255 chars), plus optional name, email, avatar_url. Actors are upserted automatically; the same id always maps to the same user record.
actionyesWhat happened, in resource.verb form (e.g. member.invited). Lowercase letters, digits, and underscores, with at least one dot; ≤ 200 chars.
targetnoWhat it happened to — type, id, name (all optional).
metadatanoAny JSON object with extra context (roles, amounts, filenames). ≤ 4 KB.
timestampnoISO 8601 time the action happened — defaults to now. Use it to backfill history.
session_idnoGroups events into a session (any stable string ≤ 255 chars — a UUID is preferred; other strings are mapped to a stable UUID). Sessions are created automatically.
contextnoThe end user's ip_address and user_agent. Without it the API can only see your server's address.

A successful call returns { id, queued_at } — event IDs are UUIDs.

Node.js / TypeScript

Requires Node 18+ (uses the global fetch). Install with npm install @softechlog/node.

api/members.ts@softechlog/node
import { log } from '@/lib/softechlog';

// Fire-and-forget (non-blocking — recommended)
log.track({
  actor: { id: req.user.id, name: req.user.name, email: req.user.email },
  action: 'member.invited',                                    // resource.verb
  target: { type: 'workspace', id: workspace.id, name: workspace.name },
  metadata: { role: 'admin' },                                 // any JSON, ≤ 4 KB
  sessionId: req.session.id,                                   // optional: groups events into a session
  context: { ip_address: req.ip, user_agent: req.get('user-agent') }, // optional: the *end user's* context
});

// With await (if you need confirmation or the event ID)
const event = await log.track({ /* … */ });
console.log(event?.id); // "9b2f0f5e-…" (UUID) or null if delivery failed

Options

OptionTypeDefaultDescription
secretKeystringrequiredYour stl_sk_… secret key. The constructor throws if it is missing or malformed.
baseUrlstringhttps://api.softechlog.comOverride the API base URL
timeoutnumber5000Per-request timeout in ms
retriesnumber2Retries on network errors, 429 and 5xx (exponential backoff, honours Retry-After)
silentbooleanfalseSuppress console warnings on errors
fetchfunctionglobal fetchCustom fetch implementation (tests, polyfills)
track() never throws — network failures, timeouts and API errors resolve to null (and log a warning unless silent). Invalid actions and oversized metadata are rejected locally without a round-trip. Skip await on hot paths for zero added latency.

Serverless

Await log.flush() before returning so background track() calls finish:

handler.tsflush()
export async function handler(event) {
  log.track({ /* … */ });
  // …
  await log.flush(); // wait for background track() calls before the runtime freezes
}

The client also exposes feedToken({ actorId, targetType?, targetId?, ttlSeconds? }), which returns { token, expires_at, actor_id } for the feed component. Unlike track(), it throws on failure — a missing token is a rendering bug you want to see.

Python

Install with pip install softechlog. Works with FastAPI, Django, Flask, or plain scripts; fully typed. Use track() in sync code and atrack() in async code:

app/members.pysoftechlog · sync
from app.softechlog import log

log.track(
    actor={"id": str(user.id), "name": user.name, "email": user.email},
    action="member.invited",                 # resource.verb
    target_type="workspace",
    target_id=str(workspace.id),
    target_name=workspace.name,
    metadata={"role": "admin"},              # any JSON, ≤ 4 KB
    session_id=request.session.session_key, # optional: groups events into a session
)
app/files.pysoftechlog · async
@router.delete("/files/{file_id}")
async def delete_file(file_id: str, current_user: User = Depends(get_current_user)):
    file = await File.get(file_id)
    await file.delete()
    await log.atrack(
        actor={"id": str(current_user.id), "name": current_user.name},
        action="file.deleted",
        target_type="file",
        target_id=file_id,
        target_name=file.name,
    )
    return {"deleted": True}

Options

OptionTypeDefaultDescription
secret_keystrrequiredYour stl_sk_… secret key
base_urlstrhttps://api.softechlog.comOverride the API base URL
timeoutfloat5.0Request timeout in seconds
retriesint2Retries on network errors, 429, 5xx (backoff, honours Retry-After)
silentboolFalseSuppress warnings on errors

track() / atrack() accept actor, action, target_type, target_id, target_name, metadata, timestamp, session_id and context, never raise, and return None on failure. feed_token() / afeed_token() raise SoftechlogError. The client keeps persistent HTTP connections — call log.close() / await log.aclose() on shutdown, or use it as a context manager.

FastAPI middleware

Records the end user's IP and User-Agent. Every track()/atrack() call made while handling a request automatically carries context={"ip_address": …, "user_agent": …}; without it the API only sees your server's address.

app/main.pysoftechlog.fastapi
from softechlog.fastapi import SoftechlogMiddleware

# honours X-Forwarded-For; pass trust_proxy_headers=False if not behind a proxy
app.add_middleware(SoftechlogMiddleware)

Limits enforced by the API

  • action ≤ 200 chars, actor.id ≤ 255 chars, metadata ≤ 4 KB, session_id ≤ 255 chars.
  • 1,000 requests per minute per secret key (429 with a Retry-After header — the SDKs retry automatically).
  • Monthly plan caps: 10,000 (Free), 250,000 (Growth), 2,000,000 (Scale) events. Over the cap the API returns 402 until the 1st of next month; nothing is deleted and reads keep working. See Pricing.

Action naming convention

Stick to resource.verb and your event stream stays queryable as it grows — you can later filter with a prefix like action=member.*:

Action namesconvention
member.invited       member.removed      member.role_changed
file.uploaded        file.deleted        file.downloaded
project.created      project.archived    project.deleted
payment.succeeded    payment.failed
plan.upgraded        plan.downgraded

Start with the workflows that matter for support and audit — invites, billing, security actions — and expand coverage incrementally.