Project structure ​
Refract.Chat is one repository with several packages. Each has its own package.json and yarn.lock, and its commands run inside it (yarn --cwd <package> ...).
| Package | What it is |
|---|---|
sdk/ | @refractchat/sdk: the REST client, socket manager, shared types and logic both sides need |
backend/ | The server: Node.js, Express, Socket.IO and Sequelize, in TypeScript run with tsx |
frontend/ | The web app: React, Vite and Tailwind |
electron/ | The desktop app: a shell around the web app, with a preload bridge for native features |
mobile/ | The mobile app: Expo and React Native, using the SDK directly |
relay/ | The push relay that forwards mobile notifications to FCM and APNs |
docker/ | Compose files and example configs for deployment |
website/ | This site |
The SDK is the contract ​
Everything the server and the apps agree on lives in sdk/src/types/: request and response shapes, socket events, permissions and preferences. Change a contract there first, then the backend that produces it, then the apps that use it.
- No second copy. Import shared types from the SDK, never redefine them. Types only the backend needs go in
backend/types/. - No framework code. The SDK runs in browsers and Node.js, so it stays free of React, Express and Sequelize. Logic both sides need, such as permission maths, mention parsing and notification rules, belongs in it rather than duplicated.
- The apps call the API through it. The web app uses
import { api } from "@refractchat/sdk", never rawfetchcalls. - Permission bits are stored in the database. Only ever append new ones; never reorder or reuse a bit.
The SDK is ESM, so its relative imports end in .js. Yarn copies it into each package, so after changing it run ./sync-sdk.sh (see Getting started).
Backend ​
A request goes route → middleware → controller → service → model:
- Routes (
src/routes/api/, mounted under/api/v1) compose middleware for sign-in, rate limits, membership and permissions. - Controllers (
src/controllers/) validate the request, call services, reply, and emit socket events. - Services (
src/services/) hold the database access and domain logic, with no Express in them. They returnnullfor not found, and use transactions for multi-step writes. - Models, migrations and seeders are plain JavaScript in
models/,migrations/andseeders/. Development and tests use SQLite; production uses MariaDB.
Permissions are deny-first bitmasks, resolved and cached in Redis. Every new endpoint needs the right middleware and checks of its own: a database lookup succeeding doesn't mean the caller may see the result. Redis also holds live state: presence, read positions, voice grants and the job queues.
Web app ​
@/points atsrc/. UI primitives are insrc/components/ui/, icons come fromlucide-react.- Rendering lives in
components/, reusable React behaviour inhooks/, plain helpers inlib/. - Style with Tailwind and the theme's tokens (
bg-card,text-foreground,border-border), not hard-coded colours. The palette is CSS variables insrc/styles/theme.css, which people's own themes override. - The providers in
App.tsx, outermost first:AuthProvider,SocketProvider,PreferencesProvider,NotificationSettingsProvider,DataProvider,ThemeProvider,LivekitProvider. App-wide side effects are small*Bridgecomponents inside them. - Socket subscriptions go through the SDK's socket manager, and each effect returns its cleanup function.
Desktop app ​
The web app in the desktop shell never touches Node.js. Native features are exposed only through electron/preload.js, typed in frontend/src/electron.d.ts. The packaged app is served from its own origin, refract://app.
Further reading ​
backend/docs/ and docs/ in the repository hold the detailed design notes: the permission system, caching, the service layer, sockets, LiveKit, federation and more.