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 projection

Each 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:

ServicePortPurpose
causet-runtime-service8080Processes intents, writes ledger, publishes to Kafka
causet-query-service8082Executes named query templates against projection tables
causet-projection-worker8083Consumes Kafka, UPSERTs projection tables
causet-saas-cloud8085SaaS API layer — auth, routing, rate limits
causet-cloud-control-plane3000Management UI — platforms, releases, entity browser
redpanda9092Kafka-compatible message broker
postgres5432Ledger, snapshots, projection tables
minio9000Object 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'
done

Compile

causet build compile --runtime path/to/your-app --out build/tmp-out

On 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.json

Fix all compile errors before proceeding. The compiler will not produce partial output. Common compile errors:

ErrorCause
VERR_FIELD_NOT_DECLAREDA derive: expression references a payload field not declared in the event
VERR_NON_DETERMINISTIC_EXPRESSIONAn expression uses now(), random(), uuid(), or another non-deterministic function
VERR_CROSS_STREAM_WRITEA rule tries to mutate an entity of a different state type than the action’s state:

Note: Check your app.causet glob patterns if the compiler reports fewer resources than expected. Patterns like ./events/**/*.events.causet must match your actual file paths exactly.


Upload IR and create a release

Via the control plane UI

  1. Open http://localhost:3000
  2. Create a Platform if you haven’t — e.g., my-platform
  3. Create an Application — e.g., concert-app
  4. Go to IR ArtifactsUpload
  5. Upload build/tmp-out/causet.runtime.json
  6. Upload build/tmp-out/causet.projections.json
  7. Go to ReleasesNew Release → select the uploaded artifacts → Create
  8. Go to Forks → select mainDeploy 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.json

Submit 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/GetEntityEvents

Checking projection worker logs

docker compose logs -f causet-projection-worker

Look for:

[INFO] Processed ARTIST_FOLLOWED for entity user-1 → upsert user_following
[INFO] Processed CONCERT_ATTENDED for entity user-1 → upsert user_concert_history

If you see events consumed but no projection update, check that:

  1. The event type is listed in source_events for that projection
  2. The projection was included in the IR that is currently deployed
  3. 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-worker

Common issues

SymptomCheck
Intent returns 404 / unknown actionIs an active release deployed? Is the action name spelled correctly (case-sensitive)?
Query returns empty after writeIs the projection worker running? Is the event type in source_events? Did you wait for propagation?
Schema not found errorIs the fork deployed? Was DDL applied? Check control plane for deployment status.
Compile fails with file not foundCheck glob patterns in app.causet — paths are relative to the manifest, file extensions must match exactly.
Projection worker crashes on startupDid you redeploy after changing projection DDL? Schema migration may be needed.
Intent returns 401Using 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-worker

Check 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