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.
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:
| Source | Writer |
|---|---|
http | middlewares/request-logger.ts, one line per finished request |
sql | SqlService.audit — every statement run from the SQL editor |
edge, job, webhook, auth, storage, shield, realtime | lib/activity-log.ts → logActivity(level, source, message) |
events | The 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):
| Field | Meaning |
|---|---|
rps | Requests per second over the trailing 60 s |
p95 | p95 latency (ms) |
errorRate | Fraction of 4xx/5xx responses, 0–1 |
memMb | Resident set size |
reqs | Request 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):
| Finding | Category |
|---|---|
| Table is fully open over REST | security · critical |
| Table has RLS on with no policy | security · info |
| Policy depends on an unprotected table | security · critical |
| Sensitive columns publicly readable | security · critical |
| Public read access | security |
| Unauthenticated writes allowed | security |
| Authenticated write is not row-scoped | security |
SECURITY DEFINER function callable by anon | security |
| Policy filters on an unindexed column | performance · warning |
| Table without primary key | performance |
| Foreign key without index | performance |
| Unused index | performance |
| Table needs vacuum | performance |
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 wrappingA JobRun is { id, job, startedAt, finishedAt, status: 'running'|'ok'|'failed', error }.
Alert rules (DEFAULT_ALERT_RULES) evaluate a snapshot of the ledger:
| Alert kind | Default trigger |
|---|---|
consecutive_failures | 3 failures in a row |
dead_letter_depth | 100 items stuck |
backup_older_than_rpo | Newest backup older than 26 h (a daily backup with 2 h slack); 0 disables |
missed_window | A 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.