Self-Hosting

Progressive Infrastructure

Appstrate's 4-tier infrastructure model, from zero-install to full production deployment.

The Progressive Model

Appstrate resolves its infrastructure adapters at boot based on which environment variables are set. Each missing dependency is replaced by an in-process fallback, so you can start with zero external services and promote piece by piece.

Infrastructure adapters live in apps/api/src/infra/ and are loaded via dynamic imports driven by hasExternalDb(), hasRedis(), hasS3(), and getExecutionMode() in infra/mode.ts.

The 4 Tiers

Tier 0 — Zero-install

ComponentImplementation when tier condition met
DatabasePGlite (embedded WASM PostgreSQL, ./data/pglite/)
StorageLocal filesystem at FS_STORAGE_PATH (default ./data/storage)
Queue / schedulerLocalQueue — a custom in-process cron evaluator, not BullMQ
PubSubLocalPubSub — Node EventEmitter
Rate limiterrate-limiter-flexible in-memory (RateLimiterMemory)
Run executionBun subprocess on the host (RUN_ADAPTER=process, the default)

No external dependencies. Ideal for development and quick evaluation.

cp .env.example .env && bun run dev

Tier 1 — PostgreSQL

Set DATABASE_URL to switch to PostgreSQL. Data becomes persistent and multi-user works properly. Queue, PubSub, rate limiter, and storage stay in-process.

bun run docker:dev:minimal

Tier 2 — PostgreSQL + Redis

Set REDIS_URL on top of Tier 1 to unlock:

  • BullMQ (Redis-backed) for the scheduler queue
  • Redis PubSub for realtime and cross-instance run cancellation
  • Redis-backed distributed rate limiting via rate-limiter-flexible's RateLimiterRedis
  • Horizontal scaling: multiple Appstrate instances can share the same Redis and distribute work
bun run docker:dev:standard

Tier 3 — Object storage and/or container isolation

Tier 3 adds two independent capabilities on top of Tier 2:

  • S3_BUCKET routes storage to S3 or MinIO. Without it, storage stays on the filesystem at FS_STORAGE_PATH.
  • RUN_ADAPTER=docker runs each agent inside an ephemeral Docker container on an internal-only network, with a per-run sidecar for credential injection. Without it, agents run as Bun subprocesses on the host (no isolation). See Sandbox & Sidecar for the isolation model.

RUN_ADAPTER=docker additionally requires the Docker daemon socket to be accessible (DOCKER_SOCKET, default /var/run/docker.sock).

bun run docker:dev

Summary Table

TierAdded servicesIn-process fallbacks still in useUse case
0Bun onlyPGlite, LocalQueue, LocalPubSub, RateLimiterMemory, filesystem storage, Bun subprocessDevelopment, evaluation
1+ PostgreSQLLocalQueue, LocalPubSub, RateLimiterMemory, filesystem storage, Bun subprocessPersistent data, multi-user
2+ RedisFilesystem storage, Bun subprocessScheduling, distributed rate limiting, horizontal scaling
3+ S3/MinIO and/or DockerDepends on which axis you enableFull prod with isolation and/or object storage

Migrating between tiers

Promoting from Tier 0 to Tier 1+ means swapping the database backend from PGlite to PostgreSQL. There is no PGlite dump REST endpoint today — you have two practical paths:

  1. Fresh start (recommended if you have no production data yet): stop the Tier 0 instance, set DATABASE_URL, restart. Drizzle migrations run automatically at boot and create an empty schema in the new PostgreSQL database.
  2. Copy data manually: stop the Tier 0 instance, export each table from PGlite with a script that reads the local PGlite data directory, then import into PostgreSQL. PGlite and PostgreSQL share a schema, so pg_dump-compatible output can be restored with psql "$DATABASE_URL" -f backup.sql. Plan for downtime during the swap.

Promoting Tier 1 → Tier 2 (adding Redis) or Tier 2 → Tier 3 (adding S3/Docker) is non-destructive: the adapters swap at the next boot, no data migration needed.

Drizzle migrations run automatically at every boot for both PGlite and PostgreSQL. Schema evolution within a tier is handled; there is no built-in rollback path.

Other boot behaviors worth knowing

  • Orphan run cleanup: on startup, any run still in running or pending from a previous process is marked failed, and Docker containers labeled appstrate.managed=true are removed.
  • System packages sync: system-packages/*.afps ZIPs are loaded and synced to the database with orgId: null on every boot.
  • Sidecar pool warm-up: when RUN_ADAPTER=docker, SIDECAR_POOL_SIZE pre-warmed sidecar containers are created at boot (default 2, set 0 to disable).

Recommendation

For production, use Tier 3 (PostgreSQL + Redis + S3/MinIO + Docker execution) behind a TLS reverse proxy. Tier 0 is the fastest way to evaluate Appstrate without installing anything. Tiers 1 and 2 are suitable for staging or shared development environments.

On this page