Event Flow
Complete end-to-end flow from user action to frontend update
Overview
This diagram shows the complete production event flow in Synap, from user clicking a button to real-time updates across all connected clients.
Complete Flow Diagram
Step-by-Step Breakdown
Phase 1: User Intent (< 50ms)
User clicks "Create Note" in UI
// Frontend
await client.entities.create({
type: 'note',
title: 'Meeting Notes'
});
// Returns immediately with:
{ eventId: 'evt_abc123' }
What happens:
- tRPC validates authentication
publishEvent()dual-writes:- TimescaleDB ← Permanent audit trail
- Inngest ← Trigger workers
- Returns to user (non-blocking)
Phase 2: Permission Check (< 100ms)
permissionValidator worker processes event
// Automatic - triggered by Inngest
if (user === owner && action === 'create') {
await publishEvent({
type: 'entities.create.approved',
data: {...}
});
}
Outcomes:
- ✅ Approved → Emit
.approvedevent - ❌ Rejected → Emit
.rejectedevent (stops here) - ⏸️ Pending → Store for user approval (AI proposals)
Phase 3: Execution (< 200ms)
entitiesWorker creates the entity
// Listens to .approved events
await db.insert(entities).values({
id: entityId,
userId,
type: 'note',
title: 'Meeting Notes',
...
});
await publishEvent({
type: 'entities.create.validated',
data: { entityId }
});
// NEW: Real-time update
await fetch('http://localhost:3001/bridge/emit', {
method: 'POST',
body: JSON.stringify({
event: 'entity:created',
workspaceId: 'workspace-123',
data: { entityId, title: 'Meeting Notes' }
})
});
Phase 4: Real-Time Update (< 50ms)
Socket.IO bridge broadcasts to all clients
// Socket.IO bridge
io.of('/presence')
.to(`workspace:${workspaceId}`)
.emit('entity:created', {
entityId,
title: 'Meeting Notes',
userId
});
All connected clients receive update:
- TanStack Query cache invalidates
- UI re-renders with new entity
- Users see change instantly
Total Latency
| Phase | Operation | Time |
|---|---|---|
| 1 | API → publishEvent → Return | ~50ms |
| 2 | Permission validation | ~100ms |
| 3 | DB operation + validated event | ~200ms |
| 4 | Real-time broadcast | ~50ms |
| Total | User click → All clients updated | ~400ms |
Note: User sees optimistic update immediately (~50ms), then confirmation after validation (~400ms).
Fast-Path Optimization
Not all operations require the full 3-phase validation flow. Synap uses ValidationPolicy to route events intelligently:
When to Use Fast-Path
Operations that are:
- High-frequency: Chat messages, view tracking
- User-owned: Starring/pinning entities
- Low-risk: Thread metadata updates
- Reversible: Actions that can be easily undone
Fast-path events skip GlobalValidator and go straight to execution:
User Action → API → .validated Event → Executor → .completed Event
Total time: ~50-100ms (4x faster!)
When to Require Validation
Operations that are:
- Data creation: New entities, documents
- Deletions: Prevent accidental loss
- AI operations: Agent creation, enrichments
- Sensitive changes: Permission updates, workspace settings
Standard flow includes permission check:
User Action → API → .requested Event → GlobalValidator → .validated Event → Executor → .completed Event
Total time: ~200-500ms
Configuration
Workspace owners can customize which operations use fast-path:
// Example: Require approval for all chat messages
await client.workspaces.updateSettings({
workspaceId: "ws-123",
settings: {
validationRules: {
conversation_message: {
create: true, // Override default (was fast-path)
update: true,
delete: true
}
}
}
});
See Validation Policy for complete details on configuration and decision logic.
Event Audit Trail
After this flow completes, TimescaleDB contains:
SELECT type, timestamp, user_id, data
FROM events_timescale
WHERE subject_id = 'note_123'
ORDER BY timestamp;
Results:
entities.create.requested | 2024-12-26 14:45:00.100 | alice | {...}
entities.create.approved | 2024-12-26 14:45:00.200 | alice | {...}
entities.create.validated | 2024-12-26 14:45:00.400 | alice | {...}
Complete history preserved forever ✨
Error Handling
If Permission Check Fails
User → tRPC → publishEvent() → Inngest
↓
permissionValidator
↓
NOT OWNER → REJECT
↓
No .approved event
↓
Worker never triggers
↓
Entity not created ✅
If Inngest Fails
publishEvent() tries:
1. Write to TimescaleDB ✅
2. Send to Inngest ❌ (fails)
3. Mark event as inngest_pending: true
4. Background job retries later
Event is never lost, even if workers are down.
Comparison with Traditional
| Aspect | Traditional API | Synap Event Flow |
|---|---|---|
| Permission Check | In API route | In worker |
| Audit Trail | Manual logging | Automatic (events) |
| Real-time Updates | Polling/webhooks | Socket.IO |
| AI Approval | Hard to implement | Built-in |
| Fault Tolerance | Partial state risk | Event log + retries |
| Latency | ~100ms | ~400ms (with audit) |
Next Steps
- Permission Model - Authorization details
- Core Patterns - Architecture patterns
- Event Catalog - All event types