# Puter Admin & Architecture Guide

This guide is for the people who run, extend, and reason about Puter at depth: operators self-hosting the platform, architects evaluating it, and developers contributing to or building extensions for the backend. It documents the system's architecture, its configuration surface, deployment topology, security model, operational procedures, and internals — at the level of detail needed to deploy Puter to production, extend it safely, or reimplement it.

Puter is an open-source **internet operating system**: a browser-based desktop environment backed by a Node.js/Express server, with a client SDK (`puter.js`), a CLI, and a serverless Workers runtime. For end-user and developer-facing usage (the SDK, the desktop, building apps), see the companion **User Guide**. This guide covers the parts users don't touch.

> [!NOTE]
> All facts here are derived from the source at commit `98a21c4`. Where the code and a README disagree, the code wins. The backend is licensed **AGPL-3.0-only**; the `@heyputer/cli` package is MIT.

---

## 1. Executive Summary

| Attribute | Value |
|-----------|-------|
| What it is | Self-hostable browser desktop OS + cloud SDK platform |
| Backend runtime | Node.js **≥ 24**, TypeScript, Express **5** |
| Repository shape | npm-workspaces monorepo (`src/*`, `tools/*`, `extensions`) |
| Core packages | `backend`, `gui`, `puter-js`, `cli`, `worker`, `worker-types`, `mcp-connector`, `docs` |
| Datastores | SQL (SQLite / MySQL·MariaDB / PostgreSQL), Redis-compatible (Valkey), DynamoDB-compatible, S3-compatible object storage |
| Architecture | Layered dependency-injection kernel: **clients → stores → services → controllers → drivers** |
| Extensibility | Dynamically-loaded extensions register into every layer + add routes |
| AI providers | OpenAI, Anthropic, Google Gemini, Mistral, Groq, Together, Replicate, DeepSeek, xAI, and more, behind a driver abstraction |
| Deployment | Docker Compose (one-line installer) or from source |
| License | AGPL-3.0-only (CLI: MIT) |

**Architectural thesis.** Puter is a *kernel* that boots a set of typed registries in dependency order, wires an Express middleware pipeline, and materializes HTTP routes from controllers and extensions. Capabilities that vary by provider (AI, KV, hosting, notifications) are **drivers** behind named interfaces, callable over a uniform `/drivers/call` RPC surface and gated by per-method rate-limit and concurrency policies. The whole thing is configured by a single deep-merged JSON config and is designed to run from a laptop (SQLite, in-process fakes) up to a multi-node cluster (MySQL, Redis, S3).

---

## 2. System Architecture

### 2.1 The layered kernel

The backend is organized as five DI layers, instantiated in strict order by `PuterServer` (`src/backend/server.ts`). Each layer receives the layers below it as constructor arguments:

```
            ┌─────────────────────────────────────────────┐
 Request →  │ Express middleware pipeline (helmet, CORS,   │
            │ auth probe, host validation, body parsing…)  │
            └───────────────┬─────────────────────────────┘
                            ▼
   ┌──────────────┐  HTTP routes materialized from:
   │ CONTROLLERS  │  ← controllers (@Controller + registerRoutes)
   │              │  ← extensions  (extension.get/post/use…)
   └──────┬───────┘
          │ call into
   ┌──────▼───────┐   ┌──────────────┐
   │   DRIVERS    │   │   SERVICES   │   business logic, lifecycle
   │ (RPC, by     │   │              │
   │  interface)  │   └──────┬───────┘
   └──────┬───────┘          │
          │            ┌─────▼────────┐
          └──────────► │    STORES    │   persistence abstractions
                       └─────┬────────┘
                       ┌─────▼────────┐
                       │   CLIENTS    │   raw backends: db, redis,
                       │              │   s3, dynamo, email, event
                       └──────────────┘
```

| Layer | Constructor receives | Responsibility | Examples |
|-------|---------------------|----------------|----------|
| **clients** | `config` | Raw connections to external systems | `database`, `redis`, `s3`, `dynamodb`, `email`, `event`, `alarm` |
| **stores** | `config, clients, stores` | Persistence/domain data access | `app`, `permission`, `user`, `session`, `fs`, `kv`, `subdomain`, `group`, `oidc`, `share`, `systemKv` |
| **services** | `config, clients, stores, services` | Business logic, lifecycle hooks | `auth`, `permission`, `metering`, `fs`, `apps`, `notification`, `socket`, `acl`, `homepage`, `selfhosted`, `health`, `broadcast`, `subdomain` |
| **controllers** | `config, clients, stores, services, drivers` | HTTP route handlers | `auth`, `fs`, `apps`, `drivers`, `desktop`, `share`, `webdav`, `peer`, `hosting`, `oidc`, `system`, `notification`, `static`, `broadcast`, `wisp`, `puterai` |
| **drivers** | `config, clients, stores, services` | Pluggable, RPC-callable capabilities | `aiChat`, `aiImage`, `aiTts`, `aiVideo`, `aiOcr`, `aiSpeech2Txt`, `aiSpeech2Speech`, `kvStore`, `apps`, `subdomains`, `notifications`, `workers` |

The registries are plain objects (`puterClients`, `puterStores`, etc.). At boot the server iterates each, instantiates classes with the right dependencies, and stashes the live instances in container objects (`clientsContainers`, `storesContainers`, …) that the extension system and cross-layer lookups read from.

> [!NOTE]
> Drivers are instantiated **before** controllers so a controller can receive a typed `drivers` reference. The `/drivers/*` HTTP surface lives on a regular `DriverController`.

### 2.2 Boot sequence

`src/backend/index.ts` is the entry point. On `npm start` (after `build:ts`), it runs the compiled `dist/src/backend/index.js`:

1. **Load config** — `loadConfig()` resolves and deep-merges configuration (see [Configuration](#3-configuration)).
2. **Construct `PuterServer`** with the config and all five registries.
3. **`server.start()`** — internally `#setupServer()`:
   a. Dynamically import extensions from `config.extensions` directories.
   b. Instantiate clients, then stores, then services (each merging in extension-registered entries).
   c. Configure the rate limiter against its backend.
   d. Wire anti-CSRF and captcha to Redis.
   e. Build the Express app, set `trust proxy`, install global middleware.
   f. Instantiate drivers, then controllers; materialize controller + extension routes.
   g. Install terminal middleware (404 + error handler).
4. **Signal handling** — `SIGINT`/`SIGTERM` trigger `prepareShutdown()` then `shutdown()` (graceful, with a drain window in multi-node mode).

### 2.3 HTTP request pipeline

Global middleware is installed in this exact order (`#installGlobalMiddleware`):

```
cookieParser
 → compression
 → helmet (noSniff, hsts, ieNoOpen, permittedCrossDomainPolicies, xssFilter; x-powered-by disabled)
 → Cross-Origin-Resource-Policy: cross-origin
 → query-param sanitization (drop non-primitive query values)
 → User-Agent parsing (req.ua)
 → host header validation
 → host redirects (www → root, user-subdomain → static hosting)
 → native-app static serving (editor.*, docs.*, …)
 → CORS (reflect Origin; credentials only on the `api` subdomain)
 → IP validation (optional; emits ip.validate event)
 → OPTIONS preflight (200)
 → body parsing: express.json 50mb (+ a `text/plain;actually=json` shim) + urlencoded 100kb
 → auth probe (populate req.actor from token/cookie; never rejects)
 → per-request ALS context (snapshot actor/requestId)
 → puter-site middleware (*.puter.site / *.puter.app hosting short-circuit)
 → extension global middleware
 → [controller + extension routes]
 → 404 handler
 → error handler (alarms on 5xx)
```

Per-route options are translated to middleware in `#materializeRoute`: subdomain gating, `requireAuth`, `requireUserActor`, `adminOnly`, `allowedAppIds`, `requireVerified`, a default-on email-confirmation gate (`allowUnconfirmed` opts out), a default-on non-access-token gate (`allowAccessToken` opts out), plus captcha, anti-CSRF, rate-limit, and concurrency gates as configured.

> [!IMPORTANT]
> Routes **without** a `subdomain` option are restricted to the root origin — they won't accidentally match API-subdomain requests. `subdomain: '*'` disables the gate; an explicit subdomain restricts to it. This is how the API surface (`api.` subdomain) is isolated from the GUI surface.

### 2.4 The driver model

Drivers are the platform's plugin point for capabilities that have multiple implementations. A driver implements a **named interface** (e.g. `puter-chat-completion`); multiple drivers can implement the same interface (OpenAI and Claude both implement chat completion).

Declared two ways — decorator or imperative fields:

```typescript
// Decorator form
@Driver('puter-chat-completion', { name: 'openai-completion', default: true })
class OpenAIChatDriver extends PuterDriver {
    async complete(args) { /* ... */ }
}

// Imperative form (no decorator)
class OpenAIChatDriver extends PuterDriver {
    readonly driverInterface = 'puter-chat-completion';
    readonly driverName = 'openai';
    readonly isDefault = true;
}
```

Each driver may declare **rate-limit** and **concurrency** policies, per-method, with per-subscription overrides:

```typescript
@Driver('puter-chat-completion', {
    concurrent: {
        default: { limit: 5, backend: 'redis' },
        methods: {
            complete: {
                limit: 5,
                bySubscription: { user_free: 1, unlimited: 50 },
                backend: 'redis',
            },
        },
    },
})
```

- **Backends** for counting: `memory` (per-process), `redis` (cross-node), `kv` (DynamoDB-backed).
- **`bySubscription`** scales the cap by the caller's subscription tier (`user_free`, `temp_free`, `unlimited`, …) resolved via `MeteringService`.
- Policies are **validated eagerly at decoration/registration time** — a malformed block fails the boot, not the first request.
- Methods returning a stream wrap it in a `DriverStreamResult` (`{ dataType: 'stream', content_type, stream }`); the `/drivers/call` handler pipes it to the response instead of `res.json()`.

The built-in driver registry (`src/backend/drivers/index.ts`):

| Registry key | Interface | Implementation |
|--------------|-----------|----------------|
| `kvStore` | `puter-kvstore` | `KVStoreDriver` |
| `aiChat` | `puter-chat-completion` | `ChatCompletionDriver` |
| `aiImage` | `puter-image-generation` | `ImageGenerationDriver` |
| `aiTts` | `puter-tts` | `TTSDriver` |
| `aiVideo` | `puter-video-generation` | `VideoGenerationDriver` |
| `aiSpeech2Speech` | `puter-speech2speech` | `VoiceChangerDriver` |
| `aiSpeech2Txt` / `aiSpeech2TxtXai` | `puter-speech2txt` | `SpeechToTextDriver` / `XAISpeechToTextDriver` |
| `aiOcr` | `puter-ocr` | `OCRDriver` |
| `apps` | `puter-apps` | `AppDriver` |
| `subdomains` | `puter-subdomains` | `SubdomainDriver` |
| `notifications` | `puter-notification` | `NotificationDriver` |
| `workers` | `puter-workers` | `WorkerDriver` |

> [!NOTE]
> All AI drivers share a single `AI_RATE_LIMIT` / `AI_CONCURRENT` policy reference (`drivers/util/aiLimits.ts`). Because sibling drivers on the same interface (e.g. the two speech-to-text impls) share the same policy object, switching providers mid-session cannot dodge the cap — the controller keys limits by `(interface, method, user)`.

### 2.5 AI provider topology

The `aiChat` driver fronts many upstream providers (under `drivers/ai-chat/providers/`): OpenAI, Anthropic (Claude), Google Gemini, Mistral, Groq, Together, OpenRouter, DeepSeek, xAI, Z.AI, MiniMax, Moonshot, Ollama, and others. Image/TTS/video/OCR/speech each have their own provider sets (e.g. TTS: ElevenLabs, OpenAI, AWS Polly, Gemini, xAI; OCR via AWS Textract; image via OpenAI, Together, Replicate, Cloudflare, Gemini, xAI). The SDK's `puter.ai.chat({ model })` selects the model; the driver routes to the right provider. Costs are reported per call and metered against the caller's account by `MeteringService`.

### 2.6 Frontend (GUI)

The desktop (`src/gui`) is a single-page web application built with a custom Node build (`build.js`/`utils.js`): it concatenates and minifies a set of jQuery-based libraries, inlines icons as base64, bundles core modules with webpack, and emits `dist/gui.js` + `bundle.min.css`. It is served as static assets by the backend (`config.gui_assets_root`). The desktop sets `window.puter_gui_enabled = true`, which `puter.js` detects to run in `gui` mode and use the desktop's auth token directly. The GUI's own services (`src/gui/src/services`) extend an `AdvancedBase`/`Service` framework (e.g. `ProcessService`, `IPCService`, `ThemeService`, `LocaleService`, `ExecService`, `AntiCSRFService`).

### 2.7 The client SDK (`puter.js`)

The `Puter` class (`src/puter-js/src/index.js`) is the public client. It detects its environment, registers submodules (`auth`, `os`, `fs`, `ui`, `hosting`, `apps`, `ai`, `kv`, `perms`, `drivers`, `debug`, `peer`, plus `workers` and `net`), and manages auth tokens (with a v1→v2 silent-migration path and a reauth coordinator that drives the sign-in popup on `401 { code: 'reauth_required' }`). It talks to `api.puter.com` (or your configured API origin). Full SDK usage is in the User Guide.

---

## 3. Configuration

### 3.1 Config resolution order

`loadConfig()` (`src/backend/index.ts`) deep-merges three sources, later sources overriding earlier ones:

1. `config.default.json` — bundled OSS defaults (always loaded as the base).
2. **Runtime override** — `process.env.PUTER_CONFIG_PATH` (absolute path; used in prod/Docker) if set and present; otherwise `<package-root>/config.json` (gitignored user override); otherwise none.
3. Computed defaults — `pub_port` defaults to `port`; `origin` and `api_base_url` are derived from `protocol`/`domain`/`pub_port` if not set.

Because the merge is deep, a `config.json` can override just the keys it cares about (e.g. `database`) and inherit everything else.

> [!IMPORTANT]
> Configuration is a **JSON file**, not environment variables. The only environment variables the backend itself reads are `PUTER_CONFIG_PATH` (config location) and a handful of telemetry vars (`OTEL_*`, `NODE_ENV`). Provider keys and operational settings live in the config JSON. Don't expect `PUTER_*` env vars to configure the running server — that's the installer's job.

### 3.2 Core configuration keys

From `config.default.json` (the dev defaults) and the production `config.json` the installer writes:

| Key | Default (dev) | Purpose |
|-----|---------------|---------|
| `env` | `dev` | `dev` or `prod` |
| `port` | `4100` | Bind port |
| `pub_port` | = `port` | Externally-visible port (behind a proxy) |
| `protocol` | `http` | `http` / `https` |
| `domain` | `puter.localhost` | Primary domain |
| `origin` | computed | Externally-visible base URL |
| `api_base_url` | computed | `https://api.<domain>` |
| `cookie_name` | `puter_auth_token` | Session cookie name |
| `static_hosting_domain` | `site.puter.localhost` | `*.puter.site`-equivalent for user sites |
| `static_hosting_domain_alt` | `host.puter.localhost` | Alternate hosting domain |
| `private_app_hosting_domain` | `app.puter.localhost` | Private app hosting |
| `private_app_hosting_domain_alt` | `dev.puter.localhost` | Alternate private app domain |
| `default_user_group` / `default_temp_group` | UUIDs | Group assignment for new/temp users |
| `storage_capacity` | `104857600` (100 MB) | Per-user storage quota (bytes) |
| `is_storage_limited` | `false` | Enforce the quota |
| `min_pass_length` | `6` | Minimum password length |
| `strict_email_verification_required` | `false` | Require verified email |
| `enable_public_folders` | `true` | Enable `~/Public` |
| `trust_proxy` | unset (`false`) | Reverse-proxy hop count (see Security) |
| `gui_assets_root` | `./src/gui` | Where GUI static assets live |
| `puterjs_root` | `./src/puter-js/dist` | Where the built SDK lives |
| `extensions` | `["./extensions"]` | Extension directories to load |
| `builtin_apps` | `{ dev-center: ./src/dev-center }` | Built-in apps served by the server |

### 3.3 Secrets

| Key | Purpose |
|-----|---------|
| `jwt_secret` | **Verify-only** secret for legacy v1 tokens still in circulation |
| `jwt_secret_v2` | Signs **every new** token |
| `allow_v1_tokens` | Whether v1 tokens are still accepted |
| `url_signature_secret` | Signs URLs (e.g. file read URLs, download links) |

> [!CAUTION]
> The bundled `config.default.json` ships placeholder secrets (`dev-jwt-secret-change-me`, etc.) for local development **only**. The installer generates fresh random secrets (`openssl rand -hex 64`) for production. Never run a public instance with the default secrets — anyone can forge tokens.

The token scheme is dual-secret: `jwt_secret_v2` signs new tokens; `jwt_secret` only *verifies* old v1 tokens during the migration window. The SDK silently migrates v1→v2 via `/auth/migrate-token`; once a v2 token exists the v1 key is cleared.

### 3.4 Datastore configuration

```json
"database": {
    "engine": "mysql",          // or "sqlite" / "postgres"
    "host": "mariadb",
    "port": 3306,
    "user": "puter",
    "password": "...",
    "database": "puter",
    "migrationPaths": ["/opt/puter/dist/src/backend/clients/database/migrations/mysql"]
},
"redis": { "startupNodes": [{ "host": "valkey", "port": 6379 }], "tls": false },
"dynamo": { "endpoint": "http://dynamo:8000", "bootstrapTables": true, "aws": { ... } },
"s3": { "s3Config": { "endpoint": "http://s3:9000", "accessKeyId": "...", "secretAccessKey": "...", "region": "us-east-1", "forcePathStyle": true } },
"s3_bucket": "puter-local",
"s3_region": "us-east-1"
```

- **SQL engines:** `sqlite` (default dev; zero-config, file at `volatile/runtime/puter-database.sqlite`), `mysql`/MariaDB, and `postgres`. Migrations live under `src/backend/clients/database/migrations/{sqlite,mysql,postgres}` — point `migrationPaths` at the matching dialect.
- **Redis/Valkey:** used for sessions, anti-CSRF tokens, captcha, rate limiting, and cross-node socket fan-out. In dev with no nodes configured, an in-process `ioredis-mock` is used.
- **DynamoDB-compatible:** the `systemKv` store and `kv` rate-limit backend. `bootstrapTables: true` auto-creates tables.
- **S3-compatible:** object storage for user files. `forcePathStyle: true` for self-hosted MinIO/RustFS endpoints.

### 3.5 Other notable keys

| Key | Purpose |
|-----|---------|
| `rate_limit.backend` | Default rate-limit backend (`redis` default, or `memory`/`kv`) |
| `providers.ollama.enabled` | Toggle local-LLM provider |
| `allow_all_host_values` / `allow_no_host_header` | Relax host validation (dev only) |
| `captcha.enabled` | Enable the captcha gate |
| `enable_ip_validation` | Enable the IP-validation gate |
| `custom_domains_enabled` | Allow custom domains for hosting |

---

## 4. Deployment

### 4.1 Docker Compose (recommended)

The supported deployment is Docker Compose via the one-line installer:

```bash
curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh
# Windows: irm https://raw.githubusercontent.com/HeyPuter/puter/main/install.ps1 | iex
```

The installer creates `./puter-selfhosted/`, downloads `docker-compose.yml` and `nginx/nginx.conf`, generates secrets, writes `.env` + `puter/config/config.json`, and runs `docker compose up -d`. The stack:

| Service | Role |
|---------|------|
| `puter` | The app server (this repo's backend) |
| `mariadb` | SQL database (MySQL engine) |
| `valkey` | Redis-compatible cache/coordination |
| `dynamo` | DynamoDB-compatible KV |
| `s3` (RustFS/MinIO-class) | Object storage |
| `nginx` | Reverse proxy / TLS termination front door |

`.env` carries the infra secrets (`MARIADB_*`, `S3_*`, `HTTP_PORT`); `puter/config/config.json` carries the app config (domains, JWT secrets, datastore endpoints, `trust_proxy: 1`).

> [!WARNING]
> On native Linux (not Docker Desktop), bind-mounted data directories can hit permission errors: several upstream images run as non-root (RustFS UID 10001, dynamo UID 1000) and can't `chown` an existing bind-mount. The installer `chmod 0777`s `puter/data/{valkey,mariadb,dynamo,s3,puter}` to sidestep the UID mismatch. If you create the stack by hand, replicate this.

### 4.2 From source (development)

```bash
git clone https://github.com/HeyPuter/puter
cd puter
npm install          # postinstall sets up extensions
npm start            # runs build:ts then the compiled server
```

Requires **Node.js ≥ 24**. Default config serves on **port 4100** at `puter.localhost` using SQLite and an in-process fake S3 (`fauxqs`) — no external services. Build scripts:

| Script | Effect |
|--------|--------|
| `npm start` / `npm run dev` | `build:ts` then run `dist/src/backend/index.js` |
| `npm run build` | Build backend TS + GUI + puter-js |
| `npm run build:ts` | `tsc -p tsconfig.build.json` + write dist package.json |
| `npm run build:workerLib` | Build puter-js then the worker preamble |
| `npm run test:backend` | Vitest backend suite (`:postgres` variant for PG) |
| `npm run check-translations` | Validate i18n completeness |

### 4.3 Reverse proxy and `trust_proxy`

Puter is designed to run behind a reverse proxy (nginx in the stack, or Cloudflare). The `pub_port`/`origin` split exists precisely so the externally-visible URL can differ from the bind `port`. Set `config.trust_proxy` to the **number of proxy hops** (e.g. `1` for a single nginx/Cloudflare hop) so Express derives `req.ip`/`req.protocol` from `X-Forwarded-For` correctly.

> [!CAUTION]
> Never set `trust proxy` to `true` in production. That trusts *every* hop and makes `X-Forwarded-For` forgeable — a client could spoof its IP and defeat IP-based gating and rate limiting. Always use the exact hop count.

### 4.4 Multi-node scaling

Puter scales horizontally because its coordination state lives in shared backends, not process memory:

- **Sessions, anti-CSRF, captcha** → Redis (issue on node A, consume on node B).
- **Rate limiting** → Redis sorted-sets by default (cross-node), or `kv` (DynamoDB).
- **Socket.IO fan-out** → `@socket.io/redis-streams-adapter` broadcasts events across nodes.
- **Object storage / DB** → shared S3 + SQL.

Set `rate_limit.backend: redis` and use `memory` backends only on single-node deployments. The graceful-shutdown drain window lengthens when a `serverId` is configured (multi-node), giving in-flight requests time to finish.

### 4.5 DNS / hosting domains

A production instance needs DNS for the primary domain and the hosting wildcards: the main `domain`, `static_hosting_domain` (and `_alt`) for `*.puter.site`-style user sites, and the `private_app_hosting_domain` (and `_alt`) for app hosting. Point wildcard A records at the instance; nginx routes by `Host`.

---

## 5. Security

Puter's security model is built into the request pipeline and the actor/permission system.

### 5.1 Authentication

- **Tokens:** JWTs signed by `jwt_secret_v2`. Legacy v1 tokens are verify-only via `jwt_secret` during migration; `allow_v1_tokens` gates acceptance. The SDK migrates v1→v2 silently and falls back to an interactive reauth flow on `401 { code: 'reauth_required' }`.
- **Cookie:** session token in `config.cookie_name` (`puter_auth_token`).
- **Auth probe:** a global middleware that *populates* `req.actor` if a valid token/cookie is present but **never rejects** — rejection is the job of per-route gates.
- **Actors and subscriptions:** every authenticated request carries an actor with a subscription tier (`user_free`, `temp_free`, `unlimited`, …) used by metering and per-tier rate/concurrency caps. A `SYSTEM_ACTOR` bypasses metering for internal calls.

### 5.2 Route-level gates

Materialized per route from options (`#materializeRoute`):

| Gate | Effect |
|------|--------|
| `requireAuth` | Reject unauthenticated requests |
| `requireUserActor` | Require a real user (not an app/access token) |
| `adminOnly` | Admin accounts only (implies `requireAuth`) |
| `allowedAppIds` | Restrict to specific app IDs (implies `requireAuth`) |
| `requireVerified` | Require a verified account |
| email-confirmation gate | **Default-on** for authed routes; `allowUnconfirmed` opts out |
| non-access-token gate | **Default-on**; `allowAccessToken` opts out |
| `subdomain` | Restrict the route to a subdomain (root-only by default) |
| captcha / anti-CSRF | Bot / CSRF protection where configured |
| rate-limit / concurrency | Throttle by `(interface/route, method, actor)` |

> [!IMPORTANT]
> Authenticated routes reject **unconfirmed** accounts by default — unconfirmed users can't touch AI, FS, or driver endpoints, only essential flows (logout, confirm-email, whoami, save-account). Access tokens are also blocked by default. Both are deliberate opt-out gates, not opt-in.

### 5.3 Transport and headers

The global pipeline applies Helmet (`noSniff`, `hsts`, `ieNoOpen`, `permittedCrossDomainPolicies`, `xssFilter`), disables `x-powered-by`, sets `Cross-Origin-Resource-Policy: cross-origin`, and sanitizes query parameters to primitives. CORS reflects the caller's `Origin` (puter.js is meant to be embedded on arbitrary sites) but only sets `Access-Control-Allow-Credentials: true` on the **`api`** subdomain — never on the main GUI domain, which also gets `X-Frame-Options: SAMEORIGIN` to block framing.

### 5.4 Host validation and IP gating

Host headers are validated against the configured domain set (main + hosting domains, plus `at.<static>` and optional `nip.io`); unknown hosts get `400` unless `custom_domains_enabled`. `/healthcheck` is always reachable regardless of host. With `enable_ip_validation`, an `ip.validate` event lets listeners (IP-reputation/Redis checks) allow or deny per request — using `req.ip` (which honors `trust proxy`), never raw `X-Forwarded-For`.

### 5.5 Rate limiting and abuse control

Per-driver/method rate-limit and concurrency policies (with per-subscription tiers) throttle expensive operations. Examples from the source:

| Driver | Default rate limit | Notes |
|--------|--------------------|-------|
| KV | 400 / 10s (temp users 200) | No concurrency cap (cheap) |
| Apps | 100 / 10s (temp 50) | |
| Subdomains | 200 / 10s (temp 100) | |
| Notifications | 3000 / 30s (temp 1000) | Poll-heavy, generous |
| AI (all) | Shared `AI_RATE_LIMIT` / `AI_CONCURRENT` | Per-subscription concurrency on `complete` |

Counting backends: `redis` (cross-node, default), `memory` (single-node), `kv` (DynamoDB). Policies validate at boot, so a typo can't silently disable a limit.

### 5.6 Operational alerting

The terminal error handler raises an alarm (`clients.alarm`) on 5xx errors (and on forced codes like `upstream_rate_limited`/`upstream_auth_failed`), de-duplicated by `route + error signature` so a crash loop pages once with N occurrences rather than N pages. Errors prefixed `upstream_`/`client_` are surfaced to the user but don't page.

### 5.7 Worker and content isolation

User-hosted sites (`*.puter.site`/`*.puter.app`) are short-circuited by a dedicated middleware before any API/GUI route. Workers run in a sandboxed Worker runtime; a request carrying a `puter-auth` header gets a scoped `user.puter` context, while the deployer's own resources are reachable via `me`/`my`/`myself`. CORS on workers defaults to permissive but is toggleable per worker.

> [!CAUTION]
> Because `puter.js` is designed to be embedded on third-party sites and the user pays for usage, the platform's trust boundary is the **user's authorization**, not the embedding origin. The consent popup + per-app sandbox (`~/AppData/{app-id}`) + explicit `puter.perms` requests are what protect a user's broader data from an app they authorized. Operators enabling new drivers should set appropriate rate/concurrency caps to bound abuse.

---

## 6. Extensions

Puter supports dynamically-loaded backend extensions (configured via `config.extensions` directories, default `./extensions`). An extension is a module that, at load time, writes into a shared `extensionStore` through a global `extension` API. `PuterServer` drains that store during boot, merging extension entries into each layer.

The `extension` API surface (`src/backend/extensions.ts`):

| Capability | API |
|------------|-----|
| Register into layers | `registerClient`, `registerStore`, `registerService`, `registerController`, `registerDriver` |
| Global middleware | `registerGlobalMiddleware(mw)` |
| Routes | `extension.get/post/put/delete/patch/head/options/all(path, options?, handler)` and `extension.use(...)` |
| Events | `extension.on(eventKey, (key, data, meta) => …)` |
| Config access | `extension.config` (lazy proxy, populated at boot) |
| Cross-layer lookup | `extension.import('service:foo' | 'client:bar' | 'store:baz' | 'controller:qux' | 'driver:fred')` |

Extension routes use the **same** `RouteOptions` vocabulary as controllers (`subdomain`, `requireAuth`, `requireUserActor`, `adminOnly`, `allowedAppIds`, body parsers), so they get identical gate and parser treatment — there is no second-class route path. Extension-registered clients/stores/services/controllers/drivers are instantiated alongside the built-ins.

```javascript
// An extension module (loaded from an extensions directory)
extension.get('/hello', { requireAuth: true }, (req, res) => {
    res.json({ hello: req.actor.username });
});

extension.registerDriver('myThing', MyThingDriver);

extension.on('user.created', async (key, data, meta) => {
    // react to platform events
});
```

> [!TIP]
> Extensions are the supported way to add custom backend behavior without forking the core. TypeScript declaration merging (`IExtensionDriverInstances`, etc.) flows extension types into `this.drivers` and the `extension.import('driver')` proxy so custom layers are typed.

---

## 7. Operations

### 7.1 Health checks

`GET /healthcheck` is reachable regardless of `Host` (it bypasses host validation) — point your load balancer / orchestrator liveness probe at it. A `health` service and `HealthController` provide deeper status.

### 7.2 Logs and the admin password

```bash
cd puter-selfhosted
docker compose logs -f puter                 # follow app logs
docker compose logs puter | grep password     # first-boot admin credentials (printed once)
```

### 7.3 Start / stop / update

```bash
docker compose up -d        # start
docker compose down         # stop
docker compose pull && docker compose up -d   # update images
```

For a from-source deployment, `SIGTERM`/`SIGINT` trigger graceful shutdown; `prepareShutdown()` runs first, then `shutdown()` after a drain window (longer when `serverId` indicates a multi-node deployment).

### 7.4 Backups

Back up three things: the **SQL database** (mysqldump / `sqlite` file / `pg_dump`), the **S3 object store** (user files), and the **config + secrets** (`puter/config/config.json`, `.env`). The DynamoDB-compatible store holds rebuildable system KV; Redis/Valkey is cache and can be lost. Losing `jwt_secret_v2`/`url_signature_secret` invalidates all sessions and signed URLs — treat them as the crown jewels.

### 7.5 Telemetry

The backend integrates OpenTelemetry (`src/backend/telemetry.ts`), configured by env: `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_TRACE_SAMPLE_RATIO`, `OTEL_SERVICE_NAME`. `npm start` preloads the telemetry module (`-r ./dist/src/backend/telemetry.js`).

### 7.6 Monitoring abuse

Watch for: alarm pages on `upstream_rate_limited`/`upstream_auth_failed` (provider problems), 5xx alarm clusters (de-duped by route+signature), and rate-limit rejections. Tune driver caps and per-subscription tiers if a tier is being abused.

---

## 8. Repository Structure

```
puter/                         (npm workspaces: src/*, tools/*)
├─ config.default.json         OSS dev defaults (port 4100, sqlite, placeholder secrets)
├─ install.sh / install.ps1    one-line self-host installers
├─ src/
│  ├─ backend/                 @heyputer/backend — the kernel (Express 5, TS)
│  │  ├─ index.ts              entry: loadConfig + start PuterServer
│  │  ├─ server.ts             PuterServer: layer wiring + middleware pipeline
│  │  ├─ extensions.ts         extension API + extensionStore
│  │  ├─ clients/              db (sqlite/mysql/postgres), redis, s3, dynamodb, email, event, alarm
│  │  ├─ stores/               app, permission, user, session, fs, kv, subdomain, …
│  │  ├─ services/             auth, permission, metering, fs, apps, notification, socket, …
│  │  ├─ controllers/          auth, fs, apps, drivers, desktop, webdav, hosting, oidc, …
│  │  ├─ drivers/              ai-chat/-image/-tts/-video/-ocr/-speech*, kv, apps, subdomain, workers
│  │  └─ core/http/middleware/ gates, authProbe, rateLimit, captcha, antiCsrf, hostRedirects, …
│  ├─ gui/                     @heyputer/gui — the desktop SPA (jQuery + webpack build)
│  ├─ puter-js/                @heyputer/puter.js — the client SDK
│  ├─ cli/                     @heyputer/cli — the `puter` command (MIT)
│  ├─ worker/                  @heyputer/worker — worker preamble + s2w router
│  ├─ worker-types/            @heyputer/worker-types — Workers runtime TS types
│  ├─ mcp-connector/           Cloudflare-Worker MCP server for Puter (+ OAuth)
│  ├─ dev-center/              built-in app: developer center
│  └─ docs/                    docs.puter.com site generator (sidebar = API reference)
├─ extensions/                 default extensions directory (loaded at boot)
├─ nginx/                      reverse-proxy config (fetched by installer)
└─ tools/                      build/maintenance scripts (translations, dist packaging)
```

> [!NOTE]
> The MCP connector (`src/mcp-connector`) is a Cloudflare-Worker MCP server that exposes Puter filesystem/subdomain operations to AI agents, authenticated per-caller with their own Puter token and an OAuth flow (`.well-known/oauth-*`, `/authorize`, `/token`, `/register`). It's an optional integration, not part of the core server.

---

## 9. Code Quality & Conventions

- **TypeScript-first backend** with strict ESLint + Prettier (4-space indent, single quotes). The build uses a relaxed `tsconfig.build.json` (`noCheck`, non-strict) for emit speed while type-checking happens separately.
- **Dependency injection by convention:** every layer class takes `(config, [clients], [stores], [services], [drivers])` positionally. This makes the wiring in `server.ts` uniform and testable (tests pass empty mocks).
- **Eager validation:** driver policies validate at decoration/registration, config resolves at boot — misconfiguration fails fast, not at first request.
- **Fail-loud errors with structured codes:** `HttpError` with `legacyCode`/`statusCode`; the error handler distinguishes 4xx (client) from 5xx (page-worthy), with `upstream_`/`client_` prefixes controlling alerting.
- **Test stack:** Vitest (backend, with a Postgres variant), Mocha (GUI), Playwright (puter-js e2e). e2e tests read the admin password from `PUTER_ADMIN_PASSWORD` (the first-boot banner).
- **Extension parity:** extension routes and layers are first-class — same options, same materializer — so the plugin path doesn't rot relative to core.

---

## 10. Potential Improvements

These are observations from the source, useful for contributors and operators planning upgrades:

- **Config is JSON-only.** There's no env-var override for individual settings beyond `PUTER_CONFIG_PATH`. Operators who prefer 12-factor env config must template the JSON themselves (as the installer does). A documented env→config mapping would help container deployments.
- **Default secrets are footguns.** `config.default.json` ships working placeholder JWT/URL secrets. A boot-time refusal to start in `env: prod` with a default secret would harden against accidental exposure.
- **`trust_proxy` correctness is load-bearing.** IP gating and rate limiting depend on the correct hop count; a misconfigured proxy silently degrades both. A startup self-check (compare `req.ip` against a known probe) could warn operators.
- **Podman support is unofficial.** The installer and compose target Docker; community demand exists ([#3170](https://github.com/HeyPuter/puter/issues/3170)). Testing/​documenting a Podman path would broaden self-host reach.
- **Worker readiness signal.** The CLI cannot confirm a worker deploy is live (no liveness signal yet) — a readiness endpoint or status field would let `puter worker deploy` verify success.
- **Native-Linux bind-mount permissions.** The `chmod 0777` workaround for non-root container UIDs is pragmatic but broad; per-service UID mapping would be tighter.

---

## 11. Quick Reference

### Ports & domains (defaults)

| Item | Dev default |
|------|-------------|
| Backend bind port | `4100` |
| GUI dev server | `4000` (auto-increments if busy) |
| Primary domain | `puter.localhost` |
| Static hosting | `site.puter.localhost` / `host.puter.localhost` |
| App hosting | `app.puter.localhost` / `dev.puter.localhost` |
| Health check | `GET /healthcheck` |

### Layer instantiation order

`clients → stores → services → drivers → controllers` (extensions merged into each).

### Key files

| File | Role |
|------|------|
| `src/backend/index.ts` | Entry point + config resolution |
| `src/backend/server.ts` | `PuterServer` kernel: wiring + middleware + routes |
| `src/backend/extensions.ts` | Extension API |
| `src/backend/drivers/meta.ts` | Driver metadata + rate-limit/concurrency policy |
| `src/backend/drivers/index.ts` | Built-in driver registry |
| `config.default.json` | Bundled dev defaults |
| `install.sh` / `install.ps1` | Self-host installers |

### Driver interfaces

`puter-chat-completion` · `puter-image-generation` · `puter-tts` · `puter-video-generation` · `puter-speech2speech` · `puter-speech2txt` · `puter-ocr` · `puter-kvstore` · `puter-apps` · `puter-subdomains` · `puter-notification` · `puter-workers`

### Subscription tiers

`user_free` · `temp_free` · `unlimited` (extensible; resolved via `MeteringService`)

### Operational commands

```bash
docker compose up -d                          # start the stack
docker compose logs -f puter                  # follow logs
docker compose logs puter | grep password     # first-boot admin password
docker compose pull && docker compose up -d   # update
curl -fsSL .../install.sh | sh                # (re)install — keeps existing secrets
```

---

_Generated June 2026 from HeyPuter/puter at 98a21c4. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
