The handbook
TroubleshootingLocal developmentDeploymentProvision a new clientStandalone deploymentUpgradesArchitecture
Operate

Standalone deployment

Run AcasoOS inside a client's own cloud as an isolated single-tenant instance. Same repository, same container image, same code path as the shared deployment — one tenant per instance, with RLS still forced as defense in depth. Target: a client SRE without acaso context stands this up in under 4 hours. The in-repo ops counterpart is docs/runbooks/deploy-to-client-infra.md.

Note: Standalone is not a fork. The instance runs a tagged release image built from the acaso-os monorepo — including any bespoke code for this client, which lives in namespaced directories of that same repo. The client never patches a deployment by hand; changes go through the repo and arrive as the next release. That is what keeps Upgrades a tag swap instead of a migration project.

Requirements

ComponentRequirement
Postgres16, with pgcrypto, pgvector, and Apache AGE available. The repo's Postgres image (infra/docker/postgres/) bundles them; managed Postgres needs the extensions enabled via parameter group + infra/init-extensions.sql.
Redis7.x with persistence (streams + cache).
ComputeContainer hosting for three services: DataHub API, AgentHub API, pipeline worker.
IngressLoad balancer with TLS in front of the APIs.
LLM accessAt least one provider key (OpenAI / Anthropic / OpenRouter / self-hosted via LITELLM_API_BASE).
ToolingTerraform 1.7+ or Helm 3.14+, Docker.

1. Provision infrastructure

Pick the matching IaC under infra/:

  • infra/terraform/standalone-aws/RDS Postgres, ElastiCache Redis, ECS Fargate services, ALB, Secrets Manager.
  • infra/terraform/standalone-gcp/Cloud SQL, Memorystore, Cloud Run.
  • infra/terraform/standalone-azure/Azure DB for Postgres, Azure Cache, Container Apps.
  • infra/helm/acaso-os/Kubernetes chart against externally-provisioned Postgres + Redis.
bash
cd infra/terraform/standalone-aws
terraform init
terraform apply -var "tenant_name=BigCorp" -var "region=us-east-1" -var "vpc_cidr=10.0.0.0/16"

2. Build and push the release image

Always deploy a tagged release, never a branch build:

bash
git checkout v0.1.0
docker build -t <REGISTRY>/acaso-os:v0.1.0 .
docker push <REGISTRY>/acaso-os:v0.1.0

Clients with access to acaso's private registry pull the published image for the same tag instead.

3. Migrate, then bootstrap

Run migrations with the privileged database role, then create the tenant and the first admin key:

bash
docker run --rm \
  -e DATABASE_URL="postgresql+asyncpg://acaso:...@<DB_HOST>:5432/acaso_os" \
  <REGISTRY>/acaso-os:v0.1.0 alembic upgrade head

docker run --rm \
  -e DATABASE_URL="..." \
  -e ACASO_BOOTSTRAP_TENANT_SLUG=bigcorp \
  -e ACASO_BOOTSTRAP_TENANT_NAME="BigCorp" \
  <REGISTRY>/acaso-os:v0.1.0 python -m datahub.interface.cli.bootstrap

The bootstrap CLI prints the initial admin API key once — capture it into the client's secrets manager immediately; it cannot be retrieved later. It also seeds the default roles and the management-console Application.

4. Wire the service environment

