Event-Driven Architecture
Technical documentation for developers and architects
Design Philosophy
Core Principles
- Event-Driven First: Inngest is the central event bus - all communication goes through events
- CQRS Pattern: Commands (writes) publish events, Queries (reads) access projections directly
- Event Sourcing: TimescaleDB event store is the single source of truth (immutable history)
- Hybrid Storage: PostgreSQL for metadata, R2/MinIO for content (strict separation)
- Type-Safe: TypeScript strict mode + Zod runtime validation (SynapEvent schema)
- PostgreSQL-Only: Unified database with TimescaleDB and pgvector extensions
- LLM-Agnostic: Switch AI providers with configuration
Complete Data Flow
Architecture Overview
Synap Backend follows a pure event-driven architecture where all state changes flow through events. The complete flow is:
UI or Automation (Agents) → Events → Workers → Data Layer (Database & File Storage)
- Visual Schema
- Mermaid Code
```mermaid
graph TD
A[UI/Client App] -->|User Action| B[tRPC API]
C[Automation/Agent] -->|Agent Action| B
B -->|Publishes Event| D[Event Store<br/>TimescaleDB]
D -->|Triggers| E[Inngest Event Bus]
E -->|Dispatches| F[Workers<br/>synap/jobs]
F -->|Updates| G[Database<br/>PostgreSQL]
F -->|Stores Content| H[File Storage<br/>R2/MinIO]
F -->|Can Trigger| C
G -->|Reads| B
H -->|Reads| B
```
Detailed Flow Breakdown
1. Entry Points: UI or Automation (Agents)
UI Entry Point
// User creates a note via UI
User → App → @synap/client → tRPC API
Automation Entry Point (Agents)
// Agent creates a note automatically
Agent (LangGraph) → Tool Execution → tRPC API
Key Point: Both UI and agents use the same API and publish the same events.
2. Event Creation
Location: packages/api/src/routers/
All mutations create events:
// Example: Creating a note
const event = createSynapEvent({
type: EventTypes.NOTE_CREATION_REQUESTED,
userId,
aggregateId: entityId,
data: { content, title },
source: 'api', // or 'agent' if from automation
});
// Append to Event Store (TimescaleDB)
await eventRepo.append(event);
// Publish to Inngest for async processing
await publishEvent('api/event.logged', eventData);
Event Store: TimescaleDB hypertable for immutable event history.
3. Event Bus (Inngest)
Location: packages/jobs/src/functions/event-dispatcher.ts
Inngest receives events and dispatches them to registered handlers:
export const eventDispatcher = inngest.createFunction(
{ id: 'event-dispatcher' },
{ event: 'api/event.logged' },
async ({ event, step }) => {
const synapEvent = event.data;
// Route to appropriate handler
const handler = getHandler(synapEvent.type);
if (handler) {
await handler.handle(synapEvent);
}
}
);
Features:
- Automatic retries on failure
- Event filtering
- Handler registration
- Async processing
4. Workers (Event Handlers)
Location: packages/jobs/src/handlers/
Workers implement IEventHandler and process events:
export class NoteCreationHandler implements IEventHandler {
eventType = EventTypes.NOTE_CREATION_REQUESTED;
async handle(event: SynapEvent, step: InngestStep): Promise<HandlerResult> {
// Step 1: Upload content to storage
const fileMetadata = await step.run('upload-to-storage', async () => {
const storagePath = storage.buildPath(userId, 'note', entityId, 'md');
return await storage.upload(storagePath, content);
});
// Step 2: Create entity in database projection
await step.run('create-entity-projection', async () => {
await db.insert(entities).values({
id: entityId,
userId,
type: 'note',
title,
filePath: fileMetadata.path,
});
});
// Step 3: Publish completion event
await publishEvent('note.creation.completed', { entityId });
}
}
Key Responsibilities:
- Execute business logic
- Update database projections
- Store content in file storage
- Publish new events (if needed)
5. Data Layer
Database (PostgreSQL)
- Metadata: Entities, tags, relations, etc.
- Event Store: Immutable event history (TimescaleDB)
- Vector Store: Embeddings for semantic search (pgvector)
File Storage (R2/MinIO)
- Content: Note content, file attachments, etc.
- Path Structure:
{userId}/{entityType}/{entityId}.{ext}
Complete Example: Note Creation Flow
Step-by-Step Flow
- Visual Schema
- Mermaid Code
```mermaid
sequenceDiagram
participant UI as UI/Client
participant API as tRPC API
participant Store as Event Store
participant Bus as Inngest Bus
participant Worker as Worker Handler
participant DB as PostgreSQL
participant Storage as R2/MinIO
UI->>API: notes.create({content, title})
API->>API: Validate input
API->>API: Create SynapEvent
API->>Store: Append event (note.creation.requested)
API->>Bus: Publish 'api/event.logged'
API-->>UI: {status: 'pending', requestId}
Bus->>Worker: Dispatch event
Worker->>Storage: Upload content
Storage-->>Worker: File metadata
Worker->>DB: Insert entity record
Worker->>Store: Append event (note.creation.completed)
Worker->>Bus: Publish 'note.creation.completed'
Note over UI: Real-time update via WebSocket
Bus-->>UI: Notification: note created
```
Code Flow
- UI/Agent → Calls
notes.createvia tRPC - API Router → Validates, creates event, publishes to Inngest
- Event Store → Event appended to TimescaleDB (immutable)
- Inngest → Dispatches to registered handler
- Worker → Processes event:
- Uploads content to storage
- Creates entity in database
- Publishes completion event
- Data Layer → Updated (database + storage)
- Real-time → UI notified via WebSocket
Agent Integration
How Agents Fit In
Agents (LangGraph workflows) can:
- Read from database/storage (queries)
- Create events via API (same as UI)
- Be triggered by events (via workers)
// Agent tool that creates an entity
export const createEntityTool = {
name: 'createEntity',
execute: async (input, context) => {
// Agent calls the same API as UI
const result = await apiClient.notes.create.mutate({
content: input.content,
title: input.title,
});
return result;
},
};
Key Point: Agents use the same event-driven flow as UI actions.
Best Practices
- Always publish events for state changes - Never update projections directly from API
- Use projections for reads - Fast queries from materialized views
- Keep handlers idempotent - Events can be reprocessed
- Validate events with Zod - Type safety at runtime
- Log all events - Full audit trail
- Use Inngest for async work - Never block API responses
- Agents follow same pattern - Use API to create events, not direct DB access
Benefits
Scalability
- Horizontal scaling of workers
- Event bus handles load distribution
- No database bottlenecks
Reliability
- Automatic retries
- Event replay capability
- Full audit trail
Maintainability
- Clear separation of concerns
- Easy to add new handlers
- Type-safe event schemas
- Agents and UI use same patterns
Next: See AI Architecture for agent implementation details.