Architecture

How Appstrate is built: the API, the run sandbox with its sidecar, integrations, modules, and the execution backends.

Appstrate is a TypeScript monorepo on Bun and Hono, with a React dashboard. The API validates a request, creates a run, and starts the agent in an isolated sandbox next to a sidecar that holds every secret. This page is an overview. Each subsystem has a design document in the repository, linked in Go deeper, and those documents are the reference when this page and the code disagree.

Monorepo layout

appstrate/
├── apps/
│   ├── api/            # Hono API: routes, services, auth pipeline, OpenAPI, built-in modules
│   ├── web/            # React 19 + Vite dashboard (also served by the API)
│   └── cli/            # the `appstrate` binary
├── packages/
│   ├── core/           # shared validation, naming, semver, permissions, module contract (npm)
│   ├── afps-shared/    # bundle, SSRF and credential helpers shared with the runtime (npm)
│   ├── afps-runtime/   # portable AFPS bundle runner, signing, conformance, `afps` CLI
│   ├── runner-pi/      # drives the Pi agent run and builds the container environment
│   ├── mcp-transport/  # MCP SDK adapter used by the sidecar and the agent runtime
│   ├── connect/        # OAuth2/PKCE, API-key credentials, credential encryption
│   ├── db/             # Drizzle schema and migrations (PostgreSQL and PGlite), Better Auth
│   ├── env/            # Zod-validated environment: the source of truth for configuration
│   ├── emails/         # email templates
│   ├── ui/             # shared React design system
│   └── module-*/       # workspace modules: chat, claude-code, codex, observability, ee
├── runtime-pi/         # the run container images: agent runtime, sidecar, integration runners
└── system-packages/    # AFPS packages shipped with the platform: integrations and MCP servers

The stack is Bun, Hono, PostgreSQL 16 with Drizzle (or PGlite when no DATABASE_URL is set), Better Auth for sessions, and the Pi coding agent SDK (@earendil-works/pi-coding-agent) as the agent harness. All tenant data is isolated at the application level: every query filters by organization, and by space for space-scoped resources.

What happens on a request

The API assembles one pipeline, in this order (apps/api/src/index.ts):

