Mortar — Analytics & Monitoring
Status: the
warehouseprimitive +analytics/monitorevent ingest and summary are built and verified end-to-end on staging (2026-07-14: POST events → ClickHouse → GET summary →warehouseusage 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
-
Mortar already IS that per-tenant plane. It has the tenant / workspace model, metering, billing, AppUser identity (
auth), a 4-language SDK, and themortar.*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. -
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 themortar.authAppUser identity foridentify. This is exactly why Firebase (auth + firestore + analytics + crashlytics in one SDK) wins developers. -
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:
| Thing | Cloud resource | Verdict |
|---|---|---|
| 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 ClickHouse | new primitive |
| replay recording storage | OSS blob | existing storage → feature |
| replay playback bandwidth | egress + OSS GET (GET already absorbed) | existing network → feature |
| event ingest bandwidth | ingress (usually unbilled) | existing network → feature |
| alerting (monitor email / SMS) | Aliyun DM / SMS | existing 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.
| Engine | Holds | |
|---|---|---|
| OLTP (existing core) | Postgres | auth · data (JSONB) · files metadata · realtime state · metering ledger |
| OLAP (only new one) | ClickHouse | event 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:
-
Core capture = the Mortar SDK (
@mortar-ai/client), framework-agnostic (shipped).mortar.analytics.track()/.identify()/.captureError()is plain JS insdk/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 anonymousdistinct_idpersists inlocalStorage/AsyncStorage;identify()reuses themortar.authAppUser identity. Works in the browser + RN-JS. -
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).
Ingest — POST /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.
Read — GET /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 bywarehouseper-replay← backed bystorage+networkretention= 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(+replaysplit 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).
- 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/ captureErrorin@mortar-ai/client, batching + flush). Remaining: publish the new@mortar-ai/client+ teach Fabric’s skill/prompt to callmortar.analytics.*. - 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. - Deep product analytics (paid tier) —
track()/identify()+ funnels / retention / cohorts;replay(sampled, paid). - Billing hookup — fold each phase’s usage into Mortar metering
(
per-event/per-replay/retentioncap); ship thewarehouseprimitive.
Competitors
| Player | Monitor + Grow for user-built apps |
|---|---|
| Supabase | Infra-only (Logflare logs + Postgres metrics); no product analytics, no uptime |
| Lovable | Thin first-party: one zero-config traffic dashboard (ClickHouse); everything deeper punted to third parties (tells users to wire PostHog / GA) |
| Firebase | Full 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
- architecture.md — the 7 → 8 primitive × feature structure
- pricing.md — plan × tier two axes + metering
internal/plan/registry.go— authoritative pricing / primitive source