Browse documentation

Everything here is in-process and ephemeral by design: no external collector, no persistence, history resets on restart. That is the trade for zero operational dependencies on a single-VPS deployment.

Note

The log buffer, request meter, metrics series and shield telemetry are all single-instance structures — as is rate limiting and realtime presence. Scheduled work is not: the realtime outbox and the bucket-deletion queue take database leases, so two processes never run the same job twice.

One API container per project is the supported topology, and the Shield report now says so when it is not: every connection is tagged motherbase-api:<instanceId>, runtime.instances counts them, and more than one raises a warning naming the multiplier. See apps/server/lib/instance.ts and known issues.

Logs

lib/log-buffer.ts is a fixed-capacity ring buffer (CONSOLE_LOG_BUFFER_SIZE, default 500) that emits an entry event so Studio can stream new lines live.

interface LogEntry { id: number; at: string; level: 'info'|'warn'|'error'; source: string; message: string }

Who writes to it:

SourceWriter
httpmiddlewares/request-logger.ts, one line per finished request
sqlSqlService.audit — every statement run from the SQL editor
edge, job, webhook, auth, storage, shield, realtimelib/activity-log.ts → logActivity(level, source, message)
eventsThe event dispatcher's own failures

The request logger deliberately skips /api/admin/*, /api/health-check, /studio and /console, so the Logs screen shows real external traffic instead of your own Studio browsing. Background work has no request line at all, which is exactly why logActivity exists: without it you could not tell "the backup never ran" from "the backup ran fine".

logActivity truncates at 2000 characters and never throws — a broken log write must not fail the operation it describes.

Request meter

services/admin/request-metrics.service.ts keeps a cumulative total plus a rolling 60 s window:

requestMeter.snapshot()  // { total, perMinute, windowSec }

It counts everything except health probes, and it runs before the Studio-noise skip, so admin API calls are still metered. It is a module-level singleton because the middleware runs outside the Awilix request scope.

Shield telemetry

lib/shield-telemetry.ts keeps the last 2000 traffic events and rate-limit hits, and derives:

  • Per-route metrics — count, errors, slow (> 1000 ms), avgMs, p95Ms.
  • Rate-limit hits — scope, bucket key, IP, user, method, path.
  • Abuse candidates — keys with the most requests/errors/rate-limit hits and their top path.

Each event is classified by path into a source: /api/auth → auth, /rest/v1 → rest, /api/edge → edge, /api/files → files, everything else api. For REST and edge paths the table (or function) name is extracted, and the HTTP method is mapped to select / insert / update / delete.

Metrics time series

services/admin/metrics.service.ts samples every 10 s into a 360-sample ring (~1 hour):

FieldMeaning
rpsRequests per second over the trailing 60 s
p95p95 latency (ms)
errorRateFraction of 4xx/5xx responses, 0–1
memMbResident set size
reqsRequest count in the window

The sampler is idempotent and unref'd, so it never keeps the event loop alive on its own.

Advisor

services/admin/advisor.service.ts runs catalogue queries against your database and returns findings with a category (security / performance) and level (critical / warning / info):

FindingCategory
Table is fully open over RESTsecurity · critical
Table has RLS on with no policysecurity · info
Policy depends on an unprotected tablesecurity · critical
Sensitive columns publicly readablesecurity · critical
Public read accesssecurity
Unauthenticated writes allowedsecurity
Authenticated write is not row-scopedsecurity
SECURITY DEFINER function callable by anonsecurity
Policy filters on an unindexed columnperformance · warning
Table without primary keyperformance
Foreign key without indexperformance
Unused indexperformance
Table needs vacuumperformance

Job ledger and alerts

services/admin/job-ledger.service.ts records every background job run:

await jobLedger.record('backup', async () => { … })   // wraps and times the run
await jobLedger.note('backup', 'failed', 'disk full') // record without wrapping

A JobRun is { id, job, startedAt, finishedAt, status: 'running'|'ok'|'failed', error }.

Alert rules (DEFAULT_ALERT_RULES) evaluate a snapshot of the ledger:

Alert kindDefault trigger
consecutive_failures3 failures in a row
dead_letter_depth100 items stuck
backup_older_than_rpoNewest backup older than 26 h (a daily backup with 2 h slack); 0 disables
missed_windowA job declared a maxIntervalMs and exceeded it

Alerts carry level (critical / warning), a human message, and a small machine-readable detail object — small on purpose, because it is embedded in webhook payloads. prune(retentionDays) trims the history.

Where to see it

All of the above is exposed under /api/admin/* (see Admin & HTTP API reference) and rendered in Studio's Logs, Reports and Security screens.