Then vs Now — From OpenTelemetry, ClickHouse, and Redpanda to FORJD
Chapter 34: Then vs Now — From OpenTelemetry, ClickHouse, and Redpanda to FORJD
This chapter is the explicit Then vs Now record for DEML's data plane. Earlier chapters describe the steady-state architecture; this chapter documents what we initially operated, why that topology failed our tenancy and security bar, and what we run instead. Operators and agents must not reintroduce the retired stack under a new name.
Why a first data plane existed
DEML began as a full-stack learning and observability product. Identity, billing, Angular dashboards, and telemetry processing lived in one repository. To keep Django off the hot path, the first architecture borrowed a familiar industry pattern:
- Emit — services and the browser produced OpenTelemetry (OTLP) spans and metrics.
- Route — an OpenTelemetry Collector batched and exported high-volume telemetry.
- Buffer — a Kafka-compatible broker (Redpanda) absorbed bursts between producers and consumers.
- Store / analyze — a columnar OLAP store (ClickHouse) powered dashboards, CES-style rollups, and long-window analytics.
- Project locally — DEML workers (and later a Railway-hosted Rust data plane) materialized product read models beside Django.
That stack was coherent for a single-owner SaaS that treated telemetry as operator observability. It was the wrong spine for a multi-tenant product that must forward ciphertext-only events, bind every call to a tenant, and keep the learning/control plane free of stream-broker operations.
Then: what we initially did
| Concern | Initial DEML-owned plane |
|---|---|
| Signal shape | OTLP spans/metrics; often plaintext or lightly redacted |
| Ingest edge | OTel Collector (infrastructure/otel-collector, OTLP :4318) |
| Buffering | Redpanda (or equivalent Kafka-protocol bus) on Railway/Fly |
| Analytics warehouse | ClickHouse (columnar OLAP) queried for CES and dashboards |
| Durable processing | DEML/Railway workers, local outbox patterns, dual ownership |
| Product reads | Mix of Django models + local projections + CH queries |
| Auth to data plane | Shared service credentials, cluster DSNs, collector env vars |
| Failure mode | Partial empties, dual writes, "is CH lag or is the app down?" |
Concrete friction showed up in operations and security review:
- Credential and surface sprawl. Collector hosts, ClickHouse users/passwords, Redpanda ACLs, and worker DSNs multiplied. A partner integration could not be expressed as one tenant-bound token.
- Wrong trust boundary. Firebase end-user identity and OTLP exporter credentials are not substitutes for sealed envelopes. Labeling plaintext or OTLP product telemetry as E2EE was dishonest.
- Split brain. Django, ClickHouse, and Redpanda each held partial truth. Replay meant replaying a broker topic and reconciling a columnar table and healing local projections.
- Control-plane weight. Running OLAP + broker + collector beside identity and Stripe billing forced DEML into stream-platform ops it did not want to own.
- Symmetry failure. Not every account could run the same pipeline without provisioning CH/Redpanda capacity per tenant—antithetical to symmetrical multi-account control.
Appendix C still documents an alternate Cloud Run topology that once assumed an OTel Collector writing into an analytics warehouse. That section is historical residency math—not the product path. Appendix T records the retirement of the Railway Rust data-plane roles that briefly replaced parts of this stack before FORJD became exclusive.
Now: what we do
| Concern | Steady-state FORJD plane |
|---|---|
| Signal shape | Client-sealed AES-256-GCM envelopes (key_id + ciphertext + routing metadata) |
| Ingest edge | Angular/API → Django BFF (Firebase or deml_… key) → FORJD /api/v1/ingest |
| Buffering | FORJD Postgres outbox + Dragonfly streams (inside FORJD) |
| Analytics / CES | FORJD durable stream_results + analytics APIs via Django adapters |
| Durable processing | FORJD workflows, engine roles, Prefect soft-fail where configured |
| Product reads | Stable DEML paths; Django adapts only allowlisted FORJD routes |
| Auth to data plane | Tenant-bound opaque fjsvc_ service token; secret ref in DEML Postgres |
| Failure mode | Typed 503 / forjd_degraded (or explicit degraded: true fallback envelopes) |
DEML owns identity, profiles, roles, Stripe billing, consent, learning content, API credentials, and the Angular surface. FORJD owns sealed intake, streaming, projections, analytics, ML, SIEM/SOAR, reports, replay, DLQ, and tenant erase. Integration contract: docs/FORJD_INTEGRATION.md. Ownership appendix: Appendix T. Observability chapter (steady state): Chapter 8.
flowchart TB
subgraph then [Then — retired]
A1[Apps / browser] -->|OTLP| C1[OTel Collector]
C1 --> R1[Redpanda]
R1 --> CH[ClickHouse]
R1 --> W1[DEML / Railway workers]
W1 --> PG1[(Local projections)]
CH --> UI1[Dashboards]
PG1 --> UI1
end
subgraph now [Now — steady state]
A2[Angular / integrators] -->|Firebase or deml_ key + sealed envelope| DJ[Django BFF]
DJ -->|fjsvc_| FJ[FORJD API + engine]
FJ --> DF[(Dragonfly)]
FJ --> SB[(Supabase Postgres)]
FJ --> PR[Projections analytics ML SIEM]
A2 -->|Product reads| DJ
DJ -.->|BFF adapters| PR
end
Benefits of the cutover
- One trust model. Partners and DEML itself authenticate to FORJD with a tenant-bound
fjsvc_token. End-user Firebase JWTs terminate at Django and are never forwarded. - Ciphertext-first product lane. Sealed envelopes carry routing metadata only; plaintext lesson content, PII, and scores stay out of ingest metadata. E2EE is a real cryptographic boundary, not a marketing label on OTLP.
- Lean control plane. DEML no longer runs ClickHouse nodes, Redpanda clusters, or OTel Collectors for product telemetry. Fly Django + managed Postgres + Vercel stay focused on identity and BFF adaptation.
- Durable recovery. Replay and DLQ are FORJD contracts with idempotent projection keys—not ad-hoc "replay the topic into CH and hope the dashboard catches up."
- Fail-closed honesty. Steady
FORJD_READ_MODE=forjdreturns typed degradation; empty success is reserved for explicit cutover modes and is markeddegraded: trueso Angular never confuses an outage with an empty tenant. - Symmetrical tenancy. Every mapped account uses the same adapter paths; capacity and RLS live in FORJD, not in per-tenant CH databases provisioned from DEML.
- Clear erase saga. Account deletion calls FORJD
POST /api/v1/tenants/{id}/erasebefore local teardown—impossible to express cleanly when telemetry lived in three stores. - Optional add-ons stay optional. FORJD's lean-core / fat-extension model (
FORJD_ADDONS) keeps scanners and intel behind config gates without dragging pandas/OLAP into the sealed path.
What must not return
- Do not add OpenTelemetry Collector, ClickHouse, Redpanda, Kafka, or a DEML-local projection worker "for convenience."
- Do not query FORJD Postgres or Dragonfly from Django.
- Do not forward Firebase tokens or
deml_…keys to FORJD. - Do not treat missing FORJD SIEM/analytics routes as empty
200success lists without an explicit degraded marker. - Do not document OTLP → columnar warehouse as the product telemetry path; optional operator tracing is a separate, non-E2EE concern.
Operator checklist after reading this chapter
- Confirm production flags:
FORJD_WRITE_MODE=forjd,FORJD_READ_MODE=forjd(or cutover phase 2). - Confirm every account that needs data-plane access has
map_forjd_tenant+ secret ref (never a plaintext token in git). - Confirm Railway/ClickHouse/Redpanda/OTel collector services for product telemetry are absent from live topology (
infrastructure/railway/README.md). - Confirm dashboards and CES read through Django adapters to FORJD—not direct CH DSNs.
- On FORJD outage: restore FORJD; do not stand up a temporary Redpanda/ClickHouse sidecar.
The transition is complete when DEML's repository contains adapters and policy—not brokers and warehouses. Everything else in this book assumes Now.