Skip to content

Audit trails, logs and analytics

Rule. Every state change writes an audit entry. Audit is append-only. Evidence is immutable. Logs are structured and carry no secrets or personal data.

Why. The product exists to prove who did what. If history can be edited, or logs leak credentials, the proof and the privacy are both worthless.

How to apply.

  • One audit service. Write through a single function (apps.audit.services.record, recordAuditEvent, recordSettingsChange), inside the same transaction as the change. Settings changes record the changed field names, never values.
  • Append-only in the model and, where the stack allows, by database trigger. Archiving a space keeps its trail; only an explicit, audited purge removes it.
  • One module owns each state machine (request or loan status), and one module owns quantity maths (availability never below zero). Web, apps and admin pages all call them; nothing sets a status directly.
  • Evidence is immutable. Photo rows and QR scan records cannot be edited or deleted once attached. Photo bytes may expire under the retention policy; the row, remarks, scans and audit trail stay. Uploads land on a staging key; a workflow promotes them exactly once.
  • Hard rules: issuing needs a scan and an issue photo; returning needs a photo and a remark.
  • Structured logs. JSON lines in production with UTC time, level, logger, message and a request id (accept a clean X-Request-ID, else generate one, echo it in the response, copy it into background jobs).
  • Never log passwords, API keys, bearer tokens, provider credentials, invitation or reset links, contact details or free text. Use ids, action keys and outcomes.
  • Health: a dependency-free liveness endpoint, protected readiness and metrics endpoints with generic “unauthorized” answers, Prometheus metrics labelled by route template (never raw URLs).
  • Outbound email and webhooks are queued (an outbox), retried safely, and emitted only after the transaction commits.

Across the org. SpaceWorks: audit service with database triggers, append-only evidence, request id logging, audit action reference. Workspace, Events, Machines, Inventory, Bookings, Organization: audit helper and audit log screen in the app; Inventory states evidence, the stock ledger and audit log are append-only. Database triggers on the Next.js apps’ audit tables: not checked. Android and iOS: audit is server-side; the apps only send evidence through the server’s workflow.


Rule. Every product has in-house, counts-only reports that are always on, and every product supports the same optional list of outside providers. Providers are off by default, need consent and a configured scope, and never receive personal data.

Why. Operators need to know how their space is used; visitors need to know nothing about them leaves the server without their say. One shared list means one set of docs, one privacy review and comparable numbers across web, iOS and Android.

How to apply.

8.1 In-house (always on).

  • Reports, charts and CSV/XLSX exports from the product’s own database, scoped to the tenant and the role.
  • Counts, never people: how many, by channel and by day, not who. Member details in analytics are off by default.
  • The server counts web, iOS and Android use itself from authenticated API calls, so the apps need no extra code. Apps may send a build string (X-SpaceWorks-App-Version) on API calls; it must never carry a person, device or account identifier.
  • Read-only dashboards: a per-tenant read-only database login over curated views with row-level security feeds Metabase or Grafana.
  • Platform operators also get a deployment dashboard (health, jobs, storage, API monitor) and optional Prometheus metrics behind a bearer token.

8.2 Optional outside providers (all off by default). Supported list, as verified in SpaceWorks:

Provider Purpose (consent) Notes
Plausible Page analytics Route templates only; self-hosted origin must be allow-listed
Umami Page analytics Website id and an approved HTTPS origin
Google Analytics 4 Page analytics Google signals and ad personalisation off
Firebase Analytics Page analytics Same property as GA4: choose GA4 or Firebase, not both
PostHog Page analytics and/or session replay Same project and origin for both
Microsoft Clarity Session replay Anonymous public page only, everything masked
Sentry Diagnostics and performance Scrubbed URLs, no bodies, public DSN only
Firebase Performance Diagnostics and performance Manual sanitised traces only
Metabase, Grafana Dashboards Over the read-only views above
  • Choose at most one page-analytics provider; replay and diagnostics are separate choices.
  • Off until two things are true: the operator configured it, and the visitor consented to that provider’s purpose. Until then no script loads, no request is sent and no cookie or storage key is written. Withdrawal stops the provider at once (it cannot erase what the provider already holds).
  • Settings take public identifiers only (site id, public key, DSN). Never a secret, token or service-account file. Changes are audited by field name.
  • The operator approves outside hosts on both frontend and backend (an allow-list of HTTPS origins) and the Content Security Policy only widens for what is listed.
  • Events use approved route templates, never raw URLs, query strings or ids. Authenticated, private, payment, evidence, credential and unknown routes are excluded and fail closed. Replay runs only on anonymous public routes, with text and inputs masked and media and network bodies blocked.
  • Tag every event with client: web, ios or android, using the same event names on every client so they can be compared. No name, email, phone, member id, device id, token, free text or screen contents.
  • Required for every product, including the native apps. Android and iOS must support the same optional providers as the web, for those that have an official mobile SDK: Firebase Analytics, Crashlytics and Performance, PostHog, Sentry and Microsoft Clarity (replay; Clarity publishes Android and iOS SDKs). GA4 is reached through Firebase Analytics on mobile (same property). Plausible and Umami have no official mobile SDK (HTTP event APIs only): the apps do not call them, and mobile use is covered by the server’s own counts. Metabase and Grafana read server data and need nothing in the app. Verify each SDK’s current mobile support and licence when adopting it.
  • Mobile rules: every SDK is compiled in but inert until the deployment’s server config enables it (the app reads the provider config from the server, as it does publishable payment keys), off by default, behind the same consent screen and withdrawal as the web; no personal data; every event carries client: ios or client: android and uses the web’s event names; no advertising id, no automatic screen or view capture that could expose private content, replay only on non-sensitive screens. A native app’s contributor rules (AGENTS.md) must name exactly these SDKs as allowed and keep forbidding any other third-party SDK without owner approval.
  • Prove it offline: tests that show no loader, request or cookie before consent, and that withdrawal stops it.

Across the org.

Product In-house reports Outside providers
SpaceWorks Yes, plus Metabase and Grafana All eight web providers above (verified); Sentry on the backend by setting
Workspace Yes (analytics desk, drill-down, CSV, print packs) Not found; not checked in code
Events Yes (event analytics, funnel, PDF); platform monitoring “No third-party trackers” stated; providers not present
Machines, Bookings Reports; platform monitoring and API monitor Not found; not checked in code
Inventory Reports and exports Not found; not checked in code
Organization Org and platform analytics with a read cache Not found; not checked in code
Android, iOS Counted server-side from API calls None today (no third-party SDKs without owner approval); gap to close
Website, Docs n/a Not checked

The rule is that every product supports the same provider list (web providers in every web app; the mobile-SDK subset in Android and iOS). Today only SpaceWorks implements it; every other product’s task is in “Bring each product up to the rules” below. A statement like “no third-party trackers” is replaced by “no third-party trackers unless the operator switches one on and the visitor consents”.

Part of Space Works product rules, published from docs/product-rules.md. Edit it there.