Retrofit an Existing App
This is the primary adoption path for existing apps.
Causet sits beside your app. Keep the frontend, API routes, database, queues, and third-party clients. Move one fragile workflow into a durable, inspectable timeline.
Your API route should accept the request. Causet should own what happens next.
Start small: one endpoint, one intent, one workflow. See What runs today? before you depend on Cloud or SDKs.
What problem this solves
Existing endpoints often hide multi-step workflows:
POST /api/access-requests
→ validate request
→ write DB row
→ call approval service
→ send email
→ update status
→ maybe fail halfwayWhen a step fails, it is hard to know what completed. Logs are not a timeline. Retry and repair become guesswork.
Causet turns that into a replayable timeline of intents and events — without rewriting the product.
When to use Causet
- Multiple steps can partially fail
- External services are involved (email, approvals, CRM, payments)
- You need to explain what happened after an incident
- You want replay, fork, or repair later
- Webhooks or jobs will advance the same workflow later
When not to use Causet
- Simple CRUD with no meaningful lifecycle
- No need for audit, replay, or timeline inspection
- Failures are cheap and easy to retry by hand
- Everything must stay in one DB transaction you will not split
Full guidance: When Not to Use Causet.
Concrete example: access request
Existing endpoint
POST /api/access-requests
Old behavior
// Fragile: one route owns the whole workflow
export async function POST(req: Request) {
const body = await req.json()
await validateAccessRequest(body)
const row = await db.accessRequests.insert(body)
await approvalService.request(row.id)
await sendEmail(body.email, 'Access requested')
await db.accessRequests.update(row.id, { status: 'pending_approval' })
return Response.json({ id: row.id, status: 'pending_approval' })
}If sendEmail fails after approval was requested, status and reality diverge.
Causet behavior
POST /api/access-requests
→ validate request
→ submit request_access intent
→ return (compatibility or 202 accepted)
Causet
→ records intent
→ emits access_requested, approval_required, …
→ app receives updates (projection query, Kafka consumer, or webhook bridge)
→ frontend still reads existing DB / projection
→ failures are visible in the timeline and can be replayedHow to wrap the endpoint
// app/api/access-requests/route.ts — handoff boundary
import { serverEmitIntent } from '@causet/sdk-next/server';
export async function POST(req: Request) {
const body = await req.json();
const requestId = body.id ?? `ar_${crypto.randomUUID()}`;
// Keep auth + shape validation in the app
await validateAccessRequest(body);
await serverEmitIntent('access_request_stream', requestId, 'request_access', {
request_id: requestId,
email: body.email,
reason: body.reason,
});
// Compatibility mode: same-shaped success response
return Response.json({
id: requestId,
status: 'pending_approval',
message: 'Request received',
});
}SDK packages are Preview. Point CAUSET_API_URL at Causet Cloud (Early access) or a self-hosted stack when available. See What runs today? and Add Causet to Next.js.
Sample intent
actions:
request_access:
state: access_request
entity_id_expr: intent.request_id
input:
request_id: { type: string, required: true }
email: { type: string, required: true }
reason: { type: string, required: true }
core:
rules:
- name: record_request
then:
- op: set
path: status
value: pending_approval
- op: emit
event_type: access_requested
payload:
request_id: intent.request_id
email: intent.email
- op: emit
event_type: approval_required
payload:
request_id: intent.request_idSample events
access_requested
approval_required
access_approved
notification_sent
notification_failedSample query / projection
projections:
access_request_status:
source_events: [access_requested, access_approved, notification_sent, notification_failed]
target:
table: access_request_status
primary_key: [request_id]
queries:
access_request_status:
from: access_request_status
where:
request_id: { eq: input.request_id }Your frontend can keep reading your existing table if you update it from Causet events — see Webhooks.
Compatibility mode vs true async mode
Compatibility mode (preserve synchronous API behavior)
Use when the existing client expects a normal success response.
{
"id": "ar_123",
"status": "pending_approval",
"message": "Request received"
}- Safest first retrofit step
- Frontend often needs no change
- Causet runs the multi-step work after accept
- Good for emails, Slack, CRM sync, enrichment, background side effects
True async mode (accepted / pending + notify)
Use when the workflow is long-running or the user needs progress.
{
"status": "accepted",
"requestId": "ar_123",
"statusUrl": "/api/access-requests/ar_123"
}- Prefer
202 Accepted - Return a workflow or entity ID
- Client polls, subscribes, or opens a status view
- Notify the existing app via webhook/event bridge when status changes
How webhooks update the existing app
Causet records the timeline. Your existing app still owns user-facing tables if you want it to.
Bridge options today:
- Read Causet projections/queries from your API
- Consume Kafka ledger events and update your DB
- Outbound HTTP webhooks — Available now (see What runs today?)
Pattern for updating your DB from a Causet event (consumer or future webhook handler):
// Idempotent handler — store last processed event id
async function onAccessApproved(event: {
eventId: string
requestId: string
status: string
}) {
if (await alreadyProcessed(event.eventId)) return
await db.accessRequests.update(event.requestId, { status: event.status })
await markProcessed(event.eventId)
}Details: Use Webhooks to Update Existing Flows.
Inspect timeline state
After you hit the endpoint:
causet inspect timeline --stream access_request_stream --entity ar_123Or open Decision Timeline in Causet Cloud.
You should see the intent, emitted events, and any failed side effects — see Timeline.
Replay or repair failure
Example failure:
request submitted ✓
approval succeeded ✓
notification failed ✗
projection stale (still pending_notification)- Inspect the timeline
- Fork from the last good cursor if needed
- Replay the notification step or rebuild the projection
- Apply the verified repair
Walkthrough: Replay.
How to roll out safely
- Pick one fragile workflow (access request is a good first pick)
- Keep the existing endpoint and public contract
- Submit one intent from the route
- Move downstream steps into Causet rules / sagas / side effects
- Bridge status back to your DB if the frontend still reads it
- Run against Causet Cloud (early access) or local runtime when available
- Inspect the timeline; simulate a failure; verify replay
- Ship behind a flag or single route
- Repeat for the next workflow
Checklist: Production Rollout Checklist.
What changed?
- Frontend did not need a rewrite
- Endpoint still exists — it is a boundary, not a workflow engine
- Causet owns the durable workflow
- Failures are visible on the timeline
- Work can be inspected, forked, replayed, and repaired