Local Development
This page describes the self-hosted local development loop for team contributors with Docker and private source access. For the managed workflow, see causet dev and Quickstart against Causet Cloud.
The local development loop is: edit .causet files → compile to IR → upload IR artifacts → create and deploy a release → submit intents → query projections. This page covers each step and the tools for inspecting what is happening at each layer.
The development loop
edit .causet files
↓
causet build compile --runtime .
↓
causet.runtime.json + causet.projections.json
↓
upload IR via control plane (localhost:3000)
↓
create release → deploy to fork
↓
submit intent → query projectionEach time you change your .causet files you must recompile and redeploy. Changing a projection definition requires redeploying the release — the projection worker reads the IR to know what to do with each event.
Services
docker compose up -d starts all services:
| Service | Port | Purpose |
|---|---|---|
causet-runtime-service | 8080 | Processes intents, writes ledger, publishes to Kafka |
causet-query-service | 8082 | Executes named query templates against projection tables |
causet-projection-worker | 8083 | Consumes Kafka, UPSERTs projection tables |
causet-saas-cloud | 8085 | SaaS API layer — auth, routing, rate limits |
causet-cloud-control-plane | 3000 | Management UI — platforms, releases, entity browser |
redpanda | 9092 | Kafka-compatible message broker |
postgres | 5432 | Ledger, snapshots, projection tables |
minio | 9000 | Object storage for IR artifacts |
Health checks
# Runtime
curl http://localhost:8080/actuator/health
# Query service
curl http://localhost:8082/actuator/health
# Projection worker
curl http://localhost:8083/actuator/health
# Control plane
curl http://localhost:3000/api/health
# All at once
for port in 8080 8082 8083; do
echo -n "Port $port: "
curl -s http://localhost:$port/actuator/health | jq -r '.status'
doneCompile
causet build compile --runtime path/to/your-app --out build/tmp-outOn success:
[INFO] Compiled 3 states, 5 events, 4 actions, 2 projections, 2 queries
[INFO] Output: build/tmp-out/causet.runtime.json
[INFO] Output: build/tmp-out/causet.projections.jsonFix all compile errors before proceeding. The compiler will not produce partial output. Common compile errors:
| Error | Cause |
|---|---|
VERR_FIELD_NOT_DECLARED | A derive: expression references a payload field not declared in the event |
VERR_NON_DETERMINISTIC_EXPRESSION | An expression uses now(), random(), uuid(), or another non-deterministic function |
VERR_CROSS_STREAM_WRITE | A rule tries to mutate an entity of a different state type than the action’s state: |
Note: Check your
app.causetglob patterns if the compiler reports fewer resources than expected. Patterns like./events/**/*.events.causetmust match your actual file paths exactly.
Upload IR and create a release
Via the control plane UI
- Open
http://localhost:3000 - Create a Platform if you haven’t — e.g.,
my-platform - Create an Application — e.g.,
concert-app - Go to IR Artifacts → Upload
- Upload
build/tmp-out/causet.runtime.json - Upload
build/tmp-out/causet.projections.json - Go to Releases → New Release → select the uploaded artifacts → Create
- Go to Forks → select
main→ Deploy Release → select the release you just created → Deploy
Deployment applies DDL to the tenant schema ({platformId}_{applicationId}_{forkId}) — creating or migrating projection tables. If a projection table already exists with different columns, the migration adds new columns and drops removed ones.
Via the control plane API
# Upload runtime IR
curl -X POST "http://localhost:3000/api/platforms/my-platform/applications/concert-app/ir/runtime" \
-H "Content-Type: application/octet-stream" \
--data-binary @build/tmp-out/causet.runtime.json
# Upload projections IR
curl -X POST "http://localhost:3000/api/platforms/my-platform/applications/concert-app/ir/projections" \
-H "Content-Type: application/octet-stream" \
--data-binary @build/tmp-out/causet.projections.jsonSubmit an intent
Via the SaaS layer (with auth)
curl -X POST "http://localhost:8085/v1/platforms/my-platform/applications/concert-app/intents/execute" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"forkId": "main",
"action": "FOLLOW_ARTIST",
"entityId": "user-1",
"payload": {
"user_id": "user-1",
"artist_id": "artist-pearl-jam"
}
}'Directly to the runtime (no auth, local only)
Bypass the SaaS layer for fast local testing:
curl -X POST http://localhost:8080/api/v1/intents/submit \
-H "Content-Type: application/json" \
-d '{
"forkId": "main",
"action": "FOLLOW_ARTIST",
"entityId": "user-1",
"payload": {
"user_id": "user-1",
"artist_id": "artist-pearl-jam"
}
}'A successful response returns the committed event envelope:
{
"status": "committed",
"eventId": "evt_01j...",
"eventType": "ARTIST_FOLLOWED",
"entityId": "user-1",
"ts": 1751234567890
}A rejected intent returns a 422 with the rejection code:
{
"status": "rejected",
"code": "CANNOT_FOLLOW_SELF",
"message": "preflight rule reject_self_follow"
}Query a projection
curl -X POST "http://localhost:8082/v1/platforms/my-platform/applications/concert-app/forks/main/queries/concerts_for_user" \
-H "Content-Type: application/json" \
-d '{ "params": { "user_id": "user-1" } }'Replace concerts_for_user with your query name. The params object must include all required inputs declared in the query definition.
Inspecting the ledger
PostgreSQL direct
# Connect to postgres
docker compose exec postgres psql -U causet -d causet
# List tenant schemas
\dn
# Inspect events for an entity
SELECT event_type, entity_id, payload, created_at
FROM my_platform_concert_app_main.ledger_events
WHERE entity_id = 'user-1'
ORDER BY created_at DESC;Entity browser (gRPC, port 9090)
The control plane UI at http://localhost:3000 includes an entity browser. Navigate to your platform → application → fork → Entity Browser. Enter a state type and entity ID to see the event timeline and current snapshot.
For gRPC access directly:
# List recent events for an entity (requires grpcurl)
grpcurl -plaintext \
-d '{ "platformId": "my-platform", "applicationId": "concert-app", "forkId": "main", "stateType": "user", "entityId": "user-1" }' \
localhost:9090 causet.EntityService/GetEntityEventsChecking projection worker logs
docker compose logs -f causet-projection-workerLook for:
[INFO] Processed ARTIST_FOLLOWED for entity user-1 → upsert user_following
[INFO] Processed CONCERT_ATTENDED for entity user-1 → upsert user_concert_historyIf you see events consumed but no projection update, check that:
- The event type is listed in
source_eventsfor that projection - The projection was included in the IR that is currently deployed
- The
derive:expressions reference fields that exist in the event payload
Checking Redpanda (Kafka)
# List topics
docker compose exec redpanda rpk topic list
# Consume recent events from the projection topic
docker compose exec redpanda rpk topic consume causet.projection-events.v1 --num 10
# Check consumer group lag (is the worker keeping up?)
docker compose exec redpanda rpk group describe causet-projection-workerCommon issues
| Symptom | Check |
|---|---|
| Intent returns 404 / unknown action | Is an active release deployed? Is the action name spelled correctly (case-sensitive)? |
| Query returns empty after write | Is the projection worker running? Is the event type in source_events? Did you wait for propagation? |
| Schema not found error | Is the fork deployed? Was DDL applied? Check control plane for deployment status. |
| Compile fails with file not found | Check glob patterns in app.causet — paths are relative to the manifest, file extensions must match exactly. |
| Projection worker crashes on startup | Did you redeploy after changing projection DDL? Schema migration may be needed. |
| Intent returns 401 | Using the SaaS layer requires a valid auth token. Use the direct runtime endpoint (:8080) for local testing without auth. |
Tips
Restart the projection worker after IR changes. The projection worker loads the projections IR on startup. If you redeploy with a new IR, restart the worker:
docker compose restart causet-projection-workerCheck MinIO for IR artifacts. If the upload succeeded but the release is not picking up the right IR, verify the artifacts are in MinIO:
open http://localhost:9001 # MinIO console (credentials: minioadmin / minioadmin)Use docker compose logs early and often. Each service logs rejections, errors, and processing confirmations. Running docker compose logs -f across all services during a test session surfaces issues quickly.
Clear projection tables during development. If you change a projection’s schema significantly and the migration is not working cleanly, you can truncate the table and let the projection worker replay from the beginning:
TRUNCATE my_platform_concert_app_main.user_concert_history;The projection worker will replay from the earliest unconsumed offset. Adjust the consumer group offset in Redpanda if you need to replay all historical events.
Next steps
- Your First Event — defining and emitting events
- Your First Intent — rules and CLI submission
- Your First Projection — materializing events into queryable tables
- Your First Query — named queries over projections
- Architecture Overview — how the services fit together