softechlog
Server-side Python SDK for Softechlog — drop-in user activity logging and customer-facing activity feeds for SaaS. Works with FastAPI, Django, Flask, or plain scripts. Fully typed.
Install
pip install softechlog
Quick start
# app/softechlog.py — initialize once, import everywhere
import os
from softechlog import Softechlog
log = Softechlog(secret_key=os.environ["SOFTECHLOG_SECRET_KEY"]) # stl_sk_… — server-side only
Track an event (sync — Django, Flask, scripts)
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
)
track() returns immediately: events are queued and sent by a background thread, so a slow or unreachable Softechlog can never stall your request handler. Call log.flush() before a short-lived process (a serverless function, a cron job) exits — log.close() and interpreter exit flush automatically. Pass background=False to send inline and get the created event back.
Track an event (async — FastAPI)
from fastapi import BackgroundTasks
@router.delete("/files/{file_id}")
async def delete_file(file_id: str, background: BackgroundTasks, current_user: User = Depends(get_current_user)):
file = await File.get(file_id)
await file.delete()
# Runs after the response is sent — the client never waits on Softechlog.
background.add_task(
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}
await log.atrack(...) directly is fine too when you want the event id back; it is bounded by timeout × (retries + 1) plus backoff.
track() / atrack() never raise — failures return None and log a warning (unless silent=True). Requests are retried on network errors, timeouts, 429 and 5xx with exponential backoff. Retries are safe: every event carries an Idempotency-Key, so an event the API stored before the response was lost is never recorded twice. Invalid actions and oversized metadata are rejected locally without a round-trip. Offset-less datetime timestamps are sent as UTC.
FastAPI middleware — record the end user's IP and User-Agent
Without it, the API only sees your server's address.
from softechlog.fastapi import SoftechlogMiddleware
app.add_middleware(SoftechlogMiddleware) # honours X-Forwarded-For; pass trust_proxy_headers=False if not behind a proxy
Every track()/atrack() call made while handling a request now carries context={"ip_address": …, "user_agent": …} automatically. You can also pass context= explicitly anywhere.
Show a feed to your users
The <softechlog-feed> web component needs a feed token — a short-lived credential scoped to one user. Mint it on your server:
@router.get("/activity-token")
async def activity_token(current_user: User = Depends(get_current_user)):
tok = await log.afeed_token(actor_id=str(current_user.id), ttl_seconds=3600)
return {"token": tok.token, "expires_at": tok.expires_at}
<script src="https://softechlog.com/feed.js"></script>
<softechlog-feed feed-token="stl_ft_…"></softechlog-feed>
B2B? Show a customer's admin their whole account. Track with tenant_id=str(org.id), then mint log.feed_token(tenant_id=str(org.id)) — the token reads every event in that account and nothing else. Pass actor_id too to show one member within it.
feed_token() / afeed_token() raise SoftechlogError on failure.
Erase a user (GDPR right to erasure)
Stop sending events for the user first, then:
erasure = log.erase("user_123", mode="delete", reference="DSR-2026-0142")
if erasure.status != "completed": # 202 → finishing in the background
erasure = log.wait_for_erasure(erasure.id)
mode:"delete"(default) removes their events;"redact"keeps what happened and when but removes who, IP, device, metadata and targets.target_types(default["user"]): target types whosetarget_idis this user, so mentions in other people's events are scrubbed.[]leaves them alone.reference: your ticket id — no personal data; it is stored and appears in the evidence event.get_erasure(id),list_erasures(actor_id=, status=, limit=, cursor=),wait_for_erasure(id, timeout=60, interval=1)(raises onfailedor timeout). Async:aerase,aget_erasure,alist_erasures,await_for_erasure.
The returned Erasure never contains the user id — store it with the ticket as your record. See softechlog.com/docs/erasure.
Tamper evidence
track() with background=False (and atrack()) return leaf_hash — the event's integrity leaf, sealed into a signed, hash-chained checkpoint within 5 minutes. Keep it for high-value events.
# Daily: pin the latest checkpoint somewhere you control (e.g. S3 Object Lock)
cp = log.latest_checkpoint() # None before the first seal
report = log.verify_integrity(pins=[(cp.seq, cp.hash)], expected_retention_days=90)
if not report.ok:
print(report.failures)
Also: integrity_status(), list_checkpoints(after_seq=, limit=), checkpoint_bundle(seq), integrity_keys(), each with an a-prefixed async twin.
Verify offline — softechlog.integrity
pip install "softechlog[verify]" # adds cryptography for Ed25519
from softechlog.integrity import verify_bundle, verify_chain
keys = log.integrity_keys()
page = log.list_checkpoints(limit=500)
verify_chain(page.checkpoints, keys, pins=my_pins) # VerifyResult(ok, failures, warnings, counts)
verify_bundle(log.checkpoint_bundle(42), keys)
Bundles contain your events' IPs and user agents — handle them like an export. Encoding and threat model: softechlog.com/docs/integrity.
Errors
track() / atrack() never raise. Everything else raises SoftechlogError, with .status (HTTP status, or None for network errors) and .body.
Options
| Option | Type | Default | Description |
|---|---|---|---|
secret_key |
str | required | Your stl_sk_… secret key |
base_url |
str | https://api.softechlog.com |
Override the API base URL |
timeout |
float | 5.0 |
Request timeout in seconds |
retries |
int | 2 |
Retries on network errors, 429, 5xx (backoff, honours Retry-After; idempotent) |
silent |
bool | False |
Suppress warnings on errors |
background |
bool | True |
Sync track() queues to a worker thread instead of sending inline |
queue_size |
int | 1000 |
Max queued events in background mode (oldest are kept, new ones dropped) |
transport |
— | — | Custom httpx transport (tests) |
The client keeps persistent HTTP connections; call log.close() / await log.aclose() on shutdown (or use it as a context manager).
Limits (enforced by the API)
action≤ 200 chars,actor.id≤ 255,metadata≤ 4 KB with finite numbers; thesoftechlog.action namespace is reserved- 60 erasure requests / minute per project; 10
verify_integrity()/checkpoint_bundle()calls / minute per project - 1,000 events / minute per secret key
- Free plan: 10,000 events / month (
402when exceeded)
MIT © FutureGenSystems
Metadata
Release files for softechlog 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| softechlog-1.3.0.tar.gz | 31.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| softechlog-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.3 kB
Release files / softechlog-1.3.0.tar.gz
| Download URL | softechlog-1.3.0.tar.gz |
|---|---|
| Size | 31.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c13f1cab755c313cb7f747196b25af1c5136cb27b48fa55dd9c304b70f1287ec
|
|
BLAKE2b-256 checksum How to use checksums |
b6afbbdee1720566b730e33b2ac35c80f7730739e461583324a20bb905bb9f1c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.6
|
Release files / softechlog-1.3.0-py3-none-any.whl
| Download URL | softechlog-1.3.0-py3-none-any.whl |
|---|---|
| Size | 23.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3a5f8f39fba76c32023e7ef2b57fea2d76fe9fa933045e3937b4f6c22bd779de
|
|
BLAKE2b-256 checksum How to use checksums |
70567b023b643b88b045405c317998953f4eeec30954bf0cb7b8f7fbdf52593a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.6
|