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:

FieldRequiredDescription
actoryesWho did it — id (required), 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.
targetnoWhat it happened to — type (required if present), optional id and name.
metadatanoAny JSON object with extra context (roles, amounts, filenames).

You can also pass a timestamp to backfill historical events and a session_id to group events into a session.

Node.js / TypeScript

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',
  target: { type: 'workspace', id: workspace.id, name: workspace.name },
  metadata: { role: 'admin' },
});

// With await (if you need confirmation or the event ID)
const event = await log.track({ /* … */ });
console.log(event?.id); // evt_abc123

Options

OptionTypeDefaultDescription
secretKeystringrequiredYour stl_sk_… secret key
baseUrlstringhttps://api.softechlog.comOverride the API base URL
timeoutnumber5000Request timeout in ms
silentbooleanfalseSuppress console warnings on errors
Errors are always swallowed — a logging failure will never crash your app. Skip await on hot paths for zero added latency.

Python

The Python SDK works with FastAPI, Django, and Flask. 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",
    target_type="workspace",
    target_id=str(workspace.id),
    metadata={"role": "admin"},
)
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}

FastAPI middleware

Automatically attaches ip_address and user_agent to request state:

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

app.add_middleware(SoftechlogMiddleware, client=log)

Action naming convention

Stick to resource.verb and your event stream stays queryable as it grows:

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.