Machine-translated draft — terminology + flow still being reviewed.

Mortar — Analytics & Monitoring

Status: the warehouse primitive + analytics/monitor event ingest and summary are built and verified end-to-end on staging (2026-07-14: POST events → ClickHouse → GET summary → warehouse usage metered, within free quota). The materialized-view rollup (read-side pre-aggregation — the 80% margin lever) and the cross-request Buffer (write-side batching) are both implemented and e2e-verified. Source of truth: internal/primitive/warehouse/

Giving user-built apps Monitoring and Growth / product analytics is, in Mortar, not two separate products but two new capability modules (mortar.monitor + mortar.analytics) on one shared per-tenant event plane.

In one line: Monitor / Grow / Meter are three views of the same event stream. Meter (billing) is already a Mortar capability (pricing.md); this doc adds the other two.


Why fold into Mortar instead of shipping a separate product

  1. Mortar already IS that per-tenant plane. It has the tenant / workspace model, metering, billing, AppUser identity (auth), a 4-language SDK, and the mortar.* capability-module pattern. Monitor / Grow are per-tenant event pipelines — they already fit that shape; the natural move is two more modules alongside the rest.

  2. One SDK, one tenant, one bill = the Firebase model. The app already imports the Mortar SDK, already has a tenant, already bills through Mortar. mortar.track(...) reuses the mortar.auth AppUser identity for identify. This is exactly why Firebase (auth + firestore + analytics + crashlytics in one SDK) wins developers.

  3. It clears Supabase’s biggest gap. Supabase ships infra-only observability (Logflare logs + Postgres metrics) and no product analytics — an entire third-party ecosystem exists to fill that hole. Native Monitor + Grow makes Mortar “Supabase + PostHog + Sentry in one”: a more complete standalone BaaS and a more complete Fabric backend.


primitive vs feature — the one test

Mortar’s two layers (see architecture.md): a primitive is a cost dimension (1:1 with a cloud-bill line); a feature is a user-facing capability composed on top of primitives.

When adding Monitor / Grow, deciding “what should be a new primitive” has exactly one test:

Does this create a new cloud-bill line that no existing primitive covers? Yes → primitive. Composable from existing primitives → feature.

Running it:

ThingCloud resourceVerdict
Event aggregation (funnels / retention / DAU-MAU / high-cardinality scans)OSS can’t query it; Postgres (database) can’t run columnar aggregation at a million-tenant scale → needs ClickHousenew primitive
replay recording storageOSS blobexisting storage → feature
replay playback bandwidthegress + OSS GET (GET already absorbed)existing network → feature
event ingest bandwidthingress (usually unbilled)existing network → feature
alerting (monitor email / SMS)Aliyun DM / SMSexisting comms → feature

Conclusion: the only new primitive is the OLAP engine; monitor / analytics / replay are all features.


The one new primitive: warehouse (OLAP engine, ClickHouse)

Mortar goes from “pure Postgres OLTP” to OLTP + ClickHouse OLAP dual-engine. This is the real cost of the integration.

EngineHolds
OLTP (existing core)Postgresauth · data (JSONB) · files metadata · realtime state · metering ledger
OLAP (only new one)ClickHouseevent aggregation — monitor (error / latency) + analytics (track / funnels / retention)
  • Columnar store for high-cardinality aggregation — not a reuse of data’s Postgres JSONB.
  • A separate high-throughput ingest path (write-heavy, append-only, sampleable*), not the synchronous GORM/Postgres request path used by auth/data: ingest endpoint → buffer → ClickHouse.
  • *Sampleable except Meter — billing events can’t be dropped or sampled and are always server-generated.
  • replay recordings → OSS blob (existing storage), into neither Postgres nor ClickHouse.

So Mortar’s primitives go 7 → 8 (compute / database / cache / storage / network / function / comms / warehouse).


The three features

monitor — observability

Errors / crashes / latency / health. Events aggregate in the OLAP engine (error grouping, error rate, p95); alerting rides existing comms. This is production observability, distinct from the harness’s dev-time “Try to Fix”.

analytics — product analytics (Grow)

track() / identify() / funnels / retention / cohorts. Identity stitches sessions and users via the mortar.auth AppUser. Events aggregate in the OLAP engine.

replay — session replay

Serializes and replays everything the user actually saw / did this session (DOM mutations, clicks, scrolls, errors). Both Monitor and Grow use it: watch how a user hit a bug or dropped out of a funnel.

Why replay is billed separately: one recording is orders of magnitude heavier than a plain event. Charging “1 replay = 1 event” at a flat unit price torches margin, so Sentry / PostHog both bill it per recording. In build terms it’s a feature (storage + network), sampled + paid-tier only.


Client capture — split into two layers by ownership

