---
title: Architecture
description: How pols turns an API call into a running VM, where each part runs, and how sandbox traffic is kept apart from the API and the website.
---

pols has three parts: a **control plane** that keeps the record of every sandbox, a **runtime** on each sandbox host that runs the VMs, and an **edge** that terminates TLS and routes traffic. All of them run on dedicated servers at Hetzner in Germany.

```text
  pols CLI · MCP server · TypeScript client · curl        browsers, ssh
                  │ REST, org API key                          │
                  ▼                                            ▼
   edge ── api.pols.so · <sandbox>-<port>.on.pols.so · ssh.pols.so
                  │                                            │
                  ▼                                            │ ports, desktop,
   control plane (polsd + Postgres, reconciler)               │ ssh over the host's
                  │ create, start, stop, fork, delete,         │ private network
                  │ exec, files, computer use                  │
                  ▼                                            ▼
   sandbox host: QEMU/KVM virtual machines on ZFS, one isolated project per org
```

## Desired state and the reconciler

Lifecycle calls do not wait for the VM. `POST /v1/sandboxes`, `stop`, `resume`, `fork` and `DELETE` record what you want, the sandbox's `desired_state` (`running`, `stopped` or `deleted`), and return at once. A reconciler in the control plane then drives the host to that state, retries with backoff when something fails, and notices when a VM stopped on its own.

A sandbox's `status` says where it actually is: `pending`, `running`, `stopped`, `deleted` or `error`. A client waits until `status` equals `desired_state`; the CLI, the MCP server and the TypeScript client's `waitFor` do that for you. See [Lifecycle](/sandboxes/lifecycle/).

Commands, file transfers and computer-use actions are different: they go straight to the running VM and answer when they are done.

## The runtime

Each sandbox is a QEMU/KVM virtual machine managed by Incus. Its disk is a ZFS dataset, which is why copies are cheap: a new sandbox, a fork and a template are all clones of an existing disk and take well under a second to make. Booting the VM takes the rest of the roughly ten seconds a new sandbox needs.

Every org gets its own Incus project, and each sandbox has its own network ACL for its [egress policy](/sandboxes/egress/).

## The edge

The edge runs on the sandbox host and serves every public name:

| Name | Serves |
| --- | --- |
| `api.pols.so` | the REST API |
| `<sandbox>-<port>.on.pols.so` | a [published port](/access/ports/) |
| `<sandbox>-desktop.on.pols.so` | the [desktop](/access/desktop/) in the browser |
| `<sandbox>-cdp.on.pols.so` | the sandbox's Chrome for [Playwright or Puppeteer](/agents/computer-use/#the-browser-over-cdp) |
| `ssh.pols.so` | the [SSH gateway](/access/ssh/) |

`<sandbox>` is the sandbox ID with `-` in place of `_`. The edge keeps no state of its own besides its certificates: for every request and every SSH login it asks the control plane whether, and where, the traffic may go.

All sandbox content is served under `on.pols.so`, never under `pols.so` or `api.pols.so`. A page served by a sandbox therefore cannot read the cookies of the website or the API.

## Data and accounts

The control plane keeps its records in Postgres on the same infrastructure: orgs, users, sandboxes, templates, usage intervals and the [vault](/sandboxes/vault/), whose values are encrypted with a key kept outside the database. Of API keys, login links and sessions it stores only a SHA-256 hash.

Usage is metered from the running intervals: every start and stop is recorded in the same transaction as the status change, and usage is computed per second from them.