request
  → error handler (RFC 9457)  → Request-Id  → client IP  → CORS  → body limit
  → health, OpenAPI document, /llms.txt            (no auth)
  → shutdown gate (new writes are refused while draining)
  → Better Auth routes (/api/auth/*)
  → authentication: OIDC module strategies → API key (apst_) → session cookie
  → Appstrate-User impersonation, role-preview and realm guards
  → organization context (X-Org-Id) → permissions → space context (X-Space-Id)
  → API version (Appstrate-Version) → Idempotency-Key guard
  → route handler (per-route rate limit, idempotency)  → module routers

Authorization is RBAC: fixed organization roles, plus a role per space (a preset or an organization-defined bundle). An API key is a ceiling on its creator's authority in one space. See Authentication.

What happens when an agent runs

POST /api/agents/{scope}/{name}/run
   │  validate input, resolve model, connections and version, create the run
   ▼
Platform (API process)
   │  starts, in parallel, on a per-run isolated network
   ▼
┌─ per-run sandbox ───────────────────────────────────────────────┐
│  Sidecar            holds the secrets; speaks MCP at /mcp        │
│    ├─ credential-injecting api_call tool per integration         │
│    ├─ run_history and recall_memory tools                        │
│    ├─ /llm/* reverse proxy to the model provider                 │
│    └─ integration runners (one MCP server per local integration) │
│                                                                  │
│  Agent              Pi agent, runs your prompt                   │
│    └─ sees only the sidecar's tools, never a secret              │
└──────────────────────────────────────────────────────────────────┘
   │  signed run events (logs, status, metrics, result)
   ▼
Platform: persists the run → LISTEN/NOTIFY → SSE streams, webhooks, notifications
  • The sidecar is the security boundary. The agent talks to the sidecar over MCP and never holds a credential. The platform hands the run its sidecar address and token over stdin, not through the environment, and the agent process cannot dump its own memory. The sidecar injects credentials into outbound calls, checks every target against the integration's allowed URIs and against SSRF rules, and proxies model calls so the provider key stays out of the sandbox. Details: SIDECAR.md.
  • Everything the agent can do comes from MCP tools. The agent sees the integration tools it was given, a few first-party tools (run_history, recall_memory), and the runtime tools its manifest enables (output, log, note, pin, publish_file).
  • Runs report back through signed events. The runner posts HMAC-signed events to the platform, which persists them, finalizes the run once, and then emits the status change that drives realtime streams, webhooks, and notifications. Liveness is watched in two phases, boot and execution, so a stuck run is failed with a specific error.
  • The platform image, appstrate-pi (the agent), and appstrate-sidecar speak a versioned protocol and must be deployed at one version. Boot fails when the configured image tags differ, and logs a warning when the two images carry different build revisions.

Integrations

A connection to a third-party service is an integration: an AFPS package that declares its authentication methods (OAuth 2, API key, basic, custom fields, mTLS), the URLs it may call, and where its tools come from:

SourceWhat runsNotes
noneNothing: only the credential-injecting api_call toolThe agent calls the vendor's REST API through the sidecar
localA third-party MCP server, started by the sidecar in its own runner container (node, python, binary, or uv)Needs a container or microVM backend. Egress is limited to the integration's allowed URIs
remoteA vendor-hosted MCP server over Streamable HTTP or SSENo runner. Tool calls leave the perimeter

Credentials are injected on the sidecar side, either into the tool's environment or into HTTP calls by a per-run credential-injecting proxy, and the integration's server never reads the stored secret. An agent selects which tools it uses, and the OAuth scopes requested at consent are inferred from that selection. A run binds the connections of its actor, so an end-user's run uses the end-user's connections. Full detail: INTEGRATIONS_RUNTIME.md. The AFPS format itself is covered in AFPS specification.

Execution backends

RUN_ADAPTER picks where a run executes. It is a registry: modules can add backends.

BackendRUN_ADAPTERIsolationUse
Processprocess (default)None: the agent is a host subprocessDevelopment and Tier 0 installs. Refuses local integration runners
DockerdockerA container per agent and sidecar, on an isolated per-run networkProduction, Tiers 1 to 3 (their Compose files default to it)
FirecrackerfirecrackerOne microVM per run, behind a KVM boundary, driven through the appstrate-runner daemonUntrusted workloads. Opt-in firecracker module

See Progressive infrastructure for tiers, and FIRECRACKER.md for the microVM backend and its threat model.

Modules

Optional features are modules loaded at boot from the MODULES environment variable. A module contributes routes, permissions, OpenAPI paths, auth strategies, hooks, or an execution backend, and, for the Apache 2.0 modules, owns no database tables: they live in the core schema. Removing a module from MODULES leaves no routes, workers, or permissions behind, and its data stays in the database.

ModuleIn the default setProvides
oidcYesOAuth 2.1 and OpenID Connect server for platform users, the CLI, and your end-users
webhooksYesSigned run-event webhooks
mcpYesThe platform API exposed as an MCP server, one endpoint per organization
core-providersYesModel providers (OpenAI, Anthropic, OpenAI-compatible)
@appstrate/module-chatYesThe dashboard chat surface
firecrackerNoThe Firecracker execution backend
@appstrate/module-codex, @appstrate/module-claude-codeNoRun agents on a Codex or Claude Code subscription
@appstrate/module-observabilityNoOpenTelemetry traces and metrics
@appstrate/module-eeNoStripe billing and credit quotas. Source-available, not Apache-2.0

The contract is published in @appstrate/core, so an external module can be installed from npm and listed in MODULES. The authoring guide is apps/api/src/modules/README.md. The default MODULES value is defined in packages/env; check environment variables rather than this table for what your version ships.

Data model

  • An organization has members with a role (owner, admin, member, guest) and contains spaces. A space holds agents, runs, schedules, end-users, files, integration connections, and API keys. See SPACES.md.
  • Packages are agents, skills, MCP servers, and integrations. They are versioned (semver, with dist-tags) and immutable once published. A package belongs to a home space and is made runnable elsewhere by placing it in other spaces. Platform-provided ones have no organization.
  • Runs reference an agent version and record who or what triggered them: a member, an end-user, a schedule, or an API key. Cost is a ledger of model usage, summed per run (RUN_COST.md).
  • Memory is one table keyed by actor (member, end-user, or shared): an archive agents search, and pinned slots injected into the prompt.
  • Credentials are encrypted at rest with a keyed envelope that supports key rotation.

Realtime uses PostgreSQL LISTEN/NOTIFY fanned out to Server-Sent Events, so it needs no Redis and works with PGlite. The scheduler and distributed rate limits use Redis when REDIS_URL is set, and in-memory equivalents otherwise.

Go deeper

The design documents live in docs/architecture/ in the repository:

DocumentTopic
SIDECAR.mdSidecar protocol, MCP surface, egress and SSRF rules
INTEGRATIONS_RUNTIME.mdIntegration runners, credential injection, scopes, remote MCP
FIRECRACKER.mdThe microVM backend
SPACES.mdSpaces, personal spaces, resolution
RBAC_PERMISSIONS_SPEC.mdRoles and permissions
FILES.mdUploads, run outputs, storage quotas
RUN_COST.mdRun cost tracking
OBSERVABILITY.mdOpenTelemetry
SUPPLY_CHAIN.mdSupply-chain posture

The code is Apache 2.0, apart from packages/module-ee. See Contributing.

On this page