Skip to content

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.

Flowstate consists of four main components:

  1. Web Application: Astro 5 + React 19 app served to browsers
  2. Desktop Application: Electron app with bundled React SPA
  3. Backend Services: Express.js API server with dual transport (REST + RPC)
  4. 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 Storage

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.

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 control

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.

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.

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.

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.

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 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.

  1. Browser makes a request to /api/rooms
  2. Astro server receives the request
  3. Proxy route forwards the request to the backend
  4. Backend processes the request and returns a response
  5. Proxy route streams the response back to the browser

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.

Always use relative URLs in client-side code:

// CORRECT
const response = await fetch('/api/rooms', {
credentials: 'include',
});
// WRONG - do not use absolute URLs or env vars
const 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.

The backend exposes two APIs from the same codebase:

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 rooms
  • POST /api/rooms: Create a room
  • GET /api/auth/me: Get current user
  • GET /sync/canvas: SSE stream for canvas updates

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 rooms
  • RoomService.CreateRoom: Create a room
  • AuthService.GetCurrentUser: Get current user
  • CanvasService.GetPositions: Get canvas positions

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 route
app.get('/api/rooms', async (req, res) => {
const rooms = await roomService.listRooms(req.user.id);
res.json({ data: rooms });
});
// RPC handler
async listRooms(req: ListRoomsRequest) {
const rooms = await roomService.listRooms(req.userId);
return { rooms };
}

Changes to domain logic automatically apply to both APIs.

Flowstate syncs state changes in real-time using Server-Sent Events (SSE) backed by Redis pub/sub.

  1. Client opens an SSE connection to /sync/canvas
  2. Backend subscribes to Redis channels for that user
  3. When a state change happens (tile moved, room updated, etc.), the backend publishes an event to Redis
  4. Redis broadcasts the event to all subscribed backend instances
  5. Each backend instance sends the event to connected clients via SSE
  6. Clients receive the event and update their local state

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.

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.

Flowstate uses a tiered rate limiting system to protect against abuse and ensure fair resource allocation.

TierWindowMax RequestsUse Case
exemptN/AUnlimitedHealth checks, internal services
strict1 min5Password reset, account deletion
expensive1 min10File uploads, exports, AI operations
upload1 min20Avatar uploads, file attachments
canvas1 min100Tile movements, position updates
write1 min60Create/update/delete operations
read1 min120List/get operations
standard1 min60Default for unclassified endpoints

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.

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.

Every request flows through a middleware chain before reaching the route handler:

  1. CORS: Sets CORS headers based on the request origin
  2. Body Parsing: Parses JSON and URL-encoded request bodies
  3. Rate Limiting: Checks rate limits and rejects over-limit requests
  4. Authentication: Validates session cookies or Bearer tokens
  5. Route Handler: Processes the request and returns a response
  6. Error Handling: Catches errors and returns structured error responses

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();
}

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.

The RPC gateway exposes services covering all domain operations:

  • SignUp: Create a new account
  • SignIn: Sign in with email and password
  • SignOut: End the current session
  • GetCurrentUser: Get the authenticated user
  • UpdateProfile: Update user profile
  • DeleteAccount: Delete the user’s account
  • ListRooms: List all rooms for the user
  • GetRoom: Get a room by ID
  • CreateRoom: Create a new room
  • UpdateRoom: Update room details
  • DeleteRoom: Delete a room
  • AddSpark / UpdateSpark / DeleteSpark: Manage sparks in a room
  • ListOffices / GetOffice / CreateOffice / UpdateOffice / DeleteOffice: Manage offices
  • AddMember / RemoveMember / UpdateMemberRole: Manage membership
  • CreateInvite / AcceptInvite: Invite flow
  • GetPositions: Get all canvas positions for the user
  • UpdatePosition / BulkUpdatePositions: Move tiles
  • DeletePosition: Remove a tile from the canvas
  • PinPosition / UnpinPosition: Lock/unlock tile position
  • ResetCanvas: Clear all positions
  • 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)

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.

Flowstate uses three data stores:

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 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.

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.

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 + S3

Web 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.

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.

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.

All API inputs are validated with Zod schemas. Invalid inputs are rejected with 400 Bad Request and detailed error messages.

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.

  • 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.
  • Health Checks: All services expose /health endpoints 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.

Currently, the web and desktop apps have separate canvas implementations. The plan is to unify them into a shared package that both apps import.

The RPC gateway is designed to support mobile clients. iOS and Android apps will connect via Connect RPC, just like the desktop app.

The desktop app will support offline mode with local storage and sync when reconnected. This requires a local database (SQLite) and conflict resolution logic.

The web app can be deployed to edge locations (Cloudflare Workers, Vercel Edge) for lower latency. The backend stays centralized.

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.