Skip to content

Self-hosting

Architecture and trust boundaries

The services, ports and data flows of a critlab install, and why prototypes are served from their own origin.

A critlab install is three stateless Node.js services in front of Postgres, Redis and an S3-compatible object store. The one design decision that shapes everything else: uploaded prototypes are untrusted code, so they are never served from the app's origin.

critlab self-hosted architectureBrowsers reach one TLS layer. It routes the app host to web, every per-version sandbox host to sandbox, and /ws to realtime. web uses Postgres, Redis and S3-compatible storage; sandbox reads Postgres and storage and asks web for workspace bucket credentials over an internal route; realtime uses Redis. SMTP and an identity provider are optional.untrusted · cookieless · one origin per versioninternalBrowsercreators · reviewers · crit CLITLS: ingress controller or Caddyapp host · *.sandbox zone (wildcard) · /wsweb :3000Next.js app, API, auth, SCIMsandbox :4001serves prototype filesrealtime :4010presence, cursors, live syncPostgres 16all recordsRedis 7fan-out, live syncS3-compatible storagepackages, voice, imagesOptionalSMTP relay · IdP
Solid arrows are requests; dashed arrows are internal calls (sandbox → web for bucket credentials, web → optional services).

Components and ports

ComponentDefault portWhat it doesState
web3000Next.js app: dashboard, viewer page, REST API, sign-in (email code, OIDC, SAML), SCIM, licence checks. Health: GET /api/health.Stateless
sandbox4001Serves prototype files from the sandbox origin, injects the overlay bridge, sets the isolation headers. Health: GET /health.Stateless (in-memory file cache)
realtime4010WebSocket at /ws for presence, cursors, follow and spotlight, and live comment updates. Health: GET /health.Stateless (in-memory rooms)
Postgres5432Every record: workspaces, prototypes, versions, comments, audit log. Version 16 is what the images and chart ship.Stateful
Redis6379Pub/sub between web and realtime instances.Ephemeral
Object storage9000 (SeaweedFS)Package files (content-addressed), voice notes, screenshots, thumbnails. Any S3-compatible store.Stateful
Caddy or an ingress controller80 / 443TLS termination and routing by host.Certificates only

A migration job (the migrate image) runs database migrations before web starts; it exits when done. See Upgrades and migrations.

Hostnames

An install answers on at least two names, and they must differ:

  • The app host, e.g. critlab.example.com — web, and realtime at /ws unless you give realtime its own host.
  • The sandbox zone, e.g. s.example.com — every prototype version is served from its own subdomain, https://<label>.s.example.com/. This needs a wildcard certificate for *.s.example.com. See Domains and TLS.

Optionally, workspaces on a licensed install can serve reviewer links on their own hostname (custom share domains).

Trust boundaries

The sandbox origin is untrusted. Prototypes are HTML and JavaScript written by an AI agent or a person you may not know. critlab contains them:

  • A separate origin per version. The label in each sandbox hostname is a signed, short-lived capability issued by web for one version. A tampered, expired or never-issued label gets the same 404. Two prototypes never share an origin, so one cannot read another's storage.
  • Cookieless. The sandbox never sets or receives a session cookie. There is nothing in it to steal.
  • Sandboxed iframe with a strict CSP. The viewer frames the prototype in a sandboxed iframe with an opaque origin. The sandbox's own CSP carries a sandbox directive, so opening a prototype URL directly is sandboxed too. External images and fonts are off unless a prototype's owner turns them on; scripts, connections and frames to other origins are never allowed.
  • A narrow message bridge. The comment overlay talks to the prototype only through a token-checked postMessage bridge that forwards a fixed list of messages.
  • Never indexed. Every sandbox and viewer response carries X-Robots-Tag: noindex, nofollow, noarchive, and both origins serve a deny-all robots.txt.
  • Server-side rendering is framed too. Thumbnails are rendered in a headless browser that frames the prototype the same way, behind a network egress allowlist.

The app origin is trusted. It holds sessions, API keys and the only copy of decrypted credentials for workspace-owned buckets. The sandbox asks web for those credentials over an internal route protected by a shared secret (/api/internal/*); nothing else crosses that line.

realtime trusts only signed tokens. web mints a presence token after its own access check; realtime verifies it with the shared SANDBOX_TOKEN_SECRET and checks no origin or cookie.

Data flows

Opening a share link. The browser loads the viewer page from web. web checks access (link, password or membership), mints a sandbox label for the version, and returns a page that frames https://<label>.<zone>/. The sandbox verifies the label, looks up the version's file manifest in Postgres, streams files from object storage, and injects the bridge into HTML documents.

Leaving a comment. The bridge captures the element's selectors, text and position and a screenshot inside the sandboxed frame, and hands them to the overlay. The overlay posts them to web, which stores the comment in Postgres and the images in object storage, then publishes an ids-only event to Redis. realtime relays it to everyone viewing that prototype, whose overlays refetch.

An agent picking up feedback. The crit CLI or MCP server calls web's API with an API key. web returns threads with their screenshots, selectors and environment. The agent publishes a new version the same way the browser does.

Outbound requests from web. Only when configured: your SMTP relay, an identity provider's metadata URL, Slack-compatible webhooks, Linear or Jira when enabled, a workspace's own bucket, and page titles for links pasted into comments. Each user-supplied URL goes through one guard that refuses private, loopback and cloud-metadata addresses and re-checks every redirect. Air-gapped installs lists how to turn each off.