Browse documentation

This page exists so the places where Motherbase is not what a careful reader would expect are written down rather than discovered. Everything here was verified against the source, not inferred.

The last audit found six items. All six are fixed; each is listed below with the check that now keeps it fixed, because a fix with nothing holding it in place is a fix with a half-life. What remains is one deliberate constraint, at the bottom.


Fixed

rpcSecurityDefiner could not be set from Settings

RestApiService read an rpcSecurityDefiner field from the api.rest settings group, but the group declared only maxRows and its Zod schema stripped unknown keys on write — so the field could never be stored and REST_RPC_SECURITY_DEFINER was the only working way to allowlist a SECURITY DEFINER function. The read path looked operator-configurable and was not.

The root cause was more general than one field: SettingsService.get() consulted a group's env fallback only when the whole row was missing, so any field added to an already-saved group read as unset and silently ignored its environment variable. Absent keys are now seeded from the fallback individually, while anything the row does carry still wins — which is what keeps "clear it in Studio" meaning cleared.

The field is declared, validated and rendered in Studio. See settings reference and environment.

Held by: settings-service.unit.test.ts (round-trip, the added-field migration, and that a stored empty value overrides the env rather than falling back to it) and rest-api-rpc.integration.test.ts (the settings path actually gates the call).

GET / returned Thanks for post

A leftover placeholder, and the first thing anyone probing the host saw. It now answers with a small service descriptor naming /api/docs and /api/health-check — deliberately without a version, which is an infoleak with no caller who needs it.

The admin route table was hand-maintained while claiming to be generated

It had drifted: the four controllers that mount on a HTTP_ROUTES constant were documented with their unresolved paths — /:model instead of /rest/v1/:model, / instead of /realtime/v1/api/broadcast — so every path copied out of the docs 404'd. HEAD /rest/v1/:model was missing entirely, and several guard chains were truncated mid-token (streamUpload.single('file'), the fingerprint of a throwaway script.

There is now a real extractor (apps/server/lib/route-manifest.ts) that parses the decorators and resolves the constant base paths, npm run docs:routes writes docs/admin-routes.md, and the docs site splices that file in. See Admin & HTTP API reference.

Held by: route-manifest.unit.test.ts — it asserts the resolved paths, rejects any unresolved one, checks guard chains are balanced, and fails with "run npm run docs:routes" whenever the committed table no longer matches the code.

createSignedUrl(path, expiresIn) ignored expiresIn

The parameter was literally named _expiresIn and never read. It is now threaded through /api/files/:id/url to the driver, validated (1 second to 7 days, the AWS SigV4 ceiling) and rejected rather than clamped — a caller asking for 30 days wants a 30-day URL, and quietly returning a 7-day one is the same class of bug as ignoring the argument.

The response also carries expiresAt, which is null when nothing expires: a public object's URL is permanent, and a private object on the local driver resolves to the authenticated streaming endpoint, which is bound to the session rather than to a clock. Callers can now tell the difference instead of assuming.

Held by: storage-signed-url-expiry.unit.test.ts.

The rate-limit window needed a restart

express-rate-limit reads windowMs once at construction, so five of the six knobs in security.rateLimit applied live and the sixth silently did not. windowMin is now a field on the group, and the limiters rebuild when it changes — eagerly, on save, never inside a request handler.

Rebuilding resets the counters, which is the honest behaviour: a count of 40 means nothing once the period it was counted over has changed underneath it. A save that does not change the window does not rebuild, or an operator could hand every throttled caller a fresh allowance just by pressing Save.

Held by: rate-limit.unit.test.ts, including a live express app proving the new window is really in force and that a no-op save does not reset anyone.

Development error responses were reachable from the network

Outside production an unrecognised error answers with error.stack. That is deliberate and useful locally, and it is exactly what must not be reachable from a network.

The server now binds to 127.0.0.1 unless NODE_ENV=production — every containerised path already sets that, so this only affects a developer's own machine. BIND_HOST widens it explicitly, and doing so outside production prints a warning naming the consequence. The leak is prevented by the bind rather than by a note nobody reads.


Deliberate: one API instance per project

Rate limiting, the custom rate-limit rules, the log buffer, request metrics, shield telemetry and realtime presence all keep their state in the process. Scheduled work does not share that constraint — the realtime outbox and the bucket-deletion queue take database leases, so two processes can never run the same job twice.

Run two replicas of one project and each enforces its own share of every limit, holds a different slice of the logs, and sees a different presence set. Nothing corrupts; the numbers just quietly stop meaning what they say.

This is not going to be fixed with a shared store. Motherbase's premise is one VPS and no moving parts, and adding Redis to every project to support a topology the product does not offer is the wrong trade — you scale by adding projects, not replicas.

What changed is that it is no longer silent. Every database connection is tagged motherbase-api:<instanceId>, so the Shield report counts the live instances and raises a warning naming the multiplier and the exact subsystems that stopped being accurate:

{
  "runtime": {
    "instanceId": "9f2c1a04",
    "instances": 2,
    "perInstanceSubsystems": ["rate limiting", "log buffer", "…"]
  }
}

The count degrades to null if pg_stat_activity cannot be read — it is a diagnostic, and it must never be the reason the Shield screen fails.

Held by: shield-replicas.unit.test.ts.


How to use this page

If you are evaluating Motherbase: nothing above is an authorization or data-loss defect, and the one remaining item is a documented property of the deployment model rather than a bug.

If you are contributing: each fixed item names the test that holds it. If you find yourself deleting one of those tests to make a change pass, you are re-opening the issue above it.