How to set up a local development environment, write tests, follow code style, and submit pull requests.
- Prerequisites
- Repository Structure
- Quick Start
- Backend Development
- Frontend Development
- Agent Development
- Plugin Development
- Testing
- Code Style
- Database & Migrations
- Pull Request Process
- Debugging Tips
- Common Gotchas
| Tool | Minimum Version | Purpose |
|---|---|---|
| pnpm | 9+ (repo uses pnpm 11 workspace settings) | JavaScript/TypeScript package manager for the monorepo |
| Node.js | 20+ | Peer dependency resolution |
| Rust | 1.95.0 (see catalyst-agent/rust-toolchain.toml) |
Catalyst agent (cross-compiled targets) |
| cargo | Latest stable | Rust package manager & build tool |
| Docker | 24+ or Podman | Development infra (PostgreSQL, Redis) |
| PostgreSQL | 14+ | Primary database (provided via Docker/Podman) |
| protoc | 25.1+ | Protocol buffer compilation for agent (gRPC) |
pnpm:
npm install -g pnpmRust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup component add rustfmt clippyProtocol Buffers compiler (for agent):
# Download and install protoc 25.1
curl -LO "https://github.com/protocolbuffers/protobuf/releases/download/v25.1/protoc-25.1-linux-x86_64.zip"
unzip protoc-25.1-linux-x86_64.zip -d $HOME/.local
export PATH="$HOME/.local/bin:$PATH"Catalyst is a multi-package monorepo managed via pnpm workspaces (pnpm-workspace.yaml):
catalyst/
├── catalyst-backend/ # Fastify + TypeScript API server
│ ├── src/ # Source code (routes, services, middleware)
│ ├── prisma/ # Database schema & migrations
│ ├── tests/ # Bash integration tests
│ └── package.json
├── catalyst-frontend/ # React 19 + Vite SPA (csync server state)
│ ├── src/ # React components, pages, hooks
│ ├── e2e/ # Playwright E2E tests
│ └── package.json
├── catalyst-agent/ # Rust daemon (Tokio + containerd gRPC)
│ ├── src/ # Rust source code
│ ├── Cargo.toml # Rust dependencies
│ └── config.toml # Agent runtime configuration
├── catalyst-plugins/ # Bundled plugins
│ ├── egg-explorer/ # Game egg metadata browser
│ ├── example-plugin/ # Plugin system demonstration
│ └── ticketing-plugin/ # Support ticketing (SLA, comments, bulk ops)
├── packages/plugin-sdk/ # Official Plugin SDK
│ ├── cli/ # CLI scaffolding tool
│ ├── src/ # SDK source types & helpers
│ └── templates/ # Plugin templates (backend-only, fullstack, minimal)
├── catalyst-docker/ # Docker Compose manifests
├── scripts/ # Deployment & utility scripts
├── .github/workflows/ # CI/CD pipelines
├── package.json # Root workspace config
└── docs/ # User/admin documentation
Spin up the full stack locally in under a minute:
# 1. Clone the repository
git clone https://github.com/catalystctl/catalyst.git
cd catalyst
# 2. Start PostgreSQL and Redis
pnpm run dev:infra
# 3. Seed the database
pnpm run db:seed
pnpm run db:seed:admin # Creates admin@example.com / admin123
# 4. Start both dev servers in parallel
pnpm run dev
# Navigate to http://localhost:5173
# Login: admin@example.com / admin123# Start infrastructure
cd catalyst-docker && podman-compose up -d postgres redis
# Backend (port 3000)
cd catalyst-backend
pnpm run dev
# Frontend (port 5173)
cd catalyst-frontend
pnpm run devcd catalyst-backend
# Install dependencies
pnpm install
# Generate Prisma client
pnpm run db:generate
# Push schema to database
pnpm run db:push
# Seed development data
pnpm run db:seed| Command | Description |
|---|---|
pnpm run dev |
Start Fastify server with hot reload (--watch) |
pnpm run build |
Compile TypeScript to dist/ |
pnpm run start |
Run compiled server from dist/index.js |
pnpm run lint |
Run ESLint on src/ |
pnpm run lint -- --fix |
Auto-fix linting issues |
pnpm run test |
Run Vitest unit tests |
pnpm run test:ui |
Run Vitest with UI (interactive) |
pnpm run test:coverage |
Run tests with coverage report |
pnpm run db:generate |
Regenerate Prisma client |
pnpm run db:push |
Push schema changes to database |
pnpm run db:migrate |
Create versioned migration |
pnpm run db:studio |
Open Prisma Studio GUI |
pnpm run db:seed |
Populate with test data |
pnpm run db:seed:admin |
Create admin user only |
catalyst-backend/src/
├── index.ts # Fastify bootstrap, route registration, service startup
├── auth.ts # Better Auth initialization
├── routes/ # HTTP endpoint handlers (20+ modules)
│ ├── auth.ts # Login, register, profile, passkey, 2FA
│ ├── nodes.ts # Node CRUD, health, deployment tokens
│ ├── servers.ts # Server CRUD, power, files, databases
│ ├── admin.ts # Admin operations
│ ├── roles.ts # RBAC management
│ └── ...
├── services/ # Business logic layer
│ ├── state-machine.ts # Server lifecycle state machine
│ ├── task-scheduler.ts # Cron-based scheduled tasks
│ ├── webhook-service.ts # Outbound webhook dispatch
│ ├── alert-service.ts # Alert rule evaluation
│ └── ...
├── middleware/ # Fastify middleware
│ └── rbac.ts # Permission checking
├── lib/ # Shared utilities
│ ├── permissions.ts # RBAC permission resolution
│ ├── validation.ts # Zod validators
│ ├── rate-limits.ts # Rate limit configuration
│ └── ...
└── websocket/ # WebSocket gateway
└── gateway.ts # Agent & client connection management
Server State Machine — All server state transitions are validated in the backend before sending commands to the agent. The database is the single source of truth.
// ✅ Correct pattern
ServerStateMachine.canTransition(from, to); // Validate
prisma.server.update(...); // Persist
wsGateway.sendToAgent(...); // Notify agentRBAC Middleware — Every protected route uses the permission middleware:
app.post('/api/servers/:id/start',
{ onRequest: rbac.checkPermission('server.start') },
async (request, reply) => { /* handler */ }
);cd catalyst-frontend
# Install dependencies
pnpm install
# Start development server (port 5173)
pnpm run dev
# Run unit tests
pnpm run test
# Run E2E tests (requires backend running)
pnpm run test:e2e| Command | Description |
|---|---|
pnpm run dev |
Start Vite dev server with hot module replacement |
pnpm run build |
Build production bundle |
pnpm run preview |
Preview production build locally |
pnpm run lint |
Run ESLint on .ts/.tsx files |
pnpm run lint -- --fix |
Auto-fix linting issues |
pnpm run format |
Format code with Prettier |
pnpm run test |
Run Vitest unit tests |
pnpm run test:e2e |
Run Playwright E2E tests |
pnpm run test:screenshots |
Run Playwright screenshot crawl tests |
pnpm run test:screenshots:headed |
Run screenshot crawl with visible browser |
pnpm run build:analyze |
Analyze bundle size with visualizer |
catalyst-frontend/src/
├── main.tsx # React DOM entry point
├── App.tsx # Router, lazy-loaded routes, theming
├── components/ # Reusable UI components
│ ├── layout/ # Sidebar, header, navigation
│ └── ...
├── pages/ # Page components (mirrors routes)
├── hooks/ # React hooks (Catalyst Sync wrappers)
│ ├── useServers.ts
│ ├── useAuth.ts
│ └── ...
├── stores/ # Zustand global state
│ ├── authStore.ts
│ ├── websocketStore.ts
│ └── uiStore.ts
├── services/ # API client, WebSocket service
├── lib/ # Utilities (formatters, helpers)
└── test/setup.ts # Vitest test setup
State Management — Zustand for global UI state, Catalyst Sync for server state:
// Fetch with automatic caching and refetch
// Transitional states (installing, starting, stopping, transferring) refetch every 2s
// Normal states refetch every 10s
const { data: servers } = useQuery({
queryKey: ['servers'],
queryFn: () => api.getServers(),
refetchInterval: (query) =>
query.state.data?.some((s: Server) => transitionalStatuses.has(s.status)) ? 2000 : 10000,
});Component Structure — Pages mirror the route structure. Use Radix UI primitives with Tailwind CSS for styling.
Plugin Frontend Slots — Plugins can register React components that appear in admin tabs or server detail views. Plugin frontend code is bundled alongside the main app at build time.
Zustand manages global UI state. All stores are in catalyst-frontend/src/stores/. See Frontend Global State (Stores) for detailed documentation of authStore, themeStore, backupDownloadStore, and deprecated uiStore.
Manages the WebSocket connection used for real-time console streaming and server events.
Key state:
connected— Whether the WebSocket is connectedserverId— Currently connected server (for console streaming)consoleLines— Buffered console output lineslisteners— Registered event listeners for console events
Key methods:
connect(serverId)— Open WebSocket to/api/servers/:id/console/streamdisconnect()— Close WebSocket connectionsendCommand(command)— Send console command via WebSocketonConsoleData(callback)— Subscribe to console outputaddSseSubscriber(serverId, callback)— Register SSE fallback subscribergetSseSubscriberCount(serverId)— Check subscriber count (cap: 50 per server)
SSE Fallback: When WebSocket is unavailable, the store falls back to Server-Sent Events via GET /api/servers/:id/console/stream.
Manages sidebar state, modals, and transient UI elements.
Key state:
sidebarCollapsed— Whether the sidebar is collapsedmodals— Currently open modal registrynotifications— Toast notification queue
Hooks are in catalyst-frontend/src/hooks/. They wrap API calls with Catalyst Sync for caching, refetching, and transitional state handling.
const { data: user } = useQuery(['auth/me'], () => api.getMe());
const { mutate: login } = useMutation(() => api.login(email, password));Provides:
useAuthUser()— Current authenticated user with rolesuseAuthInit()— Auto-handles 401 errors, silently refreshes expired sessionsuseAuthPermission(permission)— Checks if current user has a specific permission
const { data: servers } = useServers();
const { mutate: startServer } = useStartServer();Provides:
useServers()— Fetches all servers user has access to; refetches every 2s during transitional states (installing, starting, stopping, transferring), every 10s otherwiseuseServer(serverId)— Single server detail with owner/permissions infouseCreateServer()— Create a new server from a templateuseStartServer()/useStopServer()/useRestartServer()— Power operations with status pollinguseDeleteServer()— Delete with ownership verificationuseTransferOwnership()— Transfer server to another useruseSuspendServer()/useResumeServer()— Admin suspension operations
const { data: files } = useFiles(serverId, path);
const { mutate: uploadFile } = useUploadFile();Provides:
useFiles(serverId, path)— List files with pagination; uses/api/servers/:id/filesuseReadFile(serverId, path)— Read file content via file tunneluseWriteFile(serverId, path)— Write file via file tunneluseUploadFile(serverId, path)— Upload file via file tunneluseDeleteFile(serverId, path)— Delete file/directoryuseRenameFile(serverId, oldPath, newPath)— Rename or move fileuseCreateDirectory(serverId, path)— Create directoryuseCompressArchive(serverId, path)— Create tar.gz archiveuseExtractArchive(serverId, archivePath, destPath)— Extract archive
File Tunnel: All file operations flow through the file tunnel (/api/servers/:id/file-tunnel/*) which has its own rate limits (see Security Settings defaults table above).
const lines = useSseConsole(serverId);
const { mutate: sendCommand } = useSendConsoleCommand(serverId);Provides:
useSseConsole(serverId)— Real-time console via SSE or WebSocketuseSendConsoleCommand(serverId)— Send commands to the server consoleuseConsoleStream(serverId)— Direct WebSocket console (alternative to SSE)
useSetupStatus()— Checks if first-time panel setup is required; redirects if souseSystemErrors()— Fetches client-side error reports fromreportSystemError()useAuditLogs()— Fetches system audit logs with filteringuseNodeHealth(nodeId)— Fetches node resource metrics (CPU, memory, disk)
API services are in catalyst-frontend/src/services/. They wrap fetch() calls with proper headers, auth tokens, and error handling.
| Service | File | Purpose |
|---|---|---|
api.ts |
services/api/ |
Core API client with auth headers, error handling, abort controllers |
websocket.ts |
services/websocket/ |
WebSocket connection manager for console streaming |
sse.ts |
services/sse/ |
SSE EventSource wrapper for console streaming fallback |
| Service | File | Purpose |
|---|---|---|
servers.ts |
services/api/servers.ts |
All server CRUD, power operations, transfer, suspend |
nodes.ts |
services/api/nodes.ts |
Node management, allocation, health checks |
auth.ts |
services/api/auth.ts |
Login, register, 2FA, passkeys, sessions, profile |
files.ts |
services/api/files.ts |
File tunnel operations, upload, download, archive |
backups.ts |
services/api/backups.ts |
Backup creation, listing, restoration, deletion |
sftp.ts |
services/api/sftp.ts |
SFTP token generation, listing, revocation |
templates.ts |
services/api/templates.ts |
Server template CRUD, variables, images |
users.ts |
services/api/users.ts |
User management, invites, roles |
roles.ts |
services/api/roles.ts |
Role CRUD, permissions |
admin.ts |
services/api/admin.ts |
Admin endpoints: settings, security, system errors |
systemErrors.ts |
services/api/systemErrors.ts |
reportSystemError() client for frontend error tracking |
plugins.ts |
services/api/plugins.ts |
Plugin installation, management, config |
// api.ts automatically attaches the auth token from authStore
class ApiClient {
async request(url, options) {
const token = authStore.getState().token;
if (token) options.headers.Authorization = `Bearer ${token}`;
const response = await fetch(url, options);
if (response.status === 401) {
// useAuthInit handles 401s silently
// Does NOT destroy new sessions (prevents login loops)
}
return response;
}
}Frontend services call reportSystemError() for client-side error tracking. This function is used by 60+ modules including AuthStore, useFileManager, useSetupStatus, useSseConsole, and all page components. Errors are collected and visible on the Admin System Errors page (/admin/system-errors).
Catalyst uses Zustand for global state management. Stores are located in catalyst-frontend/src/stores/.
File: stores/authStore.ts
Manages authentication state, login/logout flows, 2FA verification, and cross-tab session synchronization.
| Field | Type | Description |
|---|---|---|
user |
User | null |
Current authenticated user object |
isAuthenticated |
boolean |
Whether the user is logged in |
isLoading |
boolean |
Whether a login/register operation is in progress |
isReady |
boolean |
Whether auth initialization is complete |
isRefreshing |
boolean |
Whether a session refresh is in progress |
error |
string | null |
Error message from the last failed operation |
rememberMe |
boolean |
Whether "remember me" was checked (persisted to localStorage) |
_broadcast |
BroadcastChannel |
BroadcastChannel for cross-tab logout sync (internal) |
Methods:
| Method | Parameters | Description |
|---|---|---|
login() |
(values: LoginSchema, options?) |
Log in with email/password. Handles 2FA redirect. Sets cookies via HttpOnly. Uses loginGuard to prevent stale 401s from wiping state. |
register() |
(values: RegisterSchema) |
Register a new account. Same cookie-based auth pattern. |
refresh() |
() |
Refresh session by calling the auth API. Silently handles 401 during login. Clears old token-based localStorage items. |
init() |
() |
Initialize auth on app mount. Skips refresh if login is in-flight. Only sets isReady when refresh completes. |
logout() |
() |
Clear local state, fire-and-forget server sign-out, broadcast logout message to other tabs. |
setUser() |
(user: User | null) |
Manually set the user object. |
setSession() |
({ user }) |
Set session from a payload (e.g., after OAuth redirect). |
verifyTwoFactor() |
({ code, trustDevice? }) |
Verify 2FA code. trustDevice stores a 30-day trust token. |
Cross-Tab Sync:
The store uses a BroadcastChannel('catalyst-auth') to synchronize logout across tabs. When one tab logs out, all other tabs are immediately logged out and redirected to /login.
// authStore.ts (simplified)
if (typeof window !== 'undefined') {
const channel = new BroadcastChannel('catalyst-auth');
channel.onmessage = (event) => {
if (event.data?.type === 'logout') {
useAuthStore.setState({ user: null, isAuthenticated: false });
window.location.href = '/login';
}
};
}Persistence:
Only rememberMe is persisted to localStorage (via Zustand persist middleware). All other state is runtime-only.
File: stores/themeStore.ts
Manages theme state, sidebar state, server view mode, custom CSS, and dynamic color preview.
| Field | Type | Description |
|---|---|---|
theme |
'light' | 'dark' |
Current theme mode |
sidebarCollapsed |
boolean |
Whether the sidebar is collapsed |
serverViewMode |
'card' | 'list' |
How servers are displayed in the list |
themeSettings |
PublicThemeSettings | null |
Server-fetched theme settings (panel name, logo, colors) |
customCssElement |
HTMLStyleElement | null |
Reference to the injected <style> element (internal) |
Methods:
| Method | Parameters | Description |
|---|---|---|
setTheme() |
(theme) |
Switch between light/dark. Applies CSS variables to <html>. |
setServerViewMode() |
(mode) |
Toggle between card and list views |
toggleSidebar() |
() |
Toggle sidebar collapsed state |
setThemeSettings() |
(settings, customCss?) |
Set theme settings from server. Applies colors as CSS variables. Injects custom CSS if provided. |
applyTheme() |
() |
Apply the current theme settings to the DOM. Sets all CSS custom properties. |
previewColors() |
({ primaryColor?, secondaryColor?, accentColor?, themeColors? }) |
Schedule a live-preview DOM update. Batched via requestAnimationFrame to eliminate jank during rapid color changes (e.g., dragging a color picker at ~60fps). |
cancelPreview() |
() |
Cancel any pending preview frame update |
injectCustomCss() |
(css) |
Inject a <style id="catalyst-custom-css"> element into <head>. Removes old element first. |
Color System:
The store converts hex colors to HSL and generates a full 90-shade scale (50–900) for primary, secondary, and accent colors. It applies over 40 CSS custom properties including --primary, --secondary, --accent, --success, --warning, --danger, --info, and all surface/shade tokens for both light and dark modes.
Persistence:
theme, sidebarCollapsed, and serverViewMode are persisted to localStorage via Zustand persist middleware.
File: stores/backupDownloadStore.ts
Tracks backup download progress per backup ID. Used for large backup file downloads.
File: stores/uiStore.ts
Deprecated alias to themeStore. Use themeStore directly.
Hooks live in catalyst-frontend/src/hooks/. All hooks are Catalyst Sync wrappers over the API services.
| Hook | Purpose | API Endpoint | Details |
|---|---|---|---|
useAdminStats() |
Platform stats | GET /api/admin/stats |
Users, servers, nodes, active servers. Refreshes every 30s. |
useAdminHealth() |
Platform health | GET /api/admin/health |
Database status, node online/offline/stale counts. Refreshes every 15s. |
useAdminServers() |
Admin server listing | GET /api/admin/servers |
Paginated, searchable. Filters by status and owner. |
useAuditLogs() |
System-wide audit log | GET /api/admin/audit-logs |
Filters by action, resource, user, date range. |
useDatabaseHosts() |
Database host config | GET /api/admin/database-hosts |
List all database host configurations. |
useSmtpSettings() |
SMTP configuration | GET /api/admin/smtp |
Get SMTP settings for invites/notifications. |
useSecuritySettings() |
Security config | GET /api/admin/security-settings |
18 security settings: rate limits, lockout policy, file tunnel, agent limits. |
useModManagerSettings() |
Mod manager config | GET /api/admin/mod-manager |
CurseForge + Modrinth API key settings. |
useAuthLockouts() |
Lockout tracking | GET /api/admin/auth-lockouts |
Failed login tracking with search/pagination. |
useThemeSettings() |
Theme configuration | GET /api/admin/theme-settings |
Full admin theme settings. |
useOidcConfig() |
OIDC configuration | GET /api/admin/oidc |
OpenID Connect / SSO settings. |
useSystemErrors() |
System errors | GET /api/admin/system-errors |
Client-side error reporting dashboard. |
useResolveSystemError() |
Resolve error | POST /api/admin/system-errors/:id/resolve |
Mark a system error as resolved. |
| Hook | Purpose | API Endpoint | Details |
|---|---|---|---|
useAuthInit() |
Auth initialization | POST /api/auth/refresh |
Silently handles 401s. Used by App.tsx on mount. |
useProfile() |
Profile CRUD | GET/PUT /api/profile |
Personal info, avatar, password, sessions, 2FA, passkeys, API keys. |
useProfileApiKeys() |
API key management | GET/POST/PUT/DELETE /api/profile/api-keys |
Create, list, enable/disable, delete, rename API keys. |
useProfileSsoAccounts() |
SSO account linking | GET /api/profile/sso-accounts |
Link/unlink WHMCS/Paymenter accounts. |
useProfileSync() |
Profile sync | GET/PUT /api/profile |
Avatar upload and profile synchronization. |
useEulaPrompt() |
EULA acceptance | POST /api/servers/:id/eula |
Shows EULA prompts for Minecraft and similar games. |
| Hook | Purpose | API Endpoint | Details |
|---|---|---|---|
useServers() |
Server listing | GET /api/servers |
Paginated server list with metrics. Transitional states refetch every 2s. |
useServerStateUpdates() |
State changes | GET /api/servers/:id/events |
Real-time server state changes via SSE. |
useServerMetrics() |
Server metrics | GET /api/servers/:id/metrics |
Per-server CPU/memory/disk metrics. |
useServerMetricsHistory() |
Historical metrics | GET /api/servers/:id/metrics/history |
Historical metrics with time range selection. |
useClusterMetrics() |
Cluster metrics | GET /api/admin/metrics |
Cluster-wide metrics aggregation. |
useTasks() |
Scheduled tasks | GET/POST/PUT/DELETE /api/tasks/:id/tasks |
Server task CRUD with schedule management. |
useNodes() |
Node listing | GET /api/nodes |
Node listing with search. |
useTemplates() |
Template management | GET/POST/PUT/DELETE /api/templates |
Template CRUD and detail retrieval. |
useUpdateCheck() |
Update checking | GET /api/admin/update-check |
Auto-update version checking. |
| Hook | Purpose | API Endpoint | Details |
|---|---|---|---|
useFileManager() |
File operations | GET/PUT/POST /api/servers/:id/files |
Path navigation, file open/edit/save with dirty tracking. Shows file path, name, line count. |
useSseConsole() |
SSE console | GET /api/servers/:id/console/stream |
Batched 32ms flush intervals, pre-allocated IDs, streaming, polling fallback on disconnect. |
useConsole() |
Console commands | POST /api/servers/:id/console/command |
Thin wrapper around useSseConsole. Sends commands (max 4096 chars, auto-appends \n). |
| Hook | Purpose | API Endpoint | Details |
|---|---|---|---|
useSseAdminEvents() |
Admin events | GET /api/events |
Admin-level real-time notifications (notification bell). |
useSseResizeComplete() |
Resize complete | GET /api/servers/:id/events |
Server resize completion events. |
| Hook | Purpose | Details |
|---|---|---|
useKeyboardShortcut() |
Keyboard shortcuts | Global shortcuts (e.g., Ctrl+K for command palette). |
useDebounce() |
Debounce utility | Generic debounce for search inputs across pages. |
useResourceBalancer() |
Resource balancing | Suggests optimal nodes for server distribution (transfer suggestions). |
The Catalyst Agent is a Rust daemon that runs on game server nodes. It manages container lifecycles via containerd gRPC.
cd catalyst-agent
# Install dependencies (via cargo)
cargo fetch
# Build debug binary (for development, slower runtime)
cargo build
# Build release binary (optimized, for production)
cargo build --release
# Run on a node with containerd
sudo cargo run -- ./config.toml| Command | Description |
|---|---|
cargo build |
Debug build (faster compile, slower runtime) |
cargo build --release |
Optimized release build (~50MB binary) |
cargo test |
Run Rust unit tests |
cargo clippy |
Run Rust linter (fails on warnings in CI) |
cargo fmt |
Format code |
cargo fmt -- --check |
Check formatting (used in CI) |
catalyst-agent/src/
├── main.rs # Entry point, Tokio runtime, signal handling
├── websocket_handler.rs # WebSocket connection to backend
├── runtime_manager.rs # Containerd lifecycle management
├── file_manager.rs # File operations on node filesystem
└── config.rs # TOML configuration parsing
Containerd Communication — The agent connects to /run/containerd/containerd.sock (configurable in config.toml). All operations are async via Tokio:
// Container creation
containerd_client.create_container(&namespace, &config).await?;
// Health checks
containerd_client.get_container_status(container_id).await?;WebSocket Protocol — Messages use snake_case types:
// Agent → Backend
{ "type": "server_state_update", "serverId": "...", "status": "running" }
// Backend → Agent
{ "type": "start_server", "serverId": "...", "serverUuid": "..." }Catalyst supports extensible plugins with backend API routes, WebSocket handlers, scheduled tasks, and frontend React components.
# Scaffold a new plugin (from the monorepo root)
cd packages/plugin-sdk
npx @catalyst/plugin-sdk create my-plugin --template fullstack| Template | Description | Use Case |
|---|---|---|
backend-only |
API routes + WebSocket handlers only | Backend integrations |
fullstack |
Backend + frontend tabs | Interactive plugins |
minimal |
Single manifest + entry point | Simple functionality |
// plugin.json — manifest
{
"name": "my-plugin",
"version": "1.0.0",
"displayName": "My Plugin",
"config": { /* plugin config fields */ }
}
// backend/index.js — lifecycle hooks
export default {
async onLoad(context) {
// Register routes (before server starts)
const routes = defineRoutes((router) => {
router.get('/hello', async (req, reply) => { ... });
});
for (const route of routes) {
context.registerRoute(route);
}
},
async onEnable(context) {
// Register WebSocket handlers, cron jobs
},
async onDisable(context) {
// Cleanup, stop tasks
},
async onUnload(context) {
// Final cleanup
}
};| Method | Purpose |
|---|---|
context.registerRoute(route) |
Register an API route |
context.onWebSocketMessage(type, handler) |
Listen for WebSocket messages |
context.scheduleTask(cron, handler) |
Register a cron task |
context.on(event, handler) / context.emit(event, data) |
Event system |
context.getConfig(key) / context.setConfig(key, value) |
Plugin config |
context.getStorage(key) / context.setStorage(key, value) |
Persistent key-value storage |
context.collection(name) |
Typed MongoDB-like collection API |
The SDK provides test utilities:
import { createTestPlugin } from '@catalyst/plugin-sdk/testing';
const harness = createTestPlugin(myPlugin, manifest, config);
const context = await harness.load();
// Assert routes were registered
assert(context.registerRoute.calls.length > 0);Set PLUGIN_HOT_RELOAD=true in .env to auto-reload plugins on file changes (development only).
See catalyst-plugins/example-plugin/ for a full demonstration including:
- Custom API routes (
/api/plugins/example-plugin/hello) - WebSocket message handling
- Scheduled cron tasks
- Frontend admin + server tabs
- Persistent storage
- Configuration via admin UI
Catalyst uses a multi-layer testing strategy.
# Run all backend tests
pnpm run test # or: cd catalyst-backend && pnpm run test
# Run with UI (interactive watch mode)
pnpm run test:ui
# Generate coverage report
pnpm run test:coverageTest files live alongside source in src/**/__tests__/**/*.test.ts.
# Run unit tests
pnpm run test # or: cd catalyst-frontend && pnpm run testTest files follow the pattern src/**/__tests__/**/*.{test,spec}.{ts,tsx}.
# Run all E2E tests (headless)
pnpm run test:e2e
# Screenshot crawl — captures every page, tab, and modal
pnpm run test:screenshots
# Watch the browser live
pnpm run test:screenshots:headed
# Against an already-running dev server
SKIP_WEB_SERVER=1 pnpm run test:screenshotsThe screenshot crawl dynamically discovers routes from src/App.tsx, navigation links from the sidebar, and modals from DOM triggers — requiring zero test maintenance when adding pages.
Bash test suites in tests/ verify API workflows end-to-end:
# Run all integration tests
cd tests && ./run-all-tests.sh
# Run a single test
cd tests && ./01-auth.test.sh
# Quick smoke test
cd tests && ./test-backend.shTests include auth flow, template management, node registration, server state transitions, RBAC validation, WebSocket communication, file operations, and full workflows.
Key integration test flows:
| Test File | Covers |
|---|---|
01-auth.test.sh |
Registration, login, 2FA, passkey, session management |
02-templates.test.sh |
Template CRUD, variable validation, egg import |
03-nodes.test.sh |
Node registration, allocation, health checks |
04-servers.test.sh |
Server lifecycle: create → start → stop → transfer → delete |
05-rbac.test.sh |
Role-based access control, permission enforcement |
06-websocket.test.sh |
WebSocket connection, console streaming, SSE fallback |
07-files.test.sh |
File tunnel operations, upload/download, archive/extract |
smoke-test.sh |
Quick health check — starts all services and verifies /health endpoint |
Testing patterns for auth/security features:
# Test rate limiting — send multiple requests and verify 429 responses
curl -s -w '%{http_code}' http://localhost:3000/api/auth/login -d '{"email":"test@test.com","password":"test"}'
# Test password complexity — registration should reject weak passwords
curl -s http://localhost:3000/api/auth/register -d '{"email":"test@test.com","password":"weak","username":"test"}'
# Test passkey registration flow — requires HTTPS and WebAuthn-capable browserBackend unit test patterns:
// Prisma test client isolates tests from the real database
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
beforeEach(async () => {
await prisma.$transaction([
prisma.user.deleteMany(),
prisma.server.deleteMany(),
prisma.node.deleteMany(),
]);
});
afterEach(async () => {
await prisma.$disconnect();
});Frontend test patterns:
// Render with auth context
import { render, screen } from '@testing-library/react';
import { authStore } from '@/stores/authStore';
render(<Route path="/servers" element={<ServerList />} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
),
});
// Mock API services
vi.mock('@/services/api/servers', () => ({
getServers: () => Promise.resolve(mockServers),
}));# From monorepo root — runs tests in all packages
pnpm run testESLint Configuration
Backend and frontend share similar TypeScript ESLint rules but with different targets:
| Rule | Severity | Notes |
|---|---|---|
eqeqeq |
error | Require === over == |
no-debugger |
error | Prevent debugger statements |
@typescript-eslint/no-explicit-any |
off | any is acceptable |
@typescript-eslint/no-unused-vars |
off | Unused vars allowed |
@typescript-eslint/no-non-null-assertion |
warn | Prefer optional chaining |
no-console |
warn | Only warn/error allowed |
Prettier Configuration (Frontend)
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
}Auto-fix all linting:
pnpm run lint -- --fix
pnpm run format # Frontend only| Command | Description |
|---|---|
cargo fmt |
Format code (2-space indent, standard style) |
cargo clippy -- -D warnings |
Fail on all clippy warnings |
Catalyst uses PostgreSQL with Prisma ORM for the database layer.
The Prisma schema lives in catalyst-backend/prisma/schema.prisma. Key entities include User, Server, Node, Template, Role, Permission, Backup, Task, Alert, and Plugin.
# 1. Edit the schema
# catalyst-backend/prisma/schema.prisma
# 2. Generate migration
pnpm run db:migrate
# 3. Apply schema changes to database
pnpm run db:push
# 4. Regenerate Prisma client (usually automatic)
pnpm run db:generate
# 5. Inspect database visually
pnpm run db:studio# Full seed (admin user + sample servers, nodes, templates)
pnpm run db:seed
# Admin user only
pnpm run db:seed:adminDefault seeded credentials: admin@example.com / admin123
main— Stable release branchdevelop— Integration branch for features
PRs targeting either branch trigger CI checks.
Each PR runs automated checks:
| Job | Scope | Checks |
|---|---|---|
| Backend CI | catalyst-backend/** |
Lint, build, Prisma generate, security audit (pnpm audit) |
| Agent CI | catalyst-agent/** |
cargo fmt --check, cargo check, cargo clippy -- -D warnings, unit tests |
| Docker Publish | Changes in backend or frontend | Conditional Docker image build & push to GHCR |
Before submitting a PR:
- Run local tests —
pnpm run test(all packages) - Run linters —
pnpm run lint -- --fix(backend + frontend) - Format agent code —
cargo fmt(if touching the agent) - Run E2E tests —
pnpm run test:e2e(if touching UI or API) - Self-review — Check your diff for lint warnings, dead code, TODOs
- Link related issues — Reference issue numbers in the PR description
- Update tests — Add or update tests for new functionality
Use conventional commit style:
feat: add server export functionality
fix: resolve WebSocket reconnection race condition
docs: update API reference for new endpoints
refactor: simplify state machine transitions
test: add E2E test for plugin management
chore: update dependencies
- Architecture Overview — System design, data flow, and component responsibilities
- API Reference — Complete REST API endpoint reference for integration
- Plugin System Guide — Internal plugin system internals (deep dive)
- Automation Guide — API integration examples (WHMCS, Python, Node.js)
# See runtime logs (console output from Fastify)
pnpm run dev
# Inspect database state
pnpm run db:studio
# Check for SQL query issues
# Set LOG_LEVEL=debug in .env for verbose Pino logs# Check network requests in browser DevTools
# WebSocket connection shown in Network → WS tab
# React DevTools for component tree inspection# Agent uses structured logging via tracing crate
# Set RUST_LOG=debug for verbose output
sudo cargo run -- ./config.toml 2>&1 | grep -i error# Run everything with debug logging
cd catalyst-docker
docker compose up -d # Start infra
docker compose logs -f # Tail all logs
docker compose logs -f backend # Tail backend only
docker compose logs -f frontend # Tail frontend only- Never trust agent state — Always validate server state in the backend before persisting or sending commands.
- Path validation is critical — File paths must be validated on the backend before being sent to the agent. Even if the frontend validates, re-validate on the server.
- WebSocket messages use
snake_case— All message types are lowercase with underscores (e.g.,server_state_update, notserverStateUpdate).
- Transitional states auto-refetch — Servers in
installing,starting,stopping, ortransferringstates are polled every 1 second via Catalyst Sync. Don't override this behavior. - Plugin frontend components share the bundle — Plugin frontend code is compiled with the main app. Ensure plugin components don't leak state.
- Modals must be dismissed properly — Use Escape key or explicit close buttons. Don't rely on route changes to close dialogs.
- Always check containerd socket permissions — If the agent can't connect, check:
ls -l /run/containerd/ - HTTP/HTTPS URLs in URLs in URLS are HTTPS — Ensure
BACKEND_URLuseshttps://in production, or WebSocket connections will fail. - Agent heartbeats are critical — If an agent doesn't send heartbeats for ~5 minutes, it's considered disconnected.
- Don't commit
.envfiles — Use.env.examplefor documenting required variables. - Never trust data from the agent — Validate everything in the backend.
- Database is the source of truth — Persist state changes to the database immediately before sending any async commands.