Skip to main content
ThinkEx is built with a modern, scalable architecture that enables real-time collaboration and maintains data consistency through event sourcing. This page explains the core architectural patterns and design decisions.

High-Level Architecture

ThinkEx follows a client-server architecture with real-time synchronization:
  • Frontend: Next.js 16 with React 19, server and client components
  • Backend: Next.js API routes with PostgreSQL database
  • State Management: Event sourcing with optimistic updates
  • Real-time: PostgreSQL triggers with broadcast for multi-user collaboration
  • Authentication: Better Auth for secure user sessions

Event Sourcing Architecture

ThinkEx uses event sourcing as its core state management pattern. Instead of storing the current state directly, all changes are stored as immutable events that can be replayed to derive the current state.

Why Event Sourcing?

  • Complete Audit Trail: Every change is recorded with timestamp, user, and payload
  • Time Travel: Replay events to see workspace state at any point in time
  • Conflict Resolution: Events are ordered and versioned to handle concurrent edits
  • Undo/Redo: Built-in support by replaying or removing events
  • Real-time Sync: Events are broadcast to all collaborators immediately

Event Flow

Event Types

All workspace changes are represented as events in src/lib/workspace/events.ts:

Workspace Events

  • WORKSPACE_CREATED
  • GLOBAL_TITLE_SET
  • WORKSPACE_SNAPSHOT

Item Events

  • ITEM_CREATED
  • ITEM_UPDATED
  • ITEM_DELETED
  • BULK_ITEMS_CREATED
  • BULK_ITEMS_UPDATED

Folder Events

  • ITEM_MOVED_TO_FOLDER
  • ITEMS_MOVED_TO_FOLDER
  • FOLDER_CREATED_WITH_ITEMS

Layout Events

  • BULK_ITEMS_UPDATED (layout changes)
See src/lib/workspace/events.ts:9 for full event type definitions.

Event Reducer

The event reducer (src/lib/workspace/event-reducer.ts:9) is a pure function that applies events to state:
function eventReducer(state: AgentState, event: WorkspaceEvent): AgentState {
  switch (event.type) {
    case 'ITEM_CREATED':
      return { ...state, items: [...state.items, event.payload.item] };
    // ... other cases
  }
}
State is derived by replaying all events in chronological order:
const finalState = events.reduce(eventReducer, initialState);
The reducer is deterministic - replaying the same events always produces the same state. This property is critical for multi-user collaboration.

Snapshot Optimization

To avoid replaying thousands of events on every load, ThinkEx creates periodic snapshots of workspace state:
  • Snapshots are stored in the workspace_snapshots table
  • Each snapshot includes a version number and full state
  • When loading a workspace, start from the latest snapshot and replay only newer events
  • Snapshots are created automatically every N events (configurable)
See src/lib/workspace/snapshot-manager.ts for snapshot logic.

Workspace State Management

State Structure

Workspace state is defined in src/lib/workspace-state/types.ts:196:
interface AgentState {
  items: Item[];           // All cards (notes, PDFs, folders, etc.)
  globalTitle: string;     // Workspace title
  lastAction?: string;     // Last action performed
  workspaceId?: string;    // Database workspace ID
}

Item Types

ThinkEx supports multiple card types, all stored as Item objects:
  • note: Rich-text notes using BlockNote editor
  • pdf: PDF documents with annotations and OCR
  • flashcard: Study flashcards with front/back content
  • youtube: Embedded YouTube videos with transcripts
  • image: Image cards with captions
  • audio: Audio files with transcriptions and speaker diarization
  • quiz: Multiple-choice and true/false quizzes
  • folder: Container items for organizing cards
Each item has:
  • id: Unique identifier
  • type: Card type
  • name: User-editable title
  • data: Type-specific data (NoteData, PdfData, etc.)
  • layout: Position and size on canvas
  • color: Background color
  • folderId: Parent folder (optional)
  • lastModified: Timestamp for conflict detection
See src/lib/workspace-state/types.ts:177 for full Item interface.

State Synchronization

ThinkEx uses a hybrid approach combining optimistic updates with event sourcing:
  1. Optimistic Updates: UI updates immediately before server confirmation
  2. Event Broadcasting: Server broadcasts events to all connected clients
  3. State Reconciliation: Clients merge remote events with local state
  4. Conflict Detection: Version numbers detect concurrent modifications