Backend / storage / query / billing live in Mortar. Client capture is deliberately NOT built as one monolithic Keel module; it splits by whether native code is required:

  1. Core capture = the Mortar SDK (@mortar-ai/client), framework-agnostic (shipped). mortar.analytics.track() / .identify() / .captureError() is plain JS in sdk/typescript/src/analytics.ts: client-side batching (20 events / 10s / page-hide triggers) → POST /v1/{tenant}/analytics/events. Because the app already imports the Mortar SDK, already has a tenant, already bills through Mortar, track() reuses all of it with zero new deps (the Firebase model). The anonymous distinct_id persists in localStorage/AsyncStorage; identify() reuses the mortar.auth AppUser identity. Works in the browser + RN-JS.

  2. Native crash capture = a Mortar-owned Keel module (opt-in, TODO). Only the signals that genuinely need native code (uncaught native crashes / ANR / startup stacks) become a Keel native module owned by Mortar (create-keel-module, scope @mortar-ai/*, not @keel-ai/*), installed on demand. It reports to the same ingest endpoint.

This is Mortar’s structural edge over Lovable: Lovable generates code that runs on someone else’s runtime with a Supabase backend, so it can only “tell users to wire a third-party SDK into their own account”. Mortar bakes capture into its own SDK, data flowing into its own event plane — one SDK, one tenant, one bill.


API (shipped)

Two tenant-scoped endpoints, app/admin key auth (the Mortar SDK client uses an app key).

IngestPOST /v1/{tenant}/analytics/events

{ "events": [
  { "kind": "analytics", "name": "pageview", "distinct_id": "u1", "session_id": "s1", "props": {} },
  { "kind": "monitor",   "name": "error" }
]}
202 { "accepted": 2 }

kind = analytics | monitor (default analytics); max 500 events/request; name required; timestamp defaults to server time; each event meters one warehouse unit.

ReadGET /v1/{tenant}/analytics/summary?from=&to= (RFC3339; default last 7 days, 90-day cap)

{ "from": "...", "to": "...",
  "total_events": 4, "unique_visitors": 2, "sessions": 2,
  "by_name": { "pageview": 2, "signup": 1, "error": 1 },
  "series": [ { "date": "2026-07-14", "events": 4 } ] }

unique_visitors/sessions use ClickHouse uniqExactIf; series is the daily trend. No ClickHouse (MORTAR_CLICKHOUSE_URL empty) → both endpoints 503 analytics_not_configured.

Billing — reuse existing metering, no new engine

Mortar’s existing two axes: plan (Free / Pro / Team / Enterprise subscription) × tier (Nano…Large capacity, see pricing.md). Monitor / Grow add no new billing engine; their usage folds into existing metering.

  • primitive (raw cloud cost): warehouse = ClickHouse compute + storage, a new cloud-bill line.
  • user-facing billing units (feature level):
    • per-event / per-error ← backed by warehouse
    • per-replay ← backed by storage + network
    • retention = plan / tier attribute (not a metered dimension — caps storage COGS directly)

Key distinction: event (event volume) is the user-facing billing unit, not a primitive; the primitive is the new ClickHouse cloud-bill line. This matches Mortar’s reality — pricing already tracks modeled cost and bundles where sensible (the 3 baseline dimensions collapse into one tier baseline; OSS API requests are absorbed), not strict 1:1; event (+ replay split out) fits the same template.

Meter is the monetization engine under Monitor / Grow: the more a user customizes, the more events they send, the more they turn on replay / deep analysis → the higher the bill. “Grow is customizable” IS the revenue curve.


Boundary: Fabric’s own observability is not on this plane

This event plane serves “user-built apps” (Mortar tenants). Fabric’s own platform services (fabric-server / harness / mortar-cloud / keel-cloud) use a separate stack (Sentry / OpenTelemetry) that does not go through Mortar — Fabric doesn’t run on Mortar; it keeps its own accounts and observability. Don’t conflate “Monitor/Grow for tenant apps” with “Fabric platform self-monitoring”.

Where OpenTelemetry fits: it’s the right standard for operational / cross-service tracing (server-side Mortar instrumentation + eventual end-to-end traces), not the model for product analytics (Grow) — that needs a “named events + arbitrary properties + user identity” schema that OTLP spans can’t give. The client workhorse is a thin home-grown track() / crash SDK; OTel is for the server side.


Phased roadmap

Ordered by highest-leverage / lowest-cost, matching market validation (the one thing Lovable built first-party is phase 1).

  1. Zero-config traffic baseline (Grow’s 80/20) — a zero-config real-time analytics dashboard for published apps (visitors / pageviews / sources / devices) on ClickHouse. What non-technical users actually want (“is anyone using my app”). Reference: Lovable’s equivalent, ~1 engineer · 1 week. ✅ Backend shipped (ingest + summary + trend series on ClickHouse, staging e2e passed); ✅ Fabric Cloud dashboard tab landed (5 surfaces: web / iOS / Android / desktop / wechat, reading summary through the fabric-server proxy); ✅ client capture SDK core shipped (mortar.analytics.track/identify/ captureError in @mortar-ai/client, batching + flush). Remaining: publish the new @mortar-ai/client + teach Fabric’s skill/prompt to call mortar.analytics.*.
  2. Error monitoring (Monitor) — JS unhandled errors can already report via captureError(); remaining: native crash / ANR (a Mortar-owned Keel module) + a grouping dashboard. A category-wide gap (even Lovable papers over it with AI “Try to Fix”) — first mover gets a selling point.
  3. Deep product analytics (paid tier)track() / identify() + funnels / retention / cohorts; replay (sampled, paid).
  4. Billing hookup — fold each phase’s usage into Mortar metering (per-event / per-replay / retention cap); ship the warehouse primitive.

Competitors

PlayerMonitor + Grow for user-built apps
SupabaseInfra-only (Logflare logs + Postgres metrics); no product analytics, no uptime
LovableThin first-party: one zero-config traffic dashboard (ClickHouse); everything deeper punted to third parties (tells users to wire PostHog / GA)
FirebaseFull first-party suite (Crashlytics + GA for Firebase + Performance Monitoring)
Mortar (this design)Integrated first-party: monitor + analytics + replay as capabilities, one SDK, billing on existing metering; owning the Keel runtime + Mortar backend enables the capture integration Lovable can’t do

See also