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 serversThe 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 routersAuthorization 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), andappstrate-sidecarspeak 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:
| Source | What runs | Notes |
|---|---|---|
none | Nothing: only the credential-injecting api_call tool | The agent calls the vendor's REST API through the sidecar |
local | A 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 |
remote | A vendor-hosted MCP server over Streamable HTTP or SSE | No 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.
| Backend | RUN_ADAPTER | Isolation | Use |
|---|---|---|---|
| Process | process (default) | None: the agent is a host subprocess | Development and Tier 0 installs. Refuses local integration runners |
| Docker | docker | A container per agent and sidecar, on an isolated per-run network | Production, Tiers 1 to 3 (their Compose files default to it) |
| Firecracker | firecracker | One microVM per run, behind a KVM boundary, driven through the appstrate-runner daemon | Untrusted 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.
| Module | In the default set | Provides |
|---|---|---|
oidc | Yes | OAuth 2.1 and OpenID Connect server for platform users, the CLI, and your end-users |
webhooks | Yes | Signed run-event webhooks |
mcp | Yes | The platform API exposed as an MCP server, one endpoint per organization |
core-providers | Yes | Model providers (OpenAI, Anthropic, OpenAI-compatible) |
@appstrate/module-chat | Yes | The dashboard chat surface |
firecracker | No | The Firecracker execution backend |
@appstrate/module-codex, @appstrate/module-claude-code | No | Run agents on a Codex or Claude Code subscription |
@appstrate/module-observability | No | OpenTelemetry traces and metrics |
@appstrate/module-ee | No | Stripe 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:
| Document | Topic |
|---|---|
| SIDECAR.md | Sidecar protocol, MCP surface, egress and SSRF rules |
| INTEGRATIONS_RUNTIME.md | Integration runners, credential injection, scopes, remote MCP |
| FIRECRACKER.md | The microVM backend |
| SPACES.md | Spaces, personal spaces, resolution |
| RBAC_PERMISSIONS_SPEC.md | Roles and permissions |
| FILES.md | Uploads, run outputs, storage quotas |
| RUN_COST.md | Run cost tracking |
| OBSERVABILITY.md | OpenTelemetry |
| SUPPLY_CHAIN.md | Supply-chain posture |
The code is Apache 2.0, apart from packages/module-ee. See Contributing.