Architecture Overview
Flowstate Canvas is a distributed system built on modern web technologies. This document explains how the pieces fit together, how data flows through the system, and how the architecture supports real-time collaboration at scale.
High-Level System Design
Section titled “High-Level System Design”Flowstate consists of four main components:
- Web Application: Astro 5 + React 19 app served to browsers
- Desktop Application: Electron app with bundled React SPA
- Backend Services: Express.js API server with dual transport (REST + RPC)
- Data Layer: MongoDB for documents, Redis for cache/pub-sub, S3-compatible storage for files
Here’s how they connect:
Browser ↓Astro Web App ↓ (server-side proxy)Backend REST API ↓MongoDB + Redis + Object Storage
Desktop (Electron) ↓RPC Gateway ↓Backend Services (shared with REST) ↓MongoDB + Redis + Object StorageKey Architectural Decisions
Section titled “Key Architectural Decisions”Dual Transport: The backend exposes two APIs. REST for the web app (cookie auth, server-side proxy). RPC for desktop and mobile (Bearer token auth, direct connection). Both APIs call the same business logic.
API Proxy Pattern: The browser never talks directly to the backend. All /api/* requests go through the Astro server, which proxies them to the backend. This hides the backend URL, simplifies CORS, and enables server-side auth checks.
Real-Time Sync: All state changes publish events to Redis. Clients subscribe via Server-Sent Events (SSE). When you move a tile, the backend publishes a position update. All connected devices receive it and update their canvas.
Monorepo Structure: All code lives in one repository. Apps share types, utilities, and business logic through workspace packages. This keeps the codebase consistent and makes refactoring easier.
Codebase Structure
Section titled “Codebase Structure”The repository is organized as a pnpm workspace with multiple apps and shared packages:
flowstate-canvas/├── apps/│ ├── backend/ # Express.js API server│ ├── web/ # Astro web app│ ├── desktop/ # Electron desktop app│ ├── admin/ # Admin dashboard (Astro)│ └── docs/ # Starlight documentation├── packages/│ └── shared/ # Shared types and utilities└── tools/ └── flowstate-mcp/ # MCP server for AI agent controlBackend
Section titled “Backend”The backend is an Express.js server written in TypeScript with ESM modules. It handles authentication, data persistence, real-time sync, and business logic.
Key areas:
- Database client (Prisma), Redis client, storage client
- CORS, body parsing, rate limiting, authentication middleware
- REST API endpoints organized by domain
- Connect RPC service implementations
- Domain services (transport-agnostic business logic)
- Database schema and seed scripts
The backend runs as two separate services from the same codebase — one for REST and one for RPC. They share the same database and Redis instance but expose different APIs.
Web App
Section titled “Web App”The web app is built with Astro 5 and React 19. Astro handles routing, server-side rendering, and static generation. React provides interactive components and client-side state management.
Key areas:
- Interactive React components
- Zustand stores, utilities, hooks
- Route pages (Astro files)
- Global CSS (Tailwind v4)
API Proxy: A catch-all route proxies all /api/* requests to the backend. It forwards cookies, headers, and request bodies, then streams the response back to the browser.
Desktop App
Section titled “Desktop App”The desktop app is an Electron application with a React SPA. It’s structured like Discord: the main process manages windows and system integration, the renderer process runs the React app.
Key areas:
- Electron main process (window management, IPC, automation bridge)
- Preload scripts (contextBridge for secure IPC)
- React SPA (canvas, UI, stores)
Build system: The desktop app uses electron-vite for development and building. In production, it bundles the SPA and loads it via loadFile().
Automation Bridge: The main process exposes an HTTP server that allows AI agents and automation tools to control the desktop app via HTTP requests. This is a development-only feature.
Admin Dashboard
Section titled “Admin Dashboard”The admin dashboard is an Astro app for platform administrators. It provides tools for user management, content moderation, analytics, and system monitoring.
Authentication: Admins sign in via OIDC (Google Workspace or similar provider). The backend verifies OIDC tokens and checks admin roles before granting access.
Audit Logging: All admin actions are logged with timestamps, actor info, and action details.
Shared Package
Section titled “Shared Package”The shared package contains TypeScript types and utilities used across multiple apps.
Contents:
- Type definitions for API requests/responses
- Shared constants (grid size, rate limits, etc.)
- Utility functions (ID generation, validation, etc.)
The API Proxy Pattern
Section titled “The API Proxy Pattern”The web app uses a server-side proxy for all backend API calls. This is a critical architectural decision that affects how frontend code is written.
How It Works
Section titled “How It Works”- Browser makes a request to
/api/rooms - Astro server receives the request
- Proxy route forwards the request to the backend
- Backend processes the request and returns a response
- Proxy route streams the response back to the browser
Why We Do This
Section titled “Why We Do This”Security: The backend URL is never exposed to the browser. Clients can’t bypass the proxy and hit the backend directly.
Simplified CORS: The browser sees all requests as same-origin. No CORS preflight, no credential issues.
Server-Side Auth: The Astro server can inspect cookies, validate sessions, and add auth headers before forwarding requests.
Environment Flexibility: The backend URL is configured server-side via SERVER_API_URL. You can change it without rebuilding the frontend.
Writing Frontend Code
Section titled “Writing Frontend Code”Always use relative URLs in client-side code:
// CORRECTconst response = await fetch('/api/rooms', { credentials: 'include',});
// WRONG - do not use absolute URLs or env varsconst response = await fetch(`${API_URL}/api/rooms`);The credentials: 'include' option ensures cookies are sent with the request. This is required for session-based authentication.
Dual Transport: REST and RPC
Section titled “Dual Transport: REST and RPC”The backend exposes two APIs from the same codebase:
REST API (Internal)
Section titled “REST API (Internal)”Audience: Web app (via Astro proxy), admin dashboard
Authentication: Cookie-based sessions
Endpoints: /api/*, /sync/*, /upload/*
The REST API is designed for internal use. In production, it’s not publicly exposed. Only the Astro server can reach it.
Example routes:
GET /api/rooms: List roomsPOST /api/rooms: Create a roomGET /api/auth/me: Get current userGET /sync/canvas: SSE stream for canvas updates
RPC Gateway (Public)
Section titled “RPC Gateway (Public)”Audience: Desktop app, mobile apps, third-party integrations
Authentication: Bearer token (JWT)
Endpoints: /rpc (Connect RPC), /health
The RPC gateway is a separate service that exposes only the RPC API. It’s designed to be publicly accessible.
Example RPCs:
RoomService.ListRooms: List roomsRoomService.CreateRoom: Create a roomAuthService.GetCurrentUser: Get current userCanvasService.GetPositions: Get canvas positions
Shared Service Layer
Section titled “Shared Service Layer”Both APIs call the same business logic. Domain services are transport-agnostic:
// Service (transport-agnostic)export class RoomService { async listRooms(userId: string) { return prisma.room.findMany({ where: { userId } }); }}
// REST routeapp.get('/api/rooms', async (req, res) => { const rooms = await roomService.listRooms(req.user.id); res.json({ data: rooms });});
// RPC handlerasync listRooms(req: ListRoomsRequest) { const rooms = await roomService.listRooms(req.userId); return { rooms };}Changes to domain logic automatically apply to both APIs.
Real-Time Sync
Section titled “Real-Time Sync”Flowstate syncs state changes in real-time using Server-Sent Events (SSE) backed by Redis pub/sub.
How It Works
Section titled “How It Works”- Client opens an SSE connection to
/sync/canvas - Backend subscribes to Redis channels for that user
- When a state change happens (tile moved, room updated, etc.), the backend publishes an event to Redis
- Redis broadcasts the event to all subscribed backend instances
- Each backend instance sends the event to connected clients via SSE
- Clients receive the event and update their local state
Event Types
Section titled “Event Types”Position Updates: When a tile moves, all connected clients update their canvas.
CRUD Events: When a room is created, updated, or deleted, clients add, update, or remove the corresponding tile.
Cursor Movements: When a user moves their cursor, other users see the cursor position in real-time.
Presence Updates: When a user goes online or offline, clients update the user’s status indicator.
SSE vs WebSockets
Section titled “SSE vs WebSockets”Flowstate uses SSE instead of WebSockets for a few reasons:
Simplicity: SSE is a one-way stream from server to client. No handshake, no protocol negotiation, no binary framing.
HTTP/2 Multiplexing: SSE works over HTTP/2, which multiplexes multiple streams over a single connection. This is more efficient than opening multiple WebSocket connections.
Automatic Reconnection: Browsers automatically reconnect SSE streams if the connection drops. No client-side reconnection logic needed.
Firewall Friendly: SSE uses standard HTTP, so it works through corporate firewalls and proxies that block WebSockets.
For client-to-server updates (moving a tile, creating a room), clients make regular HTTP POST requests. The backend processes the request, updates the database, and publishes an event to Redis. The event flows back to all clients via SSE.
Rate Limiting
Section titled “Rate Limiting”Flowstate uses a tiered rate limiting system to protect against abuse and ensure fair resource allocation.
Rate Limit Tiers
Section titled “Rate Limit Tiers”| Tier | Window | Max Requests | Use Case |
|---|---|---|---|
exempt | N/A | Unlimited | Health checks, internal services |
strict | 1 min | 5 | Password reset, account deletion |
expensive | 1 min | 10 | File uploads, exports, AI operations |
upload | 1 min | 20 | Avatar uploads, file attachments |
canvas | 1 min | 100 | Tile movements, position updates |
write | 1 min | 60 | Create/update/delete operations |
read | 1 min | 120 | List/get operations |
standard | 1 min | 60 | Default for unclassified endpoints |
How It Works
Section titled “How It Works”Rate limits are enforced by middleware that runs before route handlers. The middleware identifies the user (by session or IP address), checks Redis for the current request count, and returns 429 Too Many Requests if over the limit.
Rate limit state is stored in Redis with a TTL matching the window duration. When the window expires, the count resets.
Applying Rate Limits
Section titled “Applying Rate Limits”Routes specify their tier in the route definition:
app.get('/api/rooms', rateLimit('read'), async (req, res) => { // Handler});
app.post('/api/rooms', rateLimit('write'), async (req, res) => { // Handler});If no tier is specified, the route uses the standard tier.
Middleware Chain
Section titled “Middleware Chain”Every request flows through a middleware chain before reaching the route handler:
- CORS: Sets CORS headers based on the request origin
- Body Parsing: Parses JSON and URL-encoded request bodies
- Rate Limiting: Checks rate limits and rejects over-limit requests
- Authentication: Validates session cookies or Bearer tokens
- Route Handler: Processes the request and returns a response
- Error Handling: Catches errors and returns structured error responses
Authentication Middleware
Section titled “Authentication Middleware”The requireAuth middleware validates the user’s session:
export function requireAuth(req, res, next) { const sessionId = req.cookies.sessionId; if (!sessionId) { return res.status(401).json({ error: 'Unauthorized' }); }
const session = await prisma.session.findUnique({ where: { id: sessionId }, include: { user: true }, });
if (!session || session.expiresAt < new Date()) { return res.status(401).json({ error: 'Session expired' }); }
req.user = session.user; next();}Error Handling
Section titled “Error Handling”The error handling middleware catches all errors and returns structured responses:
app.use((err, req, res, next) => { if (err instanceof ZodError) { return res.status(400).json({ error: 'Validation error', details: err.errors, }); }
res.status(500).json({ error: 'Internal server error', message: err.message, });});This ensures clients always receive a JSON response, even when something goes wrong.
RPC Services
Section titled “RPC Services”The RPC gateway exposes services covering all domain operations:
AuthService
Section titled “AuthService”SignUp: Create a new accountSignIn: Sign in with email and passwordSignOut: End the current sessionGetCurrentUser: Get the authenticated userUpdateProfile: Update user profileDeleteAccount: Delete the user’s account
RoomService
Section titled “RoomService”ListRooms: List all rooms for the userGetRoom: Get a room by IDCreateRoom: Create a new roomUpdateRoom: Update room detailsDeleteRoom: Delete a roomAddSpark/UpdateSpark/DeleteSpark: Manage sparks in a room
OfficeService
Section titled “OfficeService”ListOffices/GetOffice/CreateOffice/UpdateOffice/DeleteOffice: Manage officesAddMember/RemoveMember/UpdateMemberRole: Manage membershipCreateInvite/AcceptInvite: Invite flow
CanvasService
Section titled “CanvasService”GetPositions: Get all canvas positions for the userUpdatePosition/BulkUpdatePositions: Move tilesDeletePosition: Remove a tile from the canvasPinPosition/UnpinPosition: Lock/unlock tile positionResetCanvas: Clear all positions
Other Services
Section titled “Other Services”- ShelfService: Manage bookmarked links
- PassportService: Manage user profile and work history
- WidgetService: Install, configure, and remove canvas widgets
- ModerationService: Report content and handle moderation decisions
- AdminService: Platform metrics and audit log access (admin only)
Type Safety
Section titled “Type Safety”All RPCs are defined with Protocol Buffers schemas. The Connect RPC framework generates TypeScript types from the schemas, ensuring type safety across the client-server boundary.
Data Layer
Section titled “Data Layer”Flowstate uses three data stores:
MongoDB (Documents)
Section titled “MongoDB (Documents)”MongoDB stores all application data: users, sessions, rooms, sparks, offices, canvas positions, etc.
Why MongoDB: Flexible schema, rich query language, good performance for document-heavy workloads.
ORM: Prisma provides a type-safe client for MongoDB with full TypeScript types. The schema defines indexes for common queries, created automatically on schema push.
Redis (Cache and Pub/Sub)
Section titled “Redis (Cache and Pub/Sub)”Redis serves two purposes:
Caching: Frequently accessed data (user sessions, rate limit counters) is cached in Redis to reduce database load.
Pub/Sub: Real-time sync events flow through Redis pub/sub channels. Each user has a dedicated channel. The backend publishes events, and all connected instances receive them.
Object Storage (S3-compatible)
Section titled “Object Storage (S3-compatible)”S3-compatible object storage (MinIO in development, AWS S3 or equivalent in production) stores uploaded files: avatars, attachments, exports, etc.
Files are organized into buckets by type. The backend generates signed URLs for file uploads and downloads. Clients upload directly to storage without proxying through the backend.
Deployment Architecture
Section titled “Deployment Architecture”In production, the architecture looks like this:
Internet ↓Load Balancer (HTTPS) ├─→ Astro Web App (multiple instances) │ ↓ (internal routing) │ Backend REST API (multiple instances) │ └─→ RPC Gateway (multiple instances) ↓ Backend Services (shared) ↓ MongoDB Cluster + Redis Cluster + S3Web App: Deployed as a static site with server-side rendering. Astro builds the app into static HTML and server functions. The server functions proxy API requests to the backend.
Backend REST API: Deployed as a private service. Only reachable via internal routing from the web app.
RPC Gateway: Deployed as a public service with HTTPS. Desktop and mobile clients connect directly.
Database: MongoDB runs as a replica set for high availability. Redis runs as a cluster for failover. S3 provides durable object storage.
Scaling: All services are stateless and can scale horizontally. Redis pub/sub ensures events reach all instances.
Security Considerations
Section titled “Security Considerations”Authentication
Section titled “Authentication”Web App: Cookie-based sessions with HttpOnly, Secure, SameSite=Strict flags. Sessions expire after 7 days of inactivity.
Desktop/Mobile: Bearer token authentication with JWT. Tokens are signed with a secret key and include user ID and expiration.
Authorization
Section titled “Authorization”Role-Based Access Control: Offices have roles (owner, admin, member). Each role has defined permissions.
Resource Ownership: Users can only access their own data. The backend checks ownership before returning resources.
Admin Privileges: Admin users have elevated permissions for moderation and platform management. Admin actions are logged to the audit log.
Input Validation
Section titled “Input Validation”All API inputs are validated with Zod schemas. Invalid inputs are rejected with 400 Bad Request and detailed error messages.
Secrets Management
Section titled “Secrets Management”Secrets are injected at runtime via your secrets management solution. They never touch disk or version control. In production, use a secret manager such as AWS Secrets Manager or HashiCorp Vault.
Performance Optimizations
Section titled “Performance Optimizations”- Database Indexes: All common queries have indexes.
- Redis Caching: Sessions and rate limit counters are cached, reducing database load.
- SSE Multiplexing: SSE streams are multiplexed over HTTP/2.
- Lazy Loading: The frontend lazy-loads components and data. Only visible tiles are rendered.
- Image Optimization: Avatars and images are resized and compressed before storage.
Monitoring and Observability
Section titled “Monitoring and Observability”- Health Checks: All services expose
/healthendpoints for load balancer use. - Logging: Structured logs written to stdout, collected by a log aggregator in production.
- Metrics: The admin dashboard exposes platform metrics (active users, room count, error rates, etc.).
- Audit Logs: All admin actions are logged for compliance and debugging.
Future Architecture Plans
Section titled “Future Architecture Plans”Canvas Unification
Section titled “Canvas Unification”Currently, the web and desktop apps have separate canvas implementations. The plan is to unify them into a shared package that both apps import.
Mobile Apps
Section titled “Mobile Apps”The RPC gateway is designed to support mobile clients. iOS and Android apps will connect via Connect RPC, just like the desktop app.
Offline Mode
Section titled “Offline Mode”The desktop app will support offline mode with local storage and sync when reconnected. This requires a local database (SQLite) and conflict resolution logic.
Edge Deployment
Section titled “Edge Deployment”The web app can be deployed to edge locations (Cloudflare Workers, Vercel Edge) for lower latency. The backend stays centralized.
Summary
Section titled “Summary”Flowstate Canvas is a distributed system built on modern web technologies. The architecture supports real-time collaboration, horizontal scaling, and multi-platform clients. The dual transport design (REST + RPC) provides flexibility for web and native apps. The API proxy pattern keeps the backend secure and hidden from browsers. Real-time sync via SSE and Redis ensures state changes propagate instantly.
For more details on specific components, see the other platform documentation pages.