Architecture Overview

Causet is a suite of services that together implement an event-sourced rules engine with a compiled read side. This page covers the service map, data flow, and the key architectural decisions.


Service diagram

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────────┐
│ Control plane   │────▶│ causet-saas-cloud│────▶│ causet-runtime     │
│ (Next.js :3000) │     │ (:8085)          │ gRPC│ (:8080 HTTP, :9090) │
└─────────────────┘     └────────┬─────────┘     └──────────┬──────────┘
                                 │                          │
                                 │                          │ ledger + snapshots
                                 │                          ▼
                                 │                 ┌─────────────────────┐
                                 │                 │ PostgreSQL causet  │
                                 │                 └─────────────────────┘
                                 │                          │
                                 │                          │ projection-events
                                 │                          ▼
                                 │                 ┌─────────────────────┐
                                 │                 │ Redpanda / Kafka    │
                                 │                 └──────────┬──────────┘
                                 │                            │
                                 │   GetActiveRelease         │
                                 │                            ▼
                                 │                 ┌─────────────────────┐
                                 └────────────────▶│ projection-worker   │
                                                   │ (:8083)             │
                                                   └──────────┬──────────┘


                                                   ┌─────────────────────┐
         ┌────────────────────────────────────────▶│ PostgreSQL          │
         │                                         │ projections DB      │
         │                                         └──────────┬──────────┘
         │                                                    │
         ▼                                                    │
┌─────────────────┐                                           │
│ query-service   │◀──────────────────────────────────────────┘
│ (:8082)         │
└─────────────────┘

┌──────────────────────────────────────┐
│ ws-gateway-go (:8081)                │◀── causet.patches.v1 (Kafka)
└──────────────────────────────────────┘

Services

ServicePortLanguageRole
causet-runtime-service8080, 9090Java/Spring WebFluxIntent processing, rules evaluation, ledger, snapshots, gRPC entity browser
causet-saas-cloud8085Java/SpringSaaS layer: sessions, releases, active IR version per fork
causet-cloud-control-plane3000Next.jsWeb UI + API proxy
causet-projection-workerJava/Spring WebFluxKafka consumer → projection UPSERTs via R2DBC
causet-query-service8082Java/Spring WebFluxNamed + entity queries against projection tables
ws-gateway-go8081GoWebSocket fanout from Kafka patches
causet-compilerJava.causet → runtime IR + projections IR
causet-cliGoCLI for compile, deploy, ops

Write path

Intent → (SaaS API or direct) → causet-runtime-service
  → validate action exists in active IR
  → acquire per-entity cursor lock
  → run preflight rules (validation, can reject)
  → run core rules (mutations, relationship ops)
  → run side_effects rules (emit, submit, schedule)
  → persist ledger_events + entity_snapshots (sync, R2DBC)
  → publish causet.projection-events.v1 (async, fire-and-forget)
  → return response to caller

Key properties:

  • Synchronous to ledger. The runtime returns after ledger_events commit. Projection fan-out is async.
  • Per-entity serial ordering. The cursor lock ensures two intents for the same entity never race.
  • Deterministic. Same entity state + same intent → same events. No wall-clock, no randomness.
  • Fire-and-forget Kafka. Kafka publish failure does not roll back the ledger write.

Read path

Query request → causet-query-service
  → resolve active irVersion (causet-saas-cloud GetActiveRelease)
  → load causet.projections.json from S3 (Redis-cached)
  → SET LOCAL search_path = {tenant_schema}
  → execute parameterized SQL template with bound params
  → optional Redis result cache
  → return rows

Key properties:

  • Eventual consistency. Projection tables lag behind writes by milliseconds to seconds.
  • Compiled queries. Query templates are produced by the compiler — not dynamic SQL.
  • Tenant-scoped. search_path scoped per request to the fork’s schema.

Data stores

StoreTechnologyPurpose
Event storePostgreSQL causetledger_events, entity_snapshots, intent_status, decision_routes
Read modelsPostgreSQL projectionsIR-driven projection tables per tenant schema
Control planePostgreSQL causet_control_planeSaaS/control plane state, Flyway migrations
IR cacheRediscauset.projections.json TTL cache for query service + projection worker
IR artifactsS3 / MinIOCompiled IR per release version

No JDBC. All database access uses reactive R2DBC.

No Flyway for projection tables. Projection schema is IR-driven DDL applied at deploy time from causet.projections.json.


Kafka topics

TopicPurpose
causet.intents.v1Intent ingress (alternative path)
causet.ledger-events.v1Ledger stream (for external consumers)
causet.projection-events.v1Enriched events consumed by projection worker
causet.projection-dlq.v1Failed projection messages after max retries
causet.patches.v1Realtime client patches consumed by ws-gateway

IR artifacts

The compiler produces two artifacts per release:

ArtifactConsumersContains
causet.runtime.jsoncauset-runtime-serviceStreams, fields, intents, rules bytecode
causet.projections.jsoncauset-projection-worker, causet-query-serviceTables, indexes, queries, relationship streams

Artifacts stored at s3://{bucket}/{irVersion}/. Active version per fork resolved from causet-saas-cloud.


Tenant isolation

Every fork gets an isolated PostgreSQL schema:

{platformId}_{applicationId}_{forkId}   # sanitized, max 63 chars

Both the projection worker and query service use SET LOCAL search_path TO '<tenant_schema>', public scoped to each database transaction. There are no shared tables between tenants.

DDL for projection tables is applied from the IR at deploy time. Adding a column means updating the DSL → recompiling → deploying a new release.


DSL layers

LayerFormatEdited by
Product DSL.causet files (YAML)Application authors
Core Causet rulesetstreams.yaml, rules.yaml, etc.Compiler output only
Projection IRcauset.projections.jsonCompiler output only

Authors only edit .causet files. The compiler transpiles to internal formats. Never edit compiled YAML in normal workflow.


Auth

  • Clerk JWT — for control plane and SaaS API (multi-tenant orgs)
  • Anonymous sessions — for demos (POST /v1/sessions/anonymous)
  • API keys — per platform, for server-to-server integrations

Further reading