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
| Service | Port | Language | Role |
|---|---|---|---|
causet-runtime-service | 8080, 9090 | Java/Spring WebFlux | Intent processing, rules evaluation, ledger, snapshots, gRPC entity browser |
causet-saas-cloud | 8085 | Java/Spring | SaaS layer: sessions, releases, active IR version per fork |
causet-cloud-control-plane | 3000 | Next.js | Web UI + API proxy |
causet-projection-worker | — | Java/Spring WebFlux | Kafka consumer → projection UPSERTs via R2DBC |
causet-query-service | 8082 | Java/Spring WebFlux | Named + entity queries against projection tables |
ws-gateway-go | 8081 | Go | WebSocket fanout from Kafka patches |
causet-compiler | — | Java | .causet → runtime IR + projections IR |
causet-cli | — | Go | CLI 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 callerKey properties:
- Synchronous to ledger. The runtime returns after
ledger_eventscommit. 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 rowsKey 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_pathscoped per request to the fork’s schema.
Data stores
| Store | Technology | Purpose |
|---|---|---|
| Event store | PostgreSQL causet | ledger_events, entity_snapshots, intent_status, decision_routes |
| Read models | PostgreSQL projections | IR-driven projection tables per tenant schema |
| Control plane | PostgreSQL causet_control_plane | SaaS/control plane state, Flyway migrations |
| IR cache | Redis | causet.projections.json TTL cache for query service + projection worker |
| IR artifacts | S3 / MinIO | Compiled 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
| Topic | Purpose |
|---|---|
causet.intents.v1 | Intent ingress (alternative path) |
causet.ledger-events.v1 | Ledger stream (for external consumers) |
causet.projection-events.v1 | Enriched events consumed by projection worker |
causet.projection-dlq.v1 | Failed projection messages after max retries |
causet.patches.v1 | Realtime client patches consumed by ws-gateway |
IR artifacts
The compiler produces two artifacts per release:
| Artifact | Consumers | Contains |
|---|---|---|
causet.runtime.json | causet-runtime-service | Streams, fields, intents, rules bytecode |
causet.projections.json | causet-projection-worker, causet-query-service | Tables, 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 charsBoth 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
| Layer | Format | Edited by |
|---|---|---|
| Product DSL | .causet files (YAML) | Application authors |
| Core Causet ruleset | streams.yaml, rules.yaml, etc. | Compiler output only |
| Projection IR | causet.projections.json | Compiler 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
- Runtime Architecture — deep dive on intent processing
- Event Flow — end-to-end event lifecycle
- Projection Pipeline — from Kafka to PostgreSQL
- Storage — data stores in detail
- Multi-Tenancy — tenant isolation model
- Scaling Model — horizontal scale and bottlenecks