// Optimistic update
setLocalState(newState);

// Send event to server
await fetch('/api/workspaces/[id]/events', {
  method: 'POST',
  body: JSON.stringify(event)
});

// Server broadcasts to all clients
// Clients receive and apply event

Real-Time Collaboration

ThinkEx supports real-time multi-user collaboration where multiple users can edit the same workspace simultaneously.

Collaboration Model

Workspaces have three permission levels:
  • Owner: Full control (delete workspace, manage collaborators)
  • Editor: Can add/edit/delete cards and invite others
  • Viewer: Read-only access
See database schema at src/lib/db/schema.ts:188 (workspace_collaborators table).

Event Broadcasting

When a user makes a change:
  1. Event is inserted into workspace_events table
  2. PostgreSQL trigger fires (workspace_events_realtime_broadcast)
  3. Event is broadcast to channel workspace:<id>:events
  4. All subscribed clients receive the event via WebSocket
  5. Clients apply event to their local state using the reducer
See drizzle/0001_add_realtime_collaboration.sql:37 for trigger definition.

Conflict Resolution

ThinkEx handles conflicts using version vectors:
  • Each event has a version number that increments sequentially
  • Clients track the last version they’ve seen
  • When sending events, clients include their current version
  • Server rejects events with stale versions (conflict detected)
  • Client must fetch latest events and retry
// Client sends event with version
{ event, baseVersion: 42 }

// Server checks current version is 42
if (currentVersion !== baseVersion) {
  return { conflict: true, currentEvents };
}

Presence & Cursors

ThinkEx shows live cursors for all active collaborators:
  • Cursor positions are broadcast via the same WebSocket channel
  • Cursor data includes user name, color, and position
  • Stale cursors (no update for 30s) are automatically removed
See src/hooks/workspace/use-workspace-realtime.ts for real-time hooks.

Database Schema

Core Tables

workspaces

Workspace metadata: title, owner, slug, timestamps

workspace_events

Event log: all changes to workspaces

workspace_snapshots

Periodic snapshots of workspace state

workspace_collaborators

Access control: who can view/edit workspaces

chat_threads

AI conversation threads within workspaces

chat_messages

Individual messages in threads
Full schema: src/lib/db/schema.ts

Row-Level Security (RLS)

All tables use PostgreSQL Row-Level Security policies:
  • Users can only access their own workspaces or workspaces they collaborate on
  • Editors can insert events; viewers cannot
  • Share links bypass RLS with token-based authentication
See RLS policies in src/lib/db/schema.ts and migration files in drizzle/.

API Design

ThinkEx uses Next.js API routes with REST-style endpoints:

Key Endpoints

  • GET /api/workspaces - List user’s workspaces
  • POST /api/workspaces - Create new workspace
  • GET /api/workspaces/[id] - Get workspace metadata
  • PATCH /api/workspaces/[id] - Update workspace
  • DELETE /api/workspaces/[id] - Delete workspace
  • GET /api/workspaces/[id]/events - Fetch event log
  • POST /api/workspaces/[id]/events - Append new events
  • POST /api/workspaces/[id]/events/undo - Undo last event
  • GET /api/workspaces/[id]/collaborators - List collaborators
  • POST /api/workspaces/[id]/collaborators - Invite collaborator
  • DELETE /api/workspaces/[id]/collaborators/[id] - Remove collaborator
API routes are located in src/app/api/.

Design Patterns

Event Sourcing

All state changes are events that can be replayed. See Event Sourcing section above.

Optimistic UI

UI updates immediately before server confirmation, then reconciles on success/failure.

Repository Pattern

Database access is abstracted through service layers in src/lib/.

Factory Pattern

Workspace templates use factories to generate initial state: src/lib/workspace/templates.ts.

Observer Pattern

Real-time updates use WebSocket subscriptions to broadcast events.

Performance Optimizations

Snapshot System

Periodic snapshots reduce event replay time from O(n) to O(m) where m is much less than n

Indexed Queries

Database indexes on workspace_id, user_id, version, timestamp for fast queries

Lazy Loading

PDF pages and images load on-demand, not upfront

React Query

TanStack Query handles caching, deduplication, and background refetching

Build docs developers (and LLMs) love