Minimum production set per container:

  • DATABASE_URLthe non-privileged acaso_app role. Runtime never uses the superuser.
  • ADMIN_DATABASE_URLthe acaso owner role; only the migration step uses it.
  • REDIS_URLthe Redis endpoint.
  • ENVIRONMENTstandalone-<client_slug>.
  • RELEASE_VERSIONbare semver matching the image tag without the v (image v0.1.0 → 0.1.0).
  • One or more LLM provider keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY).
  • ACASO_AGENT_MODULESset explicitly: exactly the client's agent modules (agenthub.custom.<client_slug>.<module>, comma-separated). Left unset, the dev default loads the example agents — never what a client deployment wants. An empty string loads none.
  • ACASO_MCP_TOOL_MODULESonly if this client exposes internal (non-agent) tools over MCP (modules whose import calls register_mcp_tool, comma-separated). Agents that set protocols = (..., "mcp") are exposed without it; empty (the default) publishes no internal tools.
  • ACASO_PUBLIC_BASE_URLoptional; the public base URL (e.g. https://os.aca.so) used to build the A2A Agent Card url when AgentHub sits behind a proxy / load balancer. Falls back to the request host if unset.
  • ACASO_CONSOLE_ORIGINSthe console origin(s) allowed to connect, e.g. https://os.aca.so; leave empty to disable console access entirely.
  • Optional, omittable observability: SENTRY_DSN, LANGFUSE_HOST + LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY. The platform runs fully without them — nothing phones home from an air-gapped deployment.

Full catalog with defaults: Reference: Environment variables.

5. Smoke test

bash
DEPLOYMENT_URL="https://acaso.bigcorp.internal" ADMIN_KEY="<from-bootstrap>" ./scripts/smoke_test.sh

The script exits 0 only if /health and /ready return 200, /v1/ingest accepts a sample event, the worker drains it within 30s, and the sample agent runs end-to-end.

6. Connect the console and finish provisioning

The management console is a static bundle; it talks to whatever instance you point it at. Add the console origin to ACASO_CONSOLE_ORIGINS, connect with the admin key, and complete the client setup — users, roles, keys, cultivator bindings — exactly as in Provision a new client from step 2 onward (the bootstrap already created the tenant).

Hardening checklist

  • Application containers hold only DATABASE_URL (app role); ADMIN_DATABASE_URL lives in the migration job, not the services.
  • Admin key and LLM keys in the client's secrets manager; nothing in plain env files.
  • ACASO_CONSOLE_ORIGINS is an explicit list — wildcard * is rejected at startup by design.
  • Database backups / snapshots scheduled before the first real data lands (they are also the upgrade safety net).
  • TLS on the load balancer; the services never face the internet directly.

White-labeling the console

A standalone instance usually wants the console to read as the client's own brand — name, logo, and accent colour — rather than acaso's. This is build-time configuration via NEXT_PUBLIC_BRAND_* env vars, baked into the static export, so set them before building the console image and rebuild whenever they change. Leave everything unset for the stock acaso look; see packages/web/.env.example for the annotated list.

Name. Setting the brand word is usually enough — the product name and wordmark derive from it, and it flows to the browser title, the sidebar, the Connect modal, and every UI string:

bash
NEXT_PUBLIC_BRAND_NAME=Natura            # → product "Natura OS"
# NEXT_PUBLIC_BRAND_PRODUCT_NAME=Natura Intelligence   # only if not "<name> OS"
# NEXT_PUBLIC_BRAND_WORDMARK_SUFFIX=OS                 # the muted "OS"; "" hides it

Logo. Drop the client's mark into packages/web/public/brand/ and point at it. The mark sits in a rounded tile; tune or remove the tile to suit the asset:

bash
NEXT_PUBLIC_BRAND_LOGO_SRC=/brand/natura-mark.svg
# NEXT_PUBLIC_BRAND_LOGO_TILE=none        # default: acaso's dark gradient
# NEXT_PUBLIC_BRAND_LOGO_SIZE=contain     # default: "64% 64%"
# NEXT_PUBLIC_BRAND_FAVICON_SRC=/brand/natura-favicon.svg

Accent colour. Provide one base hex — the brand "500". The full light→dark ramp is generated from it, and every accent, border, focus ring, and ambient glow re-themes from that ramp:

bash
NEXT_PUBLIC_BRAND_COLOR=#00857A

After building, the exported out/index.html <title> should read <Brand> OS · Management Console, and the sidebar, Connect modal, and UI copy should reference the client brand throughout rather than acaso.

PreviousOperateProvision a new clientNext OperateUpgrades