Jesse A. Green / notebook

Puter Admin & Architecture Guide

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.

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

// 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:

@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

"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

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

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 0777s 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)

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'

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.

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

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

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

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