A self-hostable cloud hosting management platform built as a TypeScript monorepo.
openclaw.anywhere/
├── apps/
│ ├── api/ # Hono.js backend API (Node.js)
│ └── web/ # React + Vite frontend
├── packages/
│ ├── shared/ # @openclaw/shared - HTTP client, constants, validation
│ └── i18n/ # @openclaw/i18n - Internationalization (EN, FR, ES, DE)
├── scripts/ # Utility scripts
└── turbo.json # Turborepo build orchestration
- Framework: Hono.js
- Database: Neon (serverless PostgreSQL) with Drizzle ORM
- Authentication: Firebase Admin SDK + OTP (email-based) + OAuth (Google, GitHub)
- Email: Resend + React Email
- Payments: Polar SDK
- Cloud Provider: Hetzner Cloud
- DNS: Cloudflare
- SSH/Terminal: SSH2 + Bun native WebSocket for remote terminal access
- Runtime: Bun
- Framework: React 18 with Vite
- Routing: React Router DOM
- State: Zustand (with persist middleware)
- Data Fetching: TanStack React Query
- UI: shadcn/ui + Radix UI + Tailwind CSS
- Icons: Phosphor Icons
- Animation: Framer Motion
- Auth: Firebase
- Graph Visualization: React Flow (playground canvas)
- Terminal: xterm.js (in-browser terminal emulation)
- Code Editing: CodeMirror (file editor)
- Blog: MDX for content authoring
- @openclaw/shared: HTTP RequestClient, status constants, input validation, user roles, API envelope types
- @openclaw/i18n: Internationalization framework with
t()function, parameter interpolation, 4 languages
CRITICAL: Always use @/ path aliases in both the web app and the API app. Never use ../ relative imports under any circumstance. This applies to ALL .ts and .tsx files — including files in scripts/, entry points, and any other directory outside src/.
// CORRECT - Use @ path aliases
import { Button } from '@/components/ui/button'
import { api } from '@/lib/api'
import { db } from '@/db'
import { hetzner } from '@/services/hetzner'
// CORRECT - Script files also use @/ aliases
// apps/api/scripts/example.ts
import { hetzner } from '@/services/hetzner'
// INCORRECT - Never use ../ relative imports
import { Button } from '../components/ui/button' // DO NOT USE
import { db } from '../../db' // DO NOT USE
import { hetzner } from '../src/services/hetzner' // DO NOT USE (even in scripts/)./ imports are NEVER allowed. All imports must use @/ path aliases — including barrel exports, same-directory siblings, and subdirectory imports.
For workspace packages, use the package name:
import { RequestClient } from '@openclaw/shared'
import { t } from '@openclaw/i18n'CRITICAL: Always import Phosphor icons with the Icon suffix from @phosphor-icons/react. The non-suffixed names (e.g., Check, Copy, Eye) are deprecated. Every icon import must end with Icon.
// CORRECT - Always use the Icon suffix
import {
CheckIcon,
CopyIcon,
EyeIcon,
EyeSlashIcon
} from '@phosphor-icons/react'
import {
CircleNotchIcon,
WarningIcon,
LightningIcon
} from '@phosphor-icons/react'
import {
GithubLogoIcon,
DiscordLogoIcon,
SlackLogoIcon
} from '@phosphor-icons/react'
// INCORRECT - Non-suffixed names are deprecated
import { Check, Copy, Eye, EyeSlash } from '@phosphor-icons/react' // DO NOT USE
import { CircleNotch, Warning, Lightning } from '@phosphor-icons/react' // DO NOT USE
import { GithubLogo, DiscordLogo, SlackLogo } from '@phosphor-icons/react' // DO NOT USEIn JSX, use the suffixed names:
// CORRECT
<CheckIcon size={16} />
<CircleNotchIcon className='animate-spin' />
// INCORRECT
<Check size={16} /> // DO NOT USE
<CircleNotch className='animate-spin' /> // DO NOT USECRITICAL: Every .ts and .tsx file must export exactly ONE function, component, constant, or class via export default. No file should have multiple exports.
When a module needs multiple exports, convert it to a folder:
lib/example.ts (BEFORE - multiple exports)
↓
lib/example/ (AFTER - one per file)
doThing.ts → export default doThing
doOtherThing.ts → export default doOtherThing
index.ts → barrel re-exports
Barrel index.ts syntax:
import doThing from '@/lib/example/doThing'
import doOtherThing from '@/lib/example/doOtherThing'
export { doThing, doOtherThing }NEVER use export { default as X } from syntax in barrel files. Always import the default first, then re-export by name.
Private/internal modules (shared state, config) within a folder don't need to be in the barrel.
Exempt from this rule:
ts/Types.tsandts/Interfaces.ts— type centralization filests/index.ts— type barrel- Barrel
index.tsfiles — they are the aggregation mechanism - shadcn/ui components in
components/ui/— third-party generated
Reference pattern: See apps/api/src/controllers/agents/ for the canonical example.
CRITICAL: All types and interfaces must be centralized in @/ts/. This applies to BOTH apps/web AND apps/api. Never define types, interfaces, or inline object types anywhere else — not in components, hooks, services, controllers, lib files, or scripts.
This includes:
interfacedefinitionstypealias definitions- Inline object types in function parameters (e.g.,
(data: { name: string })) - Inline object types in return types (e.g.,
Promise<{ id: string }>) - Inline union types used as standalone types
- Props types for React components
Centralized Type Files:
| App | Interfaces | Types | Barrel |
|---|---|---|---|
| Web | apps/web/src/ts/Interfaces.ts |
apps/web/src/ts/Types.ts |
apps/web/src/ts/index.ts |
| API | apps/api/src/ts/Interfaces.ts |
apps/api/src/ts/Types.ts |
apps/api/src/ts/index.ts |
Type Imports Must Be at the Top of Files, Separated by an Empty Line:
// CORRECT - Type imports first, then empty line, then regular imports
import type { Claw, Plan, SSHKey, StatusConfig } from '@/ts/Interfaces'
import type { ViewMode, ToastType } from '@/ts/Types'
import { useState } from 'react'
import { api } from '@/lib/api'
// INCORRECT - Missing empty line between type and regular imports
import type { Claw } from '@/ts/Interfaces' // DO NOT USE
import { useState } from 'react' // (no blank line above)
// INCORRECT - Type imports after regular imports
import { useState } from 'react' // DO NOT USE
import type { Claw } from '@/ts/Interfaces' // (type import must be above)
// INCORRECT - Regular imports for types
import { Claw, Plan } from '@/ts/Interfaces' // DO NOT USE
// INCORRECT - Inline type definitions
interface MyComponentProps {
// DO NOT USE - put in @/ts/Interfaces.ts
title: string
}
// INCORRECT - Inline object types in functions
async function getUser(): Promise<{ id: string; name: string }> {} // DO NOT USE
function create(data: { email: string; name?: string }): void {} // DO NOT USE
// CORRECT - Use named interfaces from @/ts/Interfaces
async function getUser(): Promise<UserProfile> {} // USE THIS
function create(data: CreateUserParams): void {} // USE THISFile Organization:
@/ts/Types.ts- All type aliases (e.g.,type ViewMode = 'list' | 'grid')@/ts/Interfaces.ts- All interfaces (e.g.,interface Claw { ... })@/ts/index.ts- Barrel export for convenient imports
Categories in web Interfaces.ts:
- API / Data Models:
Claw,Plan,Location,SSHKey,Volume,UserProfile,UserStats,BillingOrder, etc. - Store Interfaces:
UIState,PreferencesState,ToastData - Auth Interfaces:
AuthContextType,VerifyOtpResponse,CachedProfile,ResolveCredentialConflictData - Component Props:
HeaderProps,EmptyStateProps,ClawCardProps,PlaygroundCanvasProps,PlaygroundDetailPanelProps, etc. - Hook Data Types:
CreateClawData,PurchaseClawData,CreateSSHKeyData,RenameClawData,UpdateClawSubdomainData, etc. - Playground Types:
PlaygroundClawNodeData - File System Types:
ClawFileEntry,ClawFilesResponse,ReadClawFileResponse,UpdateClawFileData - Version Types:
ClawVersionResponse,ClawVersionsResponse - Blog Types:
BlogPostFrontmatter,BlogPostMeta,Testimonial,Faq,CompareCompetitor,CompareFeature
Categories in api Interfaces.ts:
- API Response:
ApiResponse<T> - Cloud Provider Interface:
CloudProvider,CreateServerResult,ServerStatus,ServerTypeInfo,LocationInfo - Hetzner Types:
HetznerServer,HetznerServerType,HetznerLocation,HetznerDatacenter,HetznerVolume,HetznerSSHKey, etc. - Polar/Payment Types:
CheckoutSession,PolarSubscription,PolarOrder,PolarProduct,PolarCustomer, webhook data types - Claw Operation Types:
CreateClawBody,InitiateClawPurchase,ProvisionClawParams - Auth Types:
SendOtpBody,VerifyOtpBody,ResolveCredentialConflictBody - File Types:
ClawFileEntry,ReadClawFileBody,UpdateClawFileBody - Diagnostics Types:
DiagnosticsStatusResponse,DiagnosticsLogsResponse - Cache Types:
CacheEntry<T> - DNS Types:
CloudflareDNSRecord - Email Props:
OtpCodeEmailProps
CRITICAL: All React functional components must use the const ComponentName: FC = (): ReactNode => { ... } pattern with a separate export at the end.
For components without props:
import type { FC, ReactNode } from 'react'
const MyComponent: FC = (): ReactNode => {
return <div>Content</div>
}
export default MyComponent
// or for named exports: export { MyComponent }For components with props:
import type { FC, ReactNode } from 'react'
import type { MyComponentProps } from '@/ts/Interfaces'
const MyComponent: FC<MyComponentProps> = ({ title, description }): ReactNode => {
return (
<div>
<h1>{title}</h1>
<p>{description}</p>
</div>
)
}
export default MyComponentINCORRECT patterns - Never use:
// DO NOT USE - function declaration with inline export
export default function MyComponent() { ... }
// DO NOT USE - function declaration without FC type
function MyComponent() { ... }
// DO NOT USE - arrow function without FC type
const MyComponent = () => { ... }Key rules:
- Always import
FCandReactNodefrom 'react' usingimport type - Use
FCfor components without props,FC<PropsType>for components with props - Always include
: ReactNodeas the return type annotation - Use
export default ComponentNameat the end of the file for default exports - Use
export { ComponentName }for named exports - Internal/helper components within a file should also follow this pattern
CRITICAL: Never use the shorthand <>...</> fragment syntax. Always use the explicit <Fragment>...</Fragment> from React. This ensures fragments can always accept a key prop when needed and keeps the codebase consistent.
// CORRECT - Always use explicit Fragment
import { Fragment } from 'react'
<Fragment>
<ChildA />
<ChildB />
</Fragment>
// CORRECT - Fragment with key in .map()
{items.map((item) => (
<Fragment key={item.id}>
<ChildA />
<ChildB />
</Fragment>
))}
// INCORRECT - Never use shorthand fragment syntax
<>
<ChildA />
<ChildB />
</>CRITICAL: Never use inline magic strings, numbers, or object literals when the value represents a domain concept. Extract every such value to a named constant. The constant must be as const so TypeScript infers literal types, never string or number.
This includes:
- Status / state strings (
'online','pending','creating','canceled') - HTTP methods, headers, status codes
- Provider names, agent types, plan IDs
- URLs, hosts, ports, paths (use
PATHS/ROUTES/externalUrls/apiPaths) - Validation limits (use
inputValidationfrom@openclaw/shared) - Timeouts, intervals, thresholds, retry counts when reused
- Any literal compared with
===in more than one place
// CORRECT
import { networkStatus } from '@openclaw/shared'
if (result === networkStatus.OFFLINE) setIsOffline(true)
// CORRECT - single-use threshold scoped to module
const LATENCY_THRESHOLD_MS = 3000
if (latency > LATENCY_THRESHOLD_MS) return networkStatus.UNSTABLE
// INCORRECT - inline magic string
if (result === 'offline') setIsOffline(true) // DO NOT USE
// INCORRECT - inline magic number reused across codebase
if (latency > 3000) return 'unstable' // DO NOT USEWhere constants live:
| Scope | Location | Pattern |
|---|---|---|
| Used in 2+ apps | packages/shared/src/<name>.ts |
named export, re-exported from packages/shared/src/index.ts |
| Single app, multiple files | apps/<app>/src/lib/constants/<name>.ts |
default export per CLAUDE.md export rule |
| Single file | top of that file, SCREAMING_SNAKE_CASE for primitives, camelCase for objects |
not exported |
Shared package export & import convention:
Files in packages/shared/src/ use named exports for constants and are imported destructured in packages/shared/src/index.ts. The barrel re-exports the same name without aliasing where possible.
// packages/shared/src/networkStatus.ts
const networkStatus = {
ONLINE: 'online',
UNSTABLE: 'unstable',
OFFLINE: 'offline'
} as const
export { networkStatus }
// packages/shared/src/index.ts
import { networkStatus } from '#shared/networkStatus'
import { httpMethod } from '#shared/httpMethod'
export {
networkStatus,
httpMethod
// ...
}
// Consumer (any app)
import { networkStatus } from '@openclaw/shared'Never import a shared-package constant via default import in the barrel:
// INCORRECT
import NETWORK_STATUS from '#shared/networkStatus' // DO NOT USE
import HTTP_METHOD from '#shared/httpMethod' // DO NOT USEThis is the existing pattern for plans (PLANS, YEARLY_PAID_MONTHS) and supportedVersions (isFeatureSupported, isVersionSupported, SUPPORTED_VERSIONS). All new shared constants follow it.
API Controllers (apps/api/src/controllers/):
- Group by resource (ai, auth, claws, plans, ssh-keys, users, webhooks)
- Each controller exports individual functions
- Use barrel exports in index.ts
- Claws controller has
helpers/subdirectory for shared utilities
API Routes (apps/api/src/routes/):
- One file per resource
- Import controllers and wire to Hono routes
- Combine in routes/index.ts
Web Pages (apps/web/src/pages/):
- One component per page
- Use PageTitle for document title management
- Include PageBackground for consistent styling
- Public pages: Landing, Login, Blog, BlogPost, Compare, Terms, Privacy, Changelog, NotFound
- Protected pages: Dashboard (with Playground tab), Account, SSHKeys, Billing
Web Components (apps/web/src/components/):
ui/for shadcn/ui primitivesdashboard/for dashboard-specific components (CreateClawModal, ClawCard dropdowns/dialogs, diagnostics, logs, terminal, config, file explorer)playground/for graph visualization (PlaygroundCanvas, ClawNode, DetailPanel, Toolbar, VersionsContent)- Root level for shared components (Header, Footer, Logo, EmptyState, Toast, ProtectedRoute, etc.)
- Keep components focused and composable
Schema Location: apps/api/src/db/schema.ts
Tables:
users- Firebase authenticated users (with authMethods array, polarCustomerId, role)claws- Cloud server instances (Hetzner) with gateway tokens, Polar subscription tracking, deletion schedulingpendingClaws- Claws awaiting payment confirmation (with expiry)sshKeys- SSH key management (with Hetzner key IDs)volumes- Persistent storage volumesotpCodes- OTP authentication codes (hashed, with attempt tracking)rateLimits- Rate limiting for auth endpoints
Migrations: Use Drizzle Kit
bun --filter api db:generate # Generate migration
bun --filter api db:migrate # Run migrationsAuthentication: Bearer token middleware validates Firebase tokens. Auto-creates/updates user record on first auth. Admin-only routes protected by adminOnly middleware.
Unauthenticated endpoints (no token required):
GET /- Health check
Auth Routes (/auth):
POST /send-otp- Send OTP code via email (rate-limited)POST /verify-otp- Verify OTP and get Firebase tokenPOST /resolve-credential-conflict- Handle auth method conflicts
Plans Routes (/plans) - No auth required:
GET /- Available plans by providerGET /locations- Deployment locationsGET /volume-pricing- Storage pricingGET /availability- Plan availability by location
Claws Routes (/agents):
GET /- List user's clawsGET /admin- List all claws (admin-only)GET /:id- Get single clawPOST /- Create free clawPOST /purchase- Initiate paid claw purchaseDELETE /pending/:id- Cancel pending purchasePATCH /:id- Rename clawDELETE /:id- Soft delete (schedules deletion)POST /:id/sync- Sync status with cloud providerPOST /:id/start- Start serverPOST /:id/stop- Stop serverPOST /:id/restart- Restart serverPOST /:id/cancel-deletion- Cancel scheduled deletionPOST /:id/hard-delete- Permanent delete (admin-only)POST /:id/diagnostics/status- Service diagnosticsPOST /:id/diagnostics/logs- Retrieve logsPOST /:id/diagnostics/repair- Attempt repair (admin-only)POST /:id/reinstall- Reinstall OpenClaw (rate-limited to once per 24h for non-admins)GET /:id/export- Export claw configurationPOST /:id/files- List filesPOST /:id/files/read- Read file contentPUT /:id/files- Update file contentPOST /:id/version- Get current versionPOST /:id/versions- List available versionsPOST /:id/install-version- Install version (admin-only)POST /:id/credentials- Get credentials- WebSocket:
/:id/terminal- Real-time terminal access
SSH Keys Routes (/ssh-keys):
GET /- List SSH keysPOST /- Create SSH keyDELETE /:id- Delete SSH key
Users Routes (/users):
GET /me- Get profilePUT /me- Update profileGET /me/stats- User statisticsGET /me/billing- Billing historyGET /me/billing/:orderId/invoice- Download invoicePOST /me/billing/portal- Polar customer portal linkPOST /me/auth/:method- Connect auth method (Google, GitHub)DELETE /me/auth/:method- Disconnect auth method
Webhooks Routes (/webhooks):
POST /polar- Polar payment webhook (handles checkout, subscription lifecycle)
All API responses follow a consistent envelope format produced by the ok and fail helpers in apps/api/src/lib/response/. Controllers must return responses through these helpers — never c.json directly.
Format:
{
"success": true,
"data": null,
"message": "Human-readable message.",
"code": 200,
"version": "0.0.31"
}| Field | Type | Description |
|---|---|---|
success |
boolean |
Whether the request succeeded |
data |
T | null |
Response payload (null for side-effect-only) |
message |
string |
Translated human-readable message |
code |
number |
HTTP status code |
version |
string |
API version from package.json (auto-bumped) |
Helpers:
ok(c, data, message?, code?) — returns a success envelope.
import { ok } from '@/lib/response'
import { t } from '@openclaw/i18n'
return ok(c, claws, t('api.clawsFetched'))
return ok(c, null, t('api.clawDeleted'))
return ok(c, { scheduled: true }, t('api.clawDeletionScheduled'))fail(c, message, code?, data?) — returns an error envelope. The optional data parameter passes extra context (e.g., retryAfter for rate limits).
import { fail } from '@/lib/response'
import { t } from '@openclaw/i18n'
return fail(c, t('api.clawNotFound'), 404)
return fail(c, t('api.rateLimitExceeded'), 429, { retryAfter: 60 })Client handling: the shared RequestClient in packages/shared/ detects envelopes via duck-typing (all 5 fields present). On success: true it unwraps and returns data as T. On success: false it throws Error with the message. Client code stays clean:
const claws = await api.getClaws()claws is typed as Claw[] — the envelope is transparent.
Examples:
Success with data — GET /agents → 200
{
"success": true,
"data": [{ "id": "abc", "name": "my-claw" }],
"message": "Claws fetched successfully.",
"code": 200,
"version": "0.0.31"
}Success without data — POST /auth/send-otp → 200
{
"success": true,
"data": null,
"message": "Code sent successfully.",
"code": 200,
"version": "0.0.31"
}Error — GET /agents/nonexistent → 404
{
"success": false,
"data": null,
"message": "Claw not found.",
"code": 404,
"version": "0.0.31"
}Error with extra data — POST /auth/send-otp → 429
{
"success": false,
"data": { "retryAfter": 45 },
"message": "Too many requests. Please try again later.",
"code": 429,
"version": "0.0.31"
}The Hetzner service implements the CloudProvider interface:
createServer,getServer,getServers,startServer,stopServer,restartServer,deleteServercreateSSHKey,deleteSSHKeygetServerTypes,getLocations,getDatacenterscreateVolume,attachVolume,detachVolume,deleteVolume,getVolume
The provider resolver includes in-memory caching with TTL (5 min for server types/locations/pricing, 10 sec for individual servers).
- Firebase: Enable Authentication with Email/Password, Google, and GitHub sign-in methods. Generate a service account key for the Admin SDK
- Cloudflare: API token needs DNS edit permissions for the zone. Creates A records for each claw subdomain (60s TTL)
- Resend: Verify your sending domain.
FROM_EMAILdefaults toOpenClaw <noreply@openclaw.com> - Polar: Create an organization, generate an access token, and configure a webhook pointing to
POST /api/webhooks/polarwith the secret
Zustand Stores (apps/web/src/lib/store/):
useUIStore- Toast notifications, create modal state, ProductHunt bannerusePreferencesStore- User preferences (persisted): theme, language, admin mode
- Files: kebab-case for utilities, PascalCase for React components
- Functions: camelCase
- Types/Interfaces: PascalCase
- Constants: SCREAMING_SNAKE_CASE for routes, camelCase otherwise
- Database columns: camelCase (Drizzle handles snake_case conversion)
- Bun: >= 1.0
bun install # Install all dependencies
bun --filter api db:migrate # Run database migrations# Development
bun dev # Run all apps
bun dev:web # Run web only (port 1111)
bun dev:api # Run API only (port 2222)
# Building
bun build # Build all apps
# Database
bun --filter api db:generate
bun --filter api db:migrate
bun --filter api db:studio
# Linting & Formatting
bun check # tsc + eslint for all apps
bun lint # ESLint check
bun format # Prettier + ESLint auto-fix- Web: 1111 (proxies /api to 2222)
- API: 2222
API (apps/api/.env):
PORT=2222
CLIENT=localhost:1111
DATABASE_URL=postgresql://...
# Firebase Admin SDK
FIREBASE_PROJECT_ID=...
FIREBASE_PRIVATE_KEY=...
FIREBASE_CLIENT_EMAIL=...
# Cloud Provider
HETZNER_API_TOKEN=...
# Cloudflare DNS
CLOUDFLARE_API_TOKEN=...
CLOUDFLARE_ZONE_ID=...
# Resend (email)
RESEND_API_KEY=...
FROM_EMAIL=OpenClaw <noreply@yourdomain.com>
# Encryption (AES-256-GCM for secrets at rest)
ENCRYPTION_KEY=... # 32-byte hex string (64 chars), generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Polar (payments)
POLAR_ACCESS_TOKEN=...
POLAR_ORGANIZATION_ID=...
POLAR_WEBHOOK_SECRET=...
POLAR_PRODUCT_HETZNER_CX23=...
# ... (one POLAR_PRODUCT_* per plan)
Web (apps/web/.env):
VITE_API_URL=/api
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=...
VITE_FIREBASE_PROJECT_ID=...
VITE_FIREBASE_STORAGE_BUCKET=...
VITE_FIREBASE_MESSAGING_SENDER_ID=...
VITE_FIREBASE_APP_ID=...
| Purpose | Path |
|---|---|
| API Entry | apps/api/src/index.ts |
| API App Setup | apps/api/src/app.ts |
| DB Schema | apps/api/src/db/schema.ts |
| API Routes | apps/api/src/routes/index.ts |
| Admin Middleware | apps/api/src/middleware/adminOnly.ts |
| Provider Resolver | apps/api/src/services/provider/getProvider.ts |
| Hetzner Service | apps/api/src/services/hetzner.ts |
| Cloudflare Service | apps/api/src/services/cloudflare.ts |
| SSH Service | apps/api/src/services/ssh.ts |
| Terminal WebSocket | apps/api/src/services/terminalSocket.ts |
| Polar Services | apps/api/src/services/polar/ |
| Claw Helpers | apps/api/src/controllers/agents/helpers/ |
| Web Entry | apps/web/src/main.tsx |
| Web Routes | apps/web/src/App.tsx |
| Auth Context | apps/web/src/lib/auth/ |
| API Client (Web) | apps/web/src/lib/api.ts |
| URL Paths | apps/web/src/lib/paths.ts |
| Web Routes | apps/web/src/lib/routes.ts |
| Stores | apps/web/src/lib/store/ |
| Gateway Client | apps/web/src/lib/gateway/ |
| Dashboard Tabs | apps/web/src/lib/dashboardTabs.ts |
| Claw Detail Tabs | apps/web/src/lib/clawDetailTabs.ts |
| Blog Utilities | apps/web/src/lib/blog/ |
| Claw Utilities | apps/web/src/lib/claw-utils/ |
| Types (Web) | apps/web/src/ts/Types.ts |
| Interfaces (Web) | apps/web/src/ts/Interfaces.ts |
| Types (API) | apps/api/src/ts/Types.ts |
| Interfaces (API) | apps/api/src/ts/Interfaces.ts |
| Input Validation | packages/shared/src/inputValidation.ts |
CRITICAL: Never use hardcoded text strings in the UI. All user-facing text must use the translation function.
How it works:
- All translations are defined in
packages/i18n/src/langs/en.ts - Import and use the
t()function from@openclaw/i18n - Use dot notation for nested keys (e.g.,
t('dashboard.status.running')) - For dynamic text with parameters, use
t('key', { param: value })
// CORRECT - Use translation function
import { t } from '@openclaw/i18n'
<Button>{t('common.save')}</Button>
<p>{t('dashboard.noClawsDescription')}</p>
<span>{t('common.copiedWithLabel', { label: 'Password' })}</span>
// INCORRECT - Never hardcode text
<Button>Save</Button> // DO NOT USE
<p>Deploy OpenClaw on your first VPS</p> // DO NOT USETranslation keys are organized by category:
common.*- Shared UI text (Save, Cancel, Delete, Loading, etc.)setup.*- Initial setup/onboardinglanguage.*- Language selectiontheme.*- Theme togglenav.*- Navigation itemsfooter.*- Footer contenterrors.*- Error messagesapi.*- API response messagesemails.*- Email templatesauth.*- Authentication pagesaccount.*- Account managementbilling.*- Billing and paymentdashboard.*- Dashboard/Claws pagecreateClaw.*- Create Claw modalsshKeys.*- SSH Keys pagelanding.*- Landing pageblog.*- Blog pageschangelog.*- Release notescomparison.*- Competitor comparisoncompare.*- Full comparison tableprivacy.*- Privacy policyterms.*- Terms of serviceannouncement.*- Service announcementsproductHunt.*- ProductHunt bannerplayground.*- Playground management
When adding new features:
- First add all text strings to
packages/i18n/src/langs/en.ts - Add the same keys with translated values to ALL language files:
fr.ts,es.ts,de.ts - Use descriptive, hierarchical key names
- Then reference them in components using
t('category.keyName')
CRITICAL: Every new translation key MUST be added to ALL four language files (en, fr, es, de) simultaneously. Never add a key to only one language file — this will cause missing translations in other languages.
Date and number formatting must be locale-aware:
// CORRECT - Use getLocale() for locale-sensitive formatting
import { getLocale } from '@/lib'
new Date(dateString).toLocaleDateString(getLocale(), { year: 'numeric', month: 'long', day: 'numeric' })
new Intl.NumberFormat(getLocale(), { style: 'currency', currency: 'USD' }).format(amount)
// INCORRECT - Never hardcode locale strings
new Date(dateString).toLocaleDateString('en-US', { ... }) // DO NOT USE
new Intl.NumberFormat('en-US', { ... }).format(amount) // DO NOT USECRITICAL: All code written or modified must strictly follow these formatting and linting rules. These are enforced by ESLint and Prettier and checked by Husky pre-commit hooks. Never deviate from them.
- Single quotes — Always use single quotes (
'), never double quotes (") - No semicolons — Never end statements with semicolons
- 4-space indentation — Use 4 spaces for all indentation, never tabs, never 2 spaces
- No trailing commas — Never add trailing commas in arrays, objects, function params, or imports
- Single JSX quotes — Use single quotes in JSX attributes (
<div className='foo'>) - Tailwind class sorting — Classes are auto-sorted by
prettier-plugin-tailwindcss
These apply to both api and web — every .js, .mjs, .cjs, .ts, and .tsx file:
- 4-space indentation —
indent: ['error', 4] - Single quotes —
quotes: ['error', 'single'] - No semicolons —
semi: ['error', 'never'] - No trailing commas —
comma-dangle: ['error', 'never'] - Single JSX quotes —
jsx-quotes: ['error', 'prefer-single'] - No multiple empty lines — Max 1 empty line between code, 0 at start of file, 0 at end of file
- No newline at end of file —
eol-last: ['error', 'never'] - No
@ts-ignorerestrictions —@typescript-eslint/ban-ts-commentis off - Linebreak style — Disabled (cross-platform)
TypeScript-specific rules (.ts and .tsx files):
- Warn on unused variables — Except those prefixed with
_ - Warn on
anytype — Prefer explicit types overany - Enforce
import type— Always useimport typefor type-only imports with separate-type-imports style
React-specific rules (web app only):
- React Hooks rules — Enforced (deps arrays, rules of hooks)
- React Refresh — Warns on non-component exports in component files
On every commit, Husky runs:
bun check— Runstsc --noEmitandeslint .for api and webbun version:patch— Auto-bumps patch version inapps/api/package.jsonandapps/web/package.json- Stages the bumped
package.jsonfiles
When writing any code:
// CORRECT
const myFunction = (param: string): string => {
const result = doSomething(param)
return result
}
const myObject = {
key: 'value',
nested: {
foo: 'bar'
}
}
import type { MyType } from '@/ts/Interfaces'
import { useState } from 'react'
// INCORRECT — violates multiple rules
const myFunction = (param: string): string => {
const result = doSomething(param) // 2-space indent + semicolons
return result
}
const myObject = {
key: 'value', // double quotes + 2-space indent
nested: {
foo: 'bar' // trailing comma + double quotes
}
}bun format:check # Check if all files match Prettier rules
bun format # Auto-fix Prettier formatting
bun lint # Check ESLint rules
bun lint:fix # Auto-fix ESLint issues
bun check # Run tsc + eslint for both api and web- Always read files before modifying - Understand existing patterns first
- Use
@/imports everywhere - Always use path aliases in both web and API apps, never use../or./relative imports anywhere - Centralize types in
@/ts/- Never define types/interfaces inline; add to Types.ts or Interfaces.ts - Use
import typefor types - Always useimport typesyntax and place at top of file - Use FC pattern for components - Always use
const ComponentName: FC = (): ReactNode => { ... }withexport default ComponentNameat the end - Follow existing patterns - Match the style of surrounding code
- Keep it simple - Avoid over-engineering or adding unnecessary abstractions
- Controllers handle logic - Routes should be thin wrappers
- Use RequestClient - For API calls, use the shared HTTP client
- Zustand for state - Don't introduce additional state management
- shadcn/ui components - Prefer existing UI components over custom ones
- Use translations for all text - Never hardcode user-facing text; always use
t()from@openclaw/i18n. When adding new translation keys, add them to ALL four language files (en.ts,fr.ts,es.ts,de.ts) simultaneously. UsegetLocale()from@/libfor all date/number formatting — never hardcode'en-US' - Never write comments - Do not add code comments, JSX comments, section markers, or doc comments. The code should be self-explanatory. The only exception is when logic is truly non-obvious (e.g., bitwise operations, crypto algorithms, or workarounds for framework bugs)
- Never add console.log - Do not add
console.logstatements. Useconsole.erroronly for actual error handling in catch blocks. No debug logging, no request logging, no data logging.console.errorformat must be exactlyconsole.error('functionName', error)— first argument is the function name as a plain string (no message, no colon, no description), second argument is the caught error. The catch variable must be namederror, nevererr. For nested catches whereerroris already in scope, use a descriptive suffix likednsError,volumeError,subError - No section markers - Never write comments like
// Section Name,{/* Section */},// ========, or category headers in files - Strict formatting compliance - Every line of code must follow the Prettier and ESLint rules defined above. 4-space indentation, single quotes, no semicolons, no trailing commas, no end-of-file newlines. No exceptions
- Run checks after changes - After writing or modifying code, verify with
bun lintandbun format:checkto ensure compliance - Use camelCase for SVG attributes in JSX - React requires camelCase for SVG/HTML attributes. Use
stopColornotstop-color,stopOpacitynotstop-opacity,fillRulenotfill-rule,clipPathnotclip-path,strokeWidthnotstroke-width, etc. - Full cleanup on feature removal - When removing a feature, delete ALL related code: components, hooks, store properties, interfaces/types, translation keys, utility functions, data files, barrel exports, API routes/controllers, and constants. Never leave orphaned code behind
- Use PATHS for all URL path segments - Never hardcode URL path segments like
'/blog'or'claws'. Always usePATHSfrom@/lib/paths(or@/lib) for path segments andROUTESfrom@/lib/routes(or@/lib) for full route strings. When constructing URLs in scripts, components, or SEO metadata, usePATHS.BLOG,PATHS.LOGIN, etc. To change a URL, update it only inpaths.ts— everything else derives from it - Toast punctuation convention - All toast/notification messages must follow consistent punctuation: success messages end with
.(period) and error messages end with!(exclamation mark). This applies to all four language files. Note: French uses a space before!per French typographic rules (e.g.,claw !notclaw!) - Centralized validation constants - All input validation length limits must be defined in
packages/shared/src/inputValidation.tsas a single source of truth. Never hardcode min/max lengths in controllers, components, or translation strings. ImportinputValidationfrom@openclaw/sharedand reference the constants (e.g.,inputValidation.CLAW_NAME.MAX). Translation error messages must use{{min}}/{{max}}interpolation parameters filled from these constants. When adding new validated fields, add the limits toinputValidation.tsfirst, then use them everywhere - DropdownMenu must be non-modal - The
DropdownMenucomponent defaults tomodal={false}(set incomponents/ui/dropdown-menu.tsx). This prevents the dropdown from blocking page scroll when open. Never override this withmodal={true}unless there is a specific reason. If adding new overlay/popover components from Radix, always setmodal={false}to preserve scroll behavior - Prefer barrel destructuring imports - When a parent folder has an
index.tsbarrel that re-exports a symbol, always import from the barrel using destructuring syntax rather than the direct file path. Useimport { withErrorHandler } from '@/lib'instead ofimport withErrorHandler from '@/lib/withErrorHandler', andimport { useToast } from '@/hooks'instead ofimport useToast from '@/hooks/useToast'. This applies to any folder with a barrel (@/lib,@/hooks,@/components/dashboard,@/components/playground,@/components/ui,@/lib/store,@/components/icons, etc.). Exceptions: default-exporting page/route components referenced by the router (e.g.,import App from '@/App'), and deep paths where no parent barrel exists - Single-statement ifs must not use braces - When an
ifstatement's body is a single statement (typically areturn,throw,continue, orbreak), write it on one line without braces:if (foo) return bar, notif (foo) { return bar }. Multi-statement bodies must keep braces. Never de-brace anifthat has anelsebranch where either side would become ambiguous; keep both sides consistent - No inline translation concatenation - Never concatenate or template translation calls inline. Bad:
t('foo') + ' ' + t('bar'),`${t('foo')}: ${variable}`. Good: a singlet()call with interpolation parameters:t('greeting', { name: variable }). When a composite string is needed, add a new key with{{param}}placeholders to all four language files (en.ts,fr.ts,es.ts,de.ts) and callt()once - No inline magic values - Never use inline string literals, numbers, or object literals when the value represents a domain concept (status names, HTTP methods, agent types, hosts, ports, thresholds, validation limits, etc.). Extract every such value to a typed constant declared
as const. Cross-app constants live inpackages/shared/src/<name>.tswith a named export and are imported destructured (import { networkStatus } from '@openclaw/shared') — never as default. Single-app constants live inapps/<app>/src/lib/constants/. Single-file constants go at the top of the file. See the "Constants Rule" section for the full pattern - Shared package: named exports + destructured imports - All files in
packages/shared/src/(other than theindex.tsbarrel itself) must use named exports, andpackages/shared/src/index.tsmust import them destructured:import { httpMethod } from '#shared/httpMethod'— neverimport HTTP_METHOD from '#shared/httpMethod'. Re-export with the same name without aliasing whenever possible. This matches the existing pattern forplansandsupportedVersions