# Access and connections (/docs/access) There are two common connection methods: * **Push:** Alien uses a scoped identity in the environment to create or update resources. * **Pull:** a component in the environment opens the connection and uses the environment’s own identity. ```text push Alien control plane → cloud API → deployment pull component in deployment → outbound HTTPS → Alien control plane ``` Read [Deployment models](/docs/deploying/deployment-models) for the exact setup and [Permissions](/docs/permissions) for the generated access. # Alien framework (/docs/alien-framework) Use the Alien framework when Alien should build and update the deployment for you. You describe the remote code and the resources it needs in `alien.ts`. Alien turns that description into a deployment for the platform the environment owner chooses. Your hosted application stays in your cloud and calls the deployment through Commands, HTTP, events, or resource bindings. Start with the [Quickstart](/docs/quickstart), then read [How Alien works](/docs/how-alien-works) and [Stacks](/docs/stacks). If an existing Helm chart already defines the application in Kubernetes, use [Remote Operator](/docs/remote-operator) instead. # What data moves? (/docs/boundary) Connection direction does not tell you what data moves. A deployment can open every connection and still send commands, results, logs, errors, and telemetry to your hosted product. ```text hosted product deployment │ command input │ ├────────────────────────▶│ │◀────────────────────────┤ command result │◀────────────────────────┤ logs and telemetry ``` | Path | Examples | | ---------------------------- | --------------------------------------- | | Sent to the deployment | command inputs, HTTP requests, events | | Returned to your application | command results, HTTP responses, errors | | Reported to Alien | health, status, logs, telemetry | Write down the contents of each message before promising that data stays in one environment. A command that returns database rows moves those rows. A log that includes a request body moves that body. For each path, decide what happens when the deployment cannot be reached: fail the request, retry it, or use a hosted fallback. Test that decision with the remote side unavailable. Related: [AI Gateway](/docs/ai-gateway), [Encryption Gateway](/docs/encryption-gateway), and [Commands](/docs/commands). # Cloud Scoping (/docs/cloud-scoping) Your application lives in a small, isolated area within the customer's cloud account. This page explains what that area looks like on each provider, and how the boundary is drawn. The requirements are the same everywhere: lightweight to set up, fine-grained enough that your application can't touch anything outside its boundary, and easy for the customer's security team to review. For Operate Existing, the same boundaries define read-only inventory scope. Alien observes resources in the agreed account, project, resource group, or Kubernetes selector without taking lifecycle ownership. ## Azure — Resource Group [#azure--resource-group] A [resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal) is a container that holds related resources. It's the clearest model — the boundary is explicit and visible in the Azure portal. The customer creates a resource group for your deployment. All of your application's resources live inside it. Permissions are scoped to the group — Alien can't see or manage anything in other resource groups. ``` ╔═ Customer's Azure Subscription ════════════════════════════╗ ║ ║░ ║ ┌─ rg-acme-myapp ───────────────────────────────────┐ ║░ ║ │ │ ║░ ║ │ Container App · Blob Storage · Service Bus │ ║░ ║ │ Key Vault · Managed Identity │ ║░ ║ │ │ ║░ ║ └───────────────────────────────────────────────────┘ ║░ ║ ║░ ║ Other resource groups — untouched. ║░ ╚════════════════════════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ## GCP — Project [#gcp--project] A [GCP project](https://cloud.google.com/resource-manager/docs/creating-managing-projects) is a natural isolation boundary. Each project has its own resources, permissions, and billing. The customer creates a project for your deployment. Alien's permissions are scoped to that project with custom IAM roles generated from Alien's permission sets, resource-level bindings, and IAM Conditions where GCP supports them. Other projects in the customer's organization are invisible. ``` ╔═ Customer's GCP Organization ══════════════════════════════╗ ║ ║░ ║ ┌─ Project: acme-myapp ────────────────────────────┐ ║░ ║ │ │ ║░ ║ │ Cloud Run · Cloud Storage · Pub/Sub │ ║░ ║ │ Secret Manager · Service Account │ ║░ ║ │ │ ║░ ║ └──────────────────────────────────────────────────┘ ║░ ║ ║░ ║ Other projects — untouched. ║░ ╚════════════════════════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ## AWS — Resource Prefix [#aws--resource-prefix] AWS doesn't have a built-in container like a resource group or project. Instead, the boundary is defined through a **naming prefix** — a short, unique string that all of your deployment's resources share. Alien generates an 8-character prefix for each deployment (e.g. `k44e9b72`). Every resource is named with this prefix: `k44e9b72-worker` (Lambda), `k44e9b72-data` (S3 bucket), `k44e9b72-tasks` (SQS queue). IAM policies then restrict access to resources matching that prefix — every ARN is scoped to `k44e9b72-*`. ``` ╔═ Customer's AWS Account ═══════════════════════════════════╗ ║ ║░ ║ Resources matching prefix "k44e9b72-*": ║░ ║ ┌─────────────────────────────────────────────────────┐ ║░ ║ │ │ ║░ ║ │ k44e9b72-worker (Lambda) │ ║░ ║ │ k44e9b72-data (S3 bucket) │ ║░ ║ │ k44e9b72-tasks (SQS queue) │ ║░ ║ │ k44e9b72-secrets (Secrets Manager) │ ║░ ║ │ │ ║░ ║ └─────────────────────────────────────────────────────┘ ║░ ║ ║░ ║ Other resources — denied by IAM policy. ║░ ╚════════════════════════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` Why a prefix instead of tags or a dedicated account? A few reasons: * **Works everywhere.** S3 bucket names, Lambda function names, SQS queue names, Secrets Manager names — all support prefix-based scoping in IAM policies. Not all AWS services support tag-based access control. * **Lightweight.** No need to create a separate AWS account or organization unit per customer. Hundreds of deployments can coexist in the same account, each isolated by prefix. * **Reviewable.** The customer can look at the IAM policy and see exactly which resources the role can touch — every ARN ends with the prefix pattern. The prefix is generated automatically by Alien. You don't choose it or manage it. For observe-only AWS setups, a dedicated account is the cleanest boundary. AWS resource discovery uses account-level tagging APIs, so discovery cannot be fully constrained by tag or prefix. ## What all three share [#what-all-three-share] The mechanism differs, but the result is the same: * **Clear boundary** — your application's resources are isolated from everything else in the account. * **Revocable** — the customer can remove access at any time by deleting the resource group, project, or IAM role. * **Reviewable** — the customer's security team can inspect exactly what permissions exist before approving. Alien derives the scoping automatically from your stack definition. You define the resources; Alien generates the correct naming, policies, and role bindings for each cloud. For Operate Existing, the customer chooses the scope directly: AWS account or prefix, GCP project, Azure resource group, or Kubernetes namespace and selector. # Remote Commands (/docs/commands) Your software runs in the customer's cloud. Commands are how your backend talks to it — invoke explicit handlers on remote deployments without opening inbound ports. This is the primary way your cloud service communicates with customer deployments. An AI agent sends tool calls to a worker in the customer's VPC. A data connector runs queries against a private database. A dashboard pulls live status. Each of these is a command: your code defines handlers, your backend invokes them, and responses come back. No open ports, no VPN, no generic shell. Commands view in the dashboard — recent invocations across deployments with status, latency, and arguments ## Define a Handler [#define-a-handler] Every command is addressed to one command-enabled resource: a Worker, Container, or Daemon. How the target runs its handler depends on that compute type. ### On a Worker [#on-a-worker] A Worker receives pushed commands through Alien's Worker runtime; it never polls the command server. Register handlers with `command` from `@alienplatform/sdk`. Pass a schema to validate and type the input — any [Standard Schema](https://standardschema.dev) validator works (zod, valibot, arktype): ```typescript import { command } from "@alienplatform/sdk" import { z } from "zod" command( "generate-report", z.object({ startDate: z.string(), endDate: z.string() }), async ({ startDate, endDate }) => { const data = await fetchData(startDate, endDate) return { report: aggregate(data), rowCount: data.length } }, ) command("run-migration", async params => { // No schema: params is the decoded JSON, typed as unknown. await migrate(params) return { status: "completed" } }) ``` The handler's optional second argument carries per-invocation metadata — `commandId`, `attempt`, and `deadline`. Delivery is at-least-once, so use `attempt` to make redeliveries idempotent: ```typescript command("run-migration", async ({ version }: { version: string }, { attempt, commandId }) => { if (attempt > 1 && (await alreadyApplied(version))) { return { status: "completed", deduped: commandId } } await migrate(version) return { status: "completed" } }) ``` Enable commands on the Worker: ```typescript title="alien.ts" const toolExecutor = new alien.Worker("tool-executor") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .commandsEnabled(true) .permissions("execution") .build() ``` The handler receives the invocation's parameters directly and returns the response value. ### On a Container or Daemon [#on-a-container-or-daemon] A Container or Daemon has no Worker runtime in front of it. Enabling commands injects configuration only; the application must run an explicit **pull receiver** from `@alienplatform/commands` (or `alien-commands` in Rust). That receiver leases commands addressed to this resource, dispatches them to your handlers, and submits the responses — all over outbound HTTPS. Alien does not start a receiver sidecar, and the Operator does not run the app's receiver or handlers. The receiver is an ordinary library. `@alienplatform/commands` is pure `fetch` over the command wire protocol, with no native addon, no gRPC, and no injected globals, so it runs in a plain Node 18+ or Bun process. Nothing about the process has to be Alien-aware beyond starting the receiver and reading the environment below. ```typescript import { createCommandReceiver } from "@alienplatform/commands" import { z } from "zod" const receiver = createCommandReceiver() receiver.command( "generate-report", z.object({ startDate: z.string(), endDate: z.string() }), async ({ startDate, endDate }) => { const data = await fetchData(startDate, endDate) return { report: aggregate(data), rowCount: data.length } }, ) async function main() { await receiver.run() } void main() ``` ```rust let mut receiver = alien_commands::Receiver::from_env()?; receiver.command("generate-report", |params: serde_json::Value, _ctx| async move { let data = fetch_data(¶ms).await?; Ok(serde_json::json!({ "report": aggregate(&data), "rowCount": data.len() })) }); receiver.run().await?; ``` `createCommandReceiver()` (TypeScript) and `Receiver::from_env()` (Rust) read the same environment, which Alien injects for a command-enabled resource. Both receivers accept the identical variable names. | Variable | Required | Value | | ------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `ALIEN_COMMANDS_URL` | Yes | Base URL of the command server. Must be an `http` or `https` URL. | | `ALIEN_COMMANDS_TOKEN` | One of the two | Bearer token for the receiver's outbound lease and response requests. | | `ALIEN_COMMANDS_TOKEN_FILE` | One of the two | Path to a file holding that token. Reread once after a `401`, so projected credentials can rotate without restarting the app. | | `ALIEN_DEPLOYMENT_ID` | Yes | Deployment the leased commands belong to. | | `ALIEN_COMMANDS_TARGET_RESOURCE_ID` | Yes | This resource's id inside the deployment's stack. | | `ALIEN_COMMANDS_TARGET_RESOURCE_TYPE` | Yes | `container` or `daemon`, lowercase. | Validation runs synchronously at construction and fails fast. A missing, empty, or invalid value throws `COMMAND_RECEIVER_CONFIG_INVALID` naming the offending variable, so a misconfigured process dies at startup instead of polling nothing. `ALIEN_COMMANDS_TARGET_RESOURCE_TYPE` accepts only `container` and `daemon`. `worker` is rejected, along with any other value. A receiver must not guess its own target type, and a Worker is pushed to rather than leased. The lease loop is tunable through `ALIEN_COMMANDS_POLL_INTERVAL_MS` (default 5000), `ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS` (30000), `ALIEN_COMMANDS_POLL_JITTER` (0.1), `ALIEN_COMMANDS_LEASE_SECONDS` (60), `ALIEN_COMMANDS_MAX_LEASES` (1), and `ALIEN_COMMANDS_DRAIN_TIMEOUT_MS` (30000). The matching `createCommandReceiver()` options override the environment. `receiver.command(name, handler)` decodes the JSON parameters and passes them as the handler's first argument; add a [Standard Schema](https://standardschema.dev) validator as the middle argument to validate and type them. For non-JSON payloads, `receiver.handleRaw(name, handler)` (Rust: `receiver.handle_raw`) hands you the raw parameter bytes as `ctx.input`, a `Uint8Array` you decode yourself. The handler's second argument is the context: an abort `signal`; the effective execution-budget `deadline`; `commandId`; `attempt`; the receiver's `target` (`resourceId` and `resourceType`); and optional W3C `traceContext` (`traceparent` and `tracestate`). Rust exposes the equivalent fields as `cancellation`, `deadline`, `command_id`, `attempt`, `target`, and `trace_context`, with the raw bytes reachable through `ctx.input` (and `ctx.input_json()` for typed decoding). Whatever the handler returns is JSON-encoded and submitted as the command's success response. `run()` drives the lease loop until you trigger graceful shutdown — `receiver.stop()` in TypeScript, or in Rust grab a handle with `receiver.shutdown_handle()` *before* moving the receiver into `run()`, then call `handle.shutdown()`. Both stop taking new leases and give in-flight handlers up to 30 seconds to finish by default; after that, remaining handlers are cancelled and their leases are released before `run()` resolves. Call `run()` alongside whatever else the process does (for example, serving an HTTP API). Enable commands in the stack the same way: ```typescript title="alien.ts" const connector = new alien.Daemon("connector") .code({ type: "image", image: "ghcr.io/acme/connector:v1" }) .commandsEnabled(true) .permissions("execution") .build() ``` ## Invoke from the CLI [#invoke-from-the-cli] The CLI has no `--target` flag: it relies on single-target inference, so it works when the deployment has exactly one command-capable resource. In a deployment where the `connector` Daemon is the only command-enabled resource: ```bash alien commands invoke \ --deployment acme-corp \ --command generate-report \ --params '{"startDate": "2025-01-01"}' ``` With more than one command-capable resource (say the `tool-executor` Worker *and* the `connector` Daemon), an untargeted invoke is rejected with `COMMAND_TARGET_AMBIGUOUS` — target explicitly from code with `.target(name)`, below. ## Invoke an operations plugin from the CLI [#invoke-an-operations-plugin-from-the-cli] Operations plugins are separate from application-owned command receivers. Use `alien operations invoke` to run an enabled operation directly without an AI agent: ```bash alien operations invoke \ --project production \ --deployment acme/prod \ --operation kubernetes/rollout-status \ --params '{"namespace": "app", "workload": "deployment/api"}' ``` Alien applies the project's operations approval policy before creating the command. For an auto-approved operation, the CLI waits for completion and prints the decoded JSON result. For an operation that requires approval, it reports that approval is required and does not dispatch a command. ## Invoke from Code [#invoke-from-code] Senders use `CommandsClient` from `@alienplatform/commands`. For an Alien-hosted deployment, create the client from the deployment ID and a server-side Alien API key: ```typescript import { CommandsClient } from "@alienplatform/commands" const commands = await CommandsClient.forDeployment({ deploymentId: "deployment_123", apiKey: process.env.ALIEN_API_KEY!, }) const result = await commands .target("tool-executor") // the resource that hosts the handler .invoke("generate-report", { startDate: "2025-01-01", endDate: "2025-03-31", }) ``` `forDeployment()` resolves the manager for the deployment and mints short-lived, command-only access. Reuse the client; it refreshes that connection before expiry and once after a manager `401`. For a self-hosted or directly addressed manager, construct the client with an explicit manager URL and token: ```typescript const commands = new CommandsClient({ managerUrl: "https://manager.example.com", deploymentId: "deployment_123", token: process.env.ALIEN_COMMANDS_TOKEN, }) ``` Both clients use HTTP and go through the control plane. The command is stored there until Alien delivers it to the Worker runtime or the targeted Container or Daemon leases it over outbound HTTPS. ## How It Works [#how-it-works] 1. You invoke a command for a deployment. Code names the target with `.target(resourceId)`; the CLI uses the deployment's sole command-capable resource and rejects ambiguous deployments. 2. Alien stores the request on the command server. 3. Alien resolves how commands reach that specific target: * A **Worker** always receives a push. In a push deployment, the Manager dispatches it through the platform's Worker delivery path. In a pull deployment, the Operator leases pending Worker work and relays it to the targeted Worker runtime; the Worker itself never polls. * A **Container or Daemon** always uses the app-owned pull receiver, regardless of the deployment model. 4. The handler runs and produces a response. 5. You read the result. The customer's environment never needs to allow inbound traffic from Alien's control plane. Cloud-provider delivery or the in-environment Operator reaches the Worker runtime. App-owned Container/Daemon receivers lease only their own targeted work over outbound HTTPS. Pull-receiver delivery is at-least-once — a lease that expires before its response is submitted is redelivered. Receiver handlers should tolerate running more than once for the same command (`attempt` in the receiver context tells you the delivery attempt). Each handler runs under an execution budget of `min(command deadline, lease expiry − a 5s safety margin)`: `signal` fires when the budget expires and the handler result is discarded, so an expired lease never has an in-flight duplicate submitting late. There is no lease renewal — long work should be sized to the deadline. When a handler fails, the sender sees a typed error code: `HANDLER_ERROR` (the handler threw), `HANDLER_TIMEOUT` (budget expired), or `UNKNOWN_COMMAND` (no handler registered for the name). ## Delivery Mode Is Not the Deployment Model [#delivery-mode-is-not-the-deployment-model] "Push" and "pull" name two unrelated things in these docs. Command delivery is one axis. The [deployment model](/docs/deploying/deployment-models) is another. | | Command delivery | Deployment model | | ---------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Decided by | The target resource's type | The customer's environment, at onboarding | | Push means | Alien delivers the command to a Worker runtime, which never polls | The Deployment Manager impersonates an identity in the customer's cloud and calls cloud APIs | | Pull means | The application leases its own commands with `createCommandReceiver` or `alien_commands::Receiver` | An Operator inside the customer's environment polls outbound for releases | | Scope | One command at a time | The whole deployment | The two axes do not constrain each other. A Worker is pushed to on a push deployment and on a pull deployment. A Container or Daemon leases its own commands on both. Moving a customer from push to pull changes who applies releases; it does not change how any handler receives a command, and it does not put the Operator in the path of a Container or Daemon receiver. ## Real-World Examples [#real-world-examples] ### AI Agent — remote tool calls [#ai-agent--remote-tool-calls] ```typescript import { command, storage } from "@alienplatform/sdk" const workspace = storage("workspace") command("read-file", async ({ path }: { path: string }) => { const object = await workspace.get(path) return { content: object.data.toString("utf8") } }) command("write-file", async ({ path, content }: { path: string; content: string }) => { await workspace.put(path, Buffer.from(content)) return { ok: true } }) command("list-files", async ({ directory }: { directory: string }) => { return (await workspace.list(directory)).map((entry) => entry.location) }) ``` The agent harness runs in your cloud — planning, model calls, orchestration. When it needs to act, it sends commands to the Worker, Container, or Daemon in the customer's environment. The handler controls what leaves as the command response: a file, an aggregate, a diff, or a redacted excerpt. ### Data Connector — query behind the firewall [#data-connector--query-behind-the-firewall] ```typescript import { command, vault } from "@alienplatform/sdk" import { Pool, type PoolConfig } from "pg" command("get-users", async ({ status, limit }: { status: string; limit: number }) => { const creds = vault("credentials") const config = await creds.getJson("warehouse") const pool = new Pool(config) const { rows } = await pool.query( "SELECT id, name, email FROM users WHERE status = $1 LIMIT $2", [status, limit ?? 100] ) return { rows, count: rows.length } }) ``` Warehouse credentials stay in the customer's vault. The query result is the command response, so the handler should return only the rows, aggregates, or diagnostics you intend to expose. # Your product and the deployment (/docs/control-plane) Your hosted application owns accounts, billing, product-wide state, and the user experience. A deployment owns the code and resources that need another environment. Commands, HTTP, events, and bindings connect them. Choose the interface from the work you need to do, then document the inputs, results, logs, and telemetry that cross it. ```text hosted product deployment accounts · billing private code · resources │ │ └── commands · HTTP · events ──┘ · bindings ``` See [How Alien works](/docs/how-alien-works) and [What data moves?](/docs/boundary). # Remote debugging (/docs/debug) When your software runs in a customer's cloud, debugging usually means screenshots, copy-pasted logs, and Zoom calls. `alien debug` replaces that. It opens a secure channel into a customer's deployment and runs a local command — or an interactive shell — against it, using credentials the manager hands out just for that session. Nothing about the customer's network changes: there are no inbound ports, no VPN, and no shared cloud access. The deployment behaves like another region in your own cloud. ```bash # Run a cloud CLI against the customer's environment alien debug acme/prod -- aws sts get-caller-identity alien debug acme/prod -- gcloud projects list alien debug acme/prod -- kubectl get pods # No command drops you into a shell with the env already set alien debug acme/prod ``` A deployment can be referenced by ID (`dep_...`), by name, or as `/`. ## How it works [#how-it-works] `alien debug` asks the manager for a short-lived debug session, then runs your command with the environment that session returns. Two things make it safe: * **Least-privilege identity.** The session acts as the deployment's own scoped identity — the same one Alien uses to manage that environment (see [Impersonation](/docs/impersonation)). It can touch the deployment's [isolated area](/docs/cloud-scoping) and nothing else. The customer controls what that identity is allowed to do, and access can require [approval](/docs/permissions). * **Ephemeral credentials.** Any credential files (like a kubeconfig) are written to a per-session temp directory with `0600` permissions and deleted when the command exits. Nothing is left on disk. Under the hood the channel uses whichever [deployment model](/docs/deploying/deployment-models) the customer is on: * **Push.** The CLI runs a loopback proxy on `127.0.0.1` and points the cloud CLI at it (`AWS_ENDPOINT_URL` and the GCP/Azure equivalents). Requests tunnel to the manager over an authenticated WebSocket, where they're re-signed with the impersonated identity and forwarded to the cloud. Your `aws` command thinks it's talking to AWS; it's really talking through Alien. * **Pull.** A lightweight agent inside the environment calls out over HTTPS. Same result, no inbound connection. ## Scope and auditing [#scope-and-auditing] Debug sessions are scoped to operating the deployment, not reading customer data. They run as the management identity, are bounded by the permissions the customer granted, and leave an audit trail. When you need to inspect data on demand instead, reach for a [remote command](/docs/commands), which runs inside the environment and returns only what you ask for. ## What's next [#whats-next] # Deployment portal (/docs/deployment-portal) After you publish a release, create a deployment group and generate a setup link. The environment owner chooses a supported platform and runs the generated setup for that environment. ```bash alien onboard "Acme" \ --external-id org_123 \ --setup-items application ``` Test the complete handoff before sending it: open the link, complete setup, confirm the deployment checks in, and verify one safe operation. Read the detailed [Deployment Portal guide](/docs/deploying/deployment-portal) and [Customer setup](/docs/deploying/onboarding-customers). # Events (/docs/events) Events call a handler inside the deployment. Use them when the caller does not need an immediate result. ```text Queue message ─┐ Storage event ─┼──> handler in your Worker Schedule ──────┘ ``` ```typescript import { onCronEvent, onQueueMessage, onStorageEvent, } from "@alienplatform/sdk" onQueueMessage("jobs", async message => { // Queue delivery is at least once. Make this handler idempotent. }) onStorageEvent("uploads", async event => { // React to an object change in this deployment. }) onCronEvent("nightly", async () => { // Run scheduled work. }) ``` The handler uses the same bindings as other code in the Worker. Queue delivery is at least once, so processing must tolerate retries and duplicate messages. Continue with [Worker events and triggers](/docs/infrastructure/worker/events-and-triggers), [Queue](/docs/infrastructure/queue), or [Storage](/docs/infrastructure/storage). # External URLs (/docs/external-urls) When a Worker, Container, or Daemon declares a named public endpoint, Alien creates an HTTPS endpoint for that resource endpoint. Containers can also declare TCP endpoints for non-HTTP protocols (see [TCP Endpoints](#tcp-endpoints)). Alien creates the infrastructure — the customer's network controls who can reach it. Depending on how the customer configures their environment, the same endpoint could be: * **On the public internet** — for receiving webhooks from SaaS services like GitHub, Stripe, or Slack * **Available only to employees** — behind a VPN or private DNS (`tool.corp.internal`), for dashboards and admin interfaces * **Available only to other services** — as an internal API that other services in the customer's environment call over HTTP ## Declaring Endpoints [#declaring-endpoints] Workers, Containers, and Daemons declare endpoints with `.publicEndpoint()`. Workers take a name only; Containers and Daemons also take the port to expose: ```typescript const api = new alien.Worker("api") .code({ type: "source", src: "./api", toolchain: { type: "typescript" } }) .publicEndpoint("api") .build() const web = new alien.Container("web") .code({ type: "image", image: "ghcr.io/acme/web:v1" }) .publicEndpoint("web", 8080, "http") .build() const agent = new alien.Daemon("agent") .code({ type: "image", image: "ghcr.io/acme/agent:v1" }) .publicEndpoint("api", 8080, "http") .build() ``` Endpoint names must be lowercase DNS labels (letters, digits, hyphens). ## Hostnames [#hostnames] Each deployment gets its own domain, and every endpoint becomes a hostname on it. By default the endpoint name is the host label — an endpoint named `api` is served at `api.`. Options control the hostname: ```typescript .publicEndpoint("api", 8080, { protocol: "http", hostLabel: "@" }) .publicEndpoint("tenants", 8080, { protocol: "http", hostLabel: "tenants", wildcardSubdomains: true, }) ``` | Option | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `hostLabel` | Overrides the host label. `"@"` serves the endpoint at the deployment domain apex. | | `wildcardSubdomains` | Also routes `*..` to the endpoint — one declaration covers per-tenant subdomains. Cannot be combined with the apex host label. | ## TCP Endpoints [#tcp-endpoints] Declare an endpoint with protocol `"tcp"` to expose a non-HTTP protocol — a database wire protocol, a message broker, a custom binary protocol: ```typescript const postgres = new alien.Container("postgres") .code({ type: "image", image: "postgres:16" }) .publicEndpoint("pg", 5432, "tcp") .build() ``` TCP endpoints route through a network load balancer instead of an HTTP front door. Traffic passes through to the container — Alien does not terminate TLS, so the workload owns its own protocol and encryption. The endpoint still gets a hostname on the deployment domain, and clients connect to it on the declared port. During rollouts and deletes, replicas are removed from the load balancer before they stop, and existing connections drain within the workload's `stopGracePeriod`. ## What Gets Created [#what-gets-created] Each platform sets up the endpoint differently: | Platform | Infrastructure | URL Format | | ---------- | ----------------------------------------------------------------- | ------------------------------------- | | AWS | API Gateway or load balancer, depending on resource type | Provider hostname or generated domain | | GCP | Cloud Run or HTTPS load balancer, depending on resource type | Provider hostname or generated domain | | Azure | Container Apps or Application Gateway, depending on resource type | Provider hostname or generated domain | | Kubernetes | Ingress or Gateway API route, depending on cluster | Generated domain or custom domain | ## TLS [#tls] Because every deployment runs in a different cloud account, there is no shared certificate. Alien issues a TLS certificate per deployment and imports it into the customer's cloud — ACM on AWS, Certificate Manager on GCP, Key Vault on Azure — then renews and re-imports it automatically. The certificate lives in the customer's account, where the load balancer references it. Customers who need to manage their own certificates can bring their own domain and certificate reference instead (see [Custom Domains](#custom-domains)); Alien never sees the private key. ## Getting the URL [#getting-the-url] Resource outputs include `publicEndpoints`, keyed by endpoint name: | Field | Description | | ---------------------- | --------------------------------------------------------------------------- | | `url` | Base URL of the endpoint. | | `host` | Hostname only. | | `wildcardHost` | Wildcard hostname (`*.`), when `wildcardSubdomains` is enabled. | | `loadBalancerEndpoint` | DNS target for CNAME records when pointing a custom domain at the endpoint. | **From the manager API:** ``` GET /v1/deployments/:id/info ``` Returns the resolved endpoint URLs per resource, so your control plane reads them instead of constructing them. ## Use Cases [#use-cases] * **AI APIs** — expose an inference endpoint in the customer's cloud that your control plane calls * **Webhooks** — receive callbacks from third-party services (Stripe, GitHub, Slack) * **Internal tools** — customer employees access dashboards or admin interfaces * **Microservices** — other services in the customer's environment call your Worker over HTTP ## Custom Domains [#custom-domains] Customers can use their own domain and TLS certificate instead of the auto-generated cloud URL. This is configured per-deployment via stack settings: ```json { "domains": { "customDomains": { "api": { "domain": "api.corp.megacorp.internal", "certificate": { "aws": { "certificateArn": "arn:aws:acm:us-east-1:..." } } } } } } ``` Platform-specific certificate references: | Platform | Certificate Source | | ---------- | ------------------------------------ | | AWS | ACM certificate ARN | | GCP | Certificate Manager certificate name | | Azure | Key Vault certificate ID | | Kubernetes | Existing TLS Secret reference | Alien handles certificate import and load balancer configuration. The customer manages DNS. # Frozen & Live Resources (/docs/frozen-and-live) `frozen` and `live` answer one question: **who is allowed to change this resource after setup?** ```ts title="alien.ts" export default new alien.Stack("my-app") .add(data, "frozen") .add(api, "live") .build() ``` * A **frozen** resource is owned by customer setup. An ordinary rollout cannot create, change, replace, or delete it. * A **live** resource is owned by Alien's Deployment Manager. It can create, update, replace, or remove the resource as it rolls out a release. This setting does not control what your application can read or write. Runtime access is configured separately with [permission profiles](/docs/permissions). ## Why the distinction exists [#why-the-distinction-exists] Customer setup and day-to-day deployment management need different cloud permissions. During setup, the customer runs the generated CloudFormation, Terraform, Helm, or CLI flow with their own credentials. That flow creates setup-owned infrastructure and the limited identity used by the Deployment Manager. After setup, the Deployment Manager uses that limited identity to provision and update live resources. It does not receive general authority to change frozen resources. ```text customer setup ongoing rollouts ────────────── ──────────────── creates frozen resources create and update live resources creates management identity leave frozen resources unchanged ``` If a later release adds or changes a frozen resource, Alien stops the rollout and asks you to run setup again. Setup authority is required because the existing management identity does not own that change. ## Which resources can use each lifecycle? [#which-resources-can-use-each-lifecycle] Workloads are live because Alien needs to deploy and replace their code: * Worker * Container * Daemon Storage, Queue, KV, Vault, Postgres, and AI can be frozen or live. Use frozen when changes should go back through customer setup. Use live when Alien must be able to create and reconcile the resource during normal rollouts. Some foundational resources, including Network, Key, Artifact Registry, and Compute Cluster, are setup-owned and therefore frozen. Alien validates the lifecycle supported by each resource type during the build. ## Permissions are separate [#permissions-are-separate] There are three distinct permission paths: 1. **Setup permissions** belong to the customer running the setup flow. 2. **Management permissions** let the Deployment Manager reconcile live resources. 3. **Runtime permissions** let your Worker, Container, or Daemon use linked resources. For live resources, Alien derives the provisioning permissions needed for ongoing reconciliation. Frozen resources do not receive those permissions. Optional features such as health checks, telemetry, Commands, and Remote Bindings may add their own narrowly scoped permissions; they do not turn a frozen resource into a live one. Observed resources are different again. Alien can discover and display them, but it did not create them and does not reconcile them. See [Operate existing environments](/docs/operate). # Google Cloud setup (/docs/google-cloud-oauth) Google Cloud setup can use Alien’s shared OAuth application or an OAuth application you register. The callback URI and requested permissions are shown in the deployment setup flow. ```text deployment portal → Google sign-in → requested project access → setup continues ``` Use the [Google Cloud OAuth guide](/docs/deploying/google-cloud-oauth) when configuring a custom application. # How Alien Works (/docs/how-alien-works) Alien provides infrastructure for deploying managed software into your customer's cloud. You define your application once, Alien handles provisioning, updates, and monitoring across every customer environment. Let's walk through how it works, step by step. ## One codebase, any cloud [#one-codebase-any-cloud] You'll have customers on AWS, GCP, and Azure. Do you need to build separate integrations for each one? No. With Alien, you define the infrastructure your application needs once, in a file called `alien.ts`: ```typescript import * as alien from "@alienplatform/core" const data = new alien.Storage("data").build() const secrets = new alien.Vault("credentials").build() const api = new alien.Worker("api") .code({ type: "source", src: "./api", toolchain: { type: "typescript" } }) .link(data) .link(secrets) .publicEndpoint("api") .permissions("execution") .build() export default new alien.Stack("my-app") .add(api, "live") .add(data, "frozen") .add(secrets, "frozen") .build() ``` Alien translates each resource to the **native service** at deploy time: ``` ┏━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━┓ ┃ Worker ┃ ┃ Storage ┃ ┗━━━━━┯━━━━━━┛ ┗━━━━━┯━━━━━━┛ │ │ ├── AWS ───▶ Lambda ├── AWS ───▶ S3 ├── GCP ───▶ Cloud Run ├── GCP ───▶ Google Cloud Storage └── Azure ─▶ Container App └── Azure ─▶ Azure Blob Storage ``` The same applies to queues, vaults, KV stores, and networking. See [Stacks](/docs/stacks) for the full reference. Notice `.add(api, "live")` and `.add(data, "frozen")`. The lifecycle says who owns changes after setup: * **Frozen**: customer setup owns it. A normal rollout cannot create, change, replace, or delete it. * **Live**: Alien's Deployment Manager can create, update, replace, or remove it during a rollout. Learn more about [frozen and live resources](/docs/frozen-and-live). ## Your code [#your-code] Because your code runs inside the customer's network, it can reach things that aren't accessible from the outside: internal wikis, private databases, on-prem APIs. Here's an example: an API that crawls the customer's internal Confluence and caches pages in their cloud storage. An AI agent in your cloud calls this to build a knowledge base from internal docs. ```typescript import { Hono } from "hono" import { storage } from "@alienplatform/sdk" import { chromium } from "playwright" const app = new Hono() const pages = storage("pages") // S3 / Cloud Storage / Blob Storage // Crawl a page from the customer's internal wiki. // This URL is only reachable from inside their network. app.post("/crawl", async (c) => { const { url } = await c.req.json() const browser = await chromium.launch() const page = await browser.newPage() await page.goto(url) // e.g. https://wiki.corp.internal/page/123 const data = { title: await page.textContent("h1"), body: await page.textContent("#main-content"), } await browser.close() // Cache in the customer's own storage await pages.put(`${encodeURIComponent(url)}.json`, Buffer.from(JSON.stringify(data))) return c.json(data) }) // Retrieve a previously crawled page app.get("/pages/:key", async (c) => { const object = await pages.get(c.req.param("key")) return c.json(JSON.parse(object.data.toString("utf8"))) }) // Export the HTTP app — Alien's worker runtime serves it, no serve() call needed export default app ``` ```rust use alien_sdk::AlienContext; use axum::{Router, Json, extract::Path, routing::{get, post}}; use bytes::Bytes; use object_store::path::Path as ObjectPath; let ctx = AlienContext::from_env().await?; let pages = ctx.bindings().storage("pages").await?; // S3 / Cloud Storage / Blob Storage let app = Router::new() // Crawl a page from the customer's internal wiki. // This URL is only reachable from inside their network. .route("/crawl", post({ let pages = pages.clone(); move |Json(req): Json| async move { let browser = Browser::new(LaunchOptions::default())?; let tab = browser.new_tab()?; tab.navigate_to(&req.url)?; // e.g. https://wiki.corp.internal/page/123 let data = WikiPage { title: tab.find_element("h1")?.get_inner_text()?, body: tab.find_element("#main-content")?.get_inner_text()?, }; // Cache in the customer's own storage let key = ObjectPath::from(format!("{}.json", urlencoding::encode(&req.url))); pages .put(&key, Bytes::from(serde_json::to_vec(&data)?).into()) .await?; Ok::<_, Error>(Json(data)) } })) // Retrieve a previously crawled page .route("/pages/:key", get({ let pages = pages.clone(); move |Path(key): Path| async move { let raw = pages.get(&ObjectPath::from(key)).await?.bytes().await?; Ok::<_, Error>(Json(serde_json::from_slice::(&raw)?)) } })); // Serve the app and register its port with the worker runtime let listener = tokio::net::TcpListener::bind("0.0.0.0:0").await?; ctx.register_http_server(listener.local_addr()?.port()).await?; axum::serve(listener, app).await?; ``` You can also use the **native cloud SDKs** directly (e.g. `@aws-sdk/client-s3`) if you prefer. Alien injects the credentials and connection details either way. See [Accessing Resources](/docs/resource-apis). Build and test locally with `alien dev`. Alien provides local equivalents for every resource: storage on the filesystem, vaults in an embedded store, queues in memory. Same API, no cloud credentials needed. See [Local Development](/docs/local-development). ## The isolated area [#the-isolated-area] Now that we know what we're deploying, where does it actually go inside the customer's cloud? Their cloud account contains their own databases, services, networking, and storage. They don't want you anywhere near that. So your application gets its own **isolated area** within their account. Your resources live inside it. Everything outside is off-limits. ``` ╔═ Customer's Cloud Account ═════════════════════════════════╗ ║ ║░ ║ Their databases, services, networking, storage ║░ ║ ║░ ║ ┌─ Isolated Area (your application) ────────────────┐ ║░ ║ │ │ ║░ ║ │ ┏━━━━━━━━━━┓ ┏━━━━━━━━━━┓ ┏━━━━━━━━━━┓ │ ║░ ║ │ ┃ Worker ┃ ┃ Storage ┃ ┃ Queue ┃ │ ║░ ║ │ ┗━━━━━━━━━━┛ ┗━━━━━━━━━━┛ ┗━━━━━━━━━━┛ │ ║░ ║ │ │ ║░ ║ └───────────────────────────────────────────────────┘ ║░ ║ ║░ ╚════════════════════════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` What does "isolated area" mean concretely? It depends on the cloud: * **Azure**: a [resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal) * **GCP**: a dedicated [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects) * **AWS**: a set of resources that share a naming prefix (e.g. `acme-*`) See [Cloud Scoping](/docs/cloud-scoping) for details on each provider. Many teams skip isolation and deploy directly into the customer's existing VPCs or shared infrastructure. This is often easier from a sales perspective, and Alien does support it to a degree. But it usually hurts reliability. The customer's environment has its own network policies, security groups, resource quotas, and configuration quirks that you don't control. When something breaks, you can't tell if it's your code or their environment. An isolated area means **you control the environment your software runs in**. You define the networking, the IAM policies, the resource configuration. The customer controls the boundary. This is the setup that scales to hundreds of customers without each one becoming a unique snowflake. Your application can also connect to the customer's existing resources (a database, an internal API, a secret store) if they choose to allow it. ## Ongoing management [#ongoing-management] Now that we know what we're deploying and where it goes: how do you manage it? With traditional self-hosting, you hand the customer a Docker image or Helm chart. They run it themselves. Your code is in their cloud, but: * Every customer runs a **different version** * You have **no logs, no metrics, no visibility** * When something breaks, you schedule a call or wait days for a partial log dump * You can't push a fix without asking them to **re-deploy manually** With Alien, the customer **shares access** to the isolated area with your account. Think of sharing a Google Drive folder: they share a specific folder, you can manage what's inside, they can **revoke access at any time**. Not every customer will agree to share cross-account access. For those cases, there's an [Operator alternative](#the-operator-alternative) that doesn't require it. Within the shared area, what you can do is limited: * **Can:** update application code, monitor health, collect telemetry (sent outbound via OpenTelemetry) * **Cannot:** create or delete infrastructure, read customer data, access anything outside the area Alien automatically derives **least-privilege** permissions from your stack definition. The customer's security team only needs to review what's actually required. ### The Alien Deployment Manager [#the-alien-deployment-manager] The **Alien Deployment Manager** runs in your own cloud. It's the control plane that manages all customer deployments from one place — alien.dev provisions and operates it for you, or you can self-host. ``` ╔═ Your Cloud ══════════════════════╗ ╔═ Customer's Cloud ══════════════════╗ ║ ║ ║ ║░ ║ ┌── Alien Deployment Manager ──┐ ║ ║ Their databases, services, infra ║░ ║ │ │ ║ ║ ║░ ║ │ │ ║ ║ ┌─ Isolated Area ──────────────┐ ║░ ║ │ │ ║ cloud APIs ║ │ │ ║░ ║ │ Push updates ─────────┼─╬────────────────╬─▶│ ┏━━━━━━━━━━┓ │ ║░ ║ │ Collect telemetry ◀────────┼─╬────────────────╬──│ ┃ Worker ┃ │ ║░ ║ │ Worker commands ─────────┼─╬────────────────╬─▶│ ┗━━━━━━━━━━┛ │ ║░ ║ │ │ ║ ║ │ ┏━━━━━━━━━━┓ │ ║░ ║ │ │ ║ ║ │ ┃ Storage ┃ │ ║░ ║ └──────────────────────────────┘ ║ ║ │ ┗━━━━━━━━━━┛ │ ║░ ║ ║ ║ │ │ ║░ ╚═══════════════════════════════════╝ ║ └──────────────────────────────┘ ║░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ║ ║░ ╚═════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` The Deployment Manager manages resources by calling **cloud provider APIs** (e.g. [UpdateFunctionCode](https://docs.aws.amazon.com/lambda/latest/api/API_UpdateFunctionCode.html) on AWS). No network connection to the customer's environment is needed. ### How access works [#how-access-works] Every cloud provider has a built-in mechanism for granting scoped access: * **Azure**: [managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) * **GCP**: [service account impersonation](https://cloud.google.com/iam/docs/service-account-overview) * **AWS**: [cross-account IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_common-scenarios_aws-accounts.html) **No passwords or keys are exchanged.** The cloud provider handles the trust. See [Impersonation](/docs/impersonation) for details. This is the **push model**. It's the default for AWS, GCP, and Azure. ## The Operator alternative [#the-operator-alternative] Some security teams won't allow **cross-account access** at all. They don't want any external identity in their cloud, even a scoped one. For those environments, a lightweight **Operator** runs inside the customer's isolated area. It connects **outbound** to the Alien Deployment Manager, fetches releases, and deploys them locally. ``` ╔═ Your Cloud ══════════════════════╗ ╔═ Customer's Cloud ══════════════════╗ ║ ║ ║ ║░ ║ ┌── Alien Deployment Manager ──┐ ║ ║ Their databases, services, infra ║░ ║ │ │ ║ ║ ║░ ║ │ │ ║ HTTPS ║ ┌─ Isolated Area ──────────────┐ ║░ ║ │ │ ║ (outbound) ║ │ │ ║░ ║ │ Fetch releases ◀───────────┼─╬────────────────╬──│─── ┏━━━━━━━━━┓ │ ║░ ║ │ Send telemetry ◀───────────┼─╬────────────────╬──│─── ┃Operator ┃ │ ║░ ║ │ Fetch Worker work ◀─────────┼─╬────────────────╬──│─── ┗━━━━┯━━━━┛ │ ║░ ║ │ │ ║ ║ │ │ push │ ║░ ║ │ │ ║ ║ │ ┏━━━━▼━━━━━┓ │ ║░ ║ │ │ ║ ║ │ ┃ Worker ┃ │ ║░ ║ └──────────────────────────────┘ ║ ║ │ ┗━━━━━━━━━━┛ │ ║░ ║ ║ ║ │ ┏━━━━━━━━━━┓ │ ║░ ╚═══════════════════════════════════╝ ║ │ ┃ Storage ┃ │ ║░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ║ │ ┗━━━━━━━━━━┛ │ ║░ ║ └──────────────────────────────┘ ║░ No cross-account access. ║ ║░ No inbound ports. ╚═════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` The Deployment Manager **never touches the customer's cloud directly**. The Operator deploys using its own local credentials. The Operator polls for deployment state and releases. It also leases pending Worker commands and pushes them to the targeted Worker runtime inside the customer's environment, so the Worker never polls the command server. A Container or Daemon runs its own receiver from `@alienplatform/commands` (or `alien-commands` in Rust); the Operator does not run that receiver or its handlers. Each app-owned receiver can lease only its own targeted work. This is the **pull model**, used for on-prem or stricter security teams. Both models give you the same capabilities. Use push when possible. See [Deployment Models](/docs/deploying/deployment-models). ## When Alien didn't deploy it [#when-alien-didnt-deploy-it] Some teams already ship BYOC with Terraform, Pulumi, Helm, or raw manifests. For those environments, Alien can run in **Operate** mode instead of Build mode. Operate creates an observe-only deployment. The customer installs the Operator or grants read-only cloud inventory access, and Alien reports existing resources, health, and connectivity without reconciling a stack or changing workloads. See [Operate Existing Environments](/docs/operate). ## Initial setup [#initial-setup] Before the Deployment Manager can deploy the application, the customer completes setup once. You generate a **deployment link** for the customer and they open it in a browser. The link leads to a **white-labeled deployment portal** — branded for your project — with four ways to do the initial setup: * a **CLI** auto-generated and branded for your project (e.g. `acme-deploy`) * a **Terraform** module to drop into an existing workspace * a **CloudFormation** template for one-click launch on AWS * a **Helm** chart for installing into an existing Kubernetes cluster The admin picks whichever fits their workflow and runs it with their own cloud credentials. Whichever method they choose prepares the same deployment: * Frozen infrastructure owned by setup * The service identity that the Deployment Manager will use for ongoing management * The configuration needed to provision live resources The admin needs elevated permissions because they're creating resources that don't exist yet. After setup, the Deployment Manager uses its auto-derived permissions to create and reconcile live resources. Frozen resources remain owned by setup; changing them requires the customer to run setup again. Health checks and telemetry are separate features with their own permissions. ## Releases [#releases] ```bash alien release ``` Every active deployment picks up the new version: * **Push mode**: the Deployment Manager reconciles live resources immediately * **Pull mode**: the Operator picks it up on its next poll (\~30 seconds) **One command. Every customer updated. No coordination.** ## After deployment [#after-deployment] Once deployed, you have full visibility and control through the Deployment Manager. ### Telemetry [#telemetry] Logs, metrics, and traces flow back from every customer environment. Debug issues without asking the customer to send you anything. ### Remote commands [#remote-commands] Invoke code inside the customer's VPC. The command travels through the Deployment Manager. No inbound ports, no VPN: ```typescript import { command, vault } from "@alienplatform/sdk" import { Pool, type PoolConfig } from "pg" // Runs inside the customer's environment command("get-user-count", async () => { const config = await vault("credentials").getJson("database") const pool = new Pool(config) const { rows } = await pool.query("SELECT count(*) FROM users") return rows[0] }) ``` Your control plane targets the resource that registered the handler: ```typescript import { CommandsClient } from "@alienplatform/commands" const commands = new CommandsClient({ managerUrl, deploymentId, token }) const result = await commands.target("customer-tools").invoke("get-user-count", {}) ``` ```bash alien commands invoke \ --deployment acme-corp \ --command get-user-count \ --params '{}' ``` This is how AI agents execute tool calls, data connectors run queries, and security scanners report results. Workers always receive pushes from the Manager or in-environment Operator; app-owned Container and Daemon receivers lease their own targeted commands over outbound HTTPS. See [Remote Commands](/docs/commands). ### Events [#events] React to things happening inside the customer's environment: ```typescript onQueueMessage("tasks", async (msg) => { // A message arrived in the customer's queue }) onStorageEvent("uploads", async (event) => { // A file was uploaded to the customer's bucket }) onCronEvent("nightly", async () => { // Runs on schedule }) ``` See [Events & Triggers](/docs/infrastructure/worker/events-and-triggers). ## What's next [#whats-next] # Impersonation (/docs/impersonation) Alien needs to manage your application inside the customer's cloud — push updates, read logs, run commands. But no one is going to give Alien their cloud password or an `AWS_ACCESS_KEY`. Instead, the customer creates a service identity inside their cloud. This identity exists only to manage your application's [isolated area](/docs/cloud-scoping). The customer decides what it can do and configures it to trust Alien. Alien then impersonates this identity whenever it needs to manage your application. This page explains how that works on each cloud provider. ## Azure — Managed Identity [#azure--managed-identity] The customer creates a [User-Assigned Managed Identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) (UAMI) inside the deployment's resource group. The identity is configured with: * **A custom role** scoped to the resource group — defines what Alien can do (update code, read logs, monitor health). * **A federated credential** — allows Alien to authenticate via [OIDC](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation). Alien proves its own identity to Azure, Azure checks the federation config, and grants access. No secrets are exchanged. ``` ╔═ Alien ═══════════════╗ ╔═ Customer's Azure ════════════════════╗ ║ ║ ║ ║░ ║ Alien's own identity ║ OIDC token ║ ┌─ Managed Identity ─────────────┐ ║░ ║ (workload identity) ╠───────────────▶║ │ │ ║░ ║ ║ ║ │ Trusts: Alien (via OIDC) │ ║░ ║ Azure verifies the ║ access ║ │ Can: custom role on rg-* │ ║░ ║ token, grants access ║◀───────────────║ │ │ ║░ ║ ║ ║ └────────────────────────────────┘ ║░ ║ ║ ║ ║░ ╚═══════════════════════╝ ╚═══════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ## GCP — Service Account Impersonation [#gcp--service-account-impersonation] The customer creates a [service account](https://cloud.google.com/iam/docs/service-account-overview) in the deployment's project. The identity is configured with: * **Custom IAM roles** generated from Alien's permission sets — scoped to the project or resource where GCP supports it, with IAM Conditions for prefix/resource limits where needed. * **Impersonation grant** — the customer grants Alien's own service account the `roles/iam.serviceAccountTokenCreator` role on the deployment's service account. This lets Alien request short-lived tokens to act as the deployment's identity. ``` ╔═ Alien ═══════════════╗ ╔═ Customer's GCP ══════════════════════╗ ║ ║ ║ ║░ ║ Alien's own service ║ generate ║ ┌─ Service Account ──────────────┐ ║░ ║ account ╠── token ──────▶║ │ │ ║░ ║ ║ (as this SA) ║ │ Trusts: Alien's SA │ ║░ ║ GCP checks ║ ║ │ Can: scoped custom roles │ ║░ ║ impersonation grant ║ short-lived ║ │ │ ║░ ║ ║◀── token ──────║ └────────────────────────────────┘ ║░ ║ ║ ║ ║░ ╚═══════════════════════╝ ╚═══════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ## AWS — Cross-Account IAM Role [#aws--cross-account-iam-role] The customer creates an [IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_common-scenarios_aws-accounts.html) in their account. The role has two parts: * **Trust policy** — specifies which external account (Alien's AWS account) is allowed to assume this role. This is how the customer says "I trust Alien." * **Permission policy** — defines what the role can do, scoped to resources matching the deployment's [prefix](/docs/cloud-scoping#aws--resource-prefix). When Alien needs to manage the deployment, it calls `sts:AssumeRole` — AWS checks the trust policy, and if it matches, returns short-lived credentials (access key + secret key + session token) that expire after one hour. Alien uses these credentials to make API calls as if it were the customer's own role. ``` ╔═ Alien ═══════════════╗ ╔═ Customer's AWS ══════════════════════╗ ║ ║ ║ ║░ ║ Alien's own IAM role ║ AssumeRole ║ ┌─ Cross-Account IAM Role ───────┐ ║░ ║ ╠───────────────▶║ │ │ ║░ ║ ║ ║ │ Trusts: Alien's account │ ║░ ║ AWS checks trust ║ short-lived ║ │ Can: prefix-scoped actions │ ║░ ║ policy, grants creds ║◀── creds ──────║ │ │ ║░ ║ ║ (1 hour) ║ └────────────────────────────────┘ ║░ ║ ║ ║ ║░ ╚═══════════════════════╝ ╚═══════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ## What they share [#what-they-share] All three mechanisms achieve the same thing: 1. **The customer creates an identity** in their cloud with limited permissions. 2. **The identity trusts Alien** — configured explicitly, not by sharing a secret. 3. **Alien proves who it is** to the cloud provider and gets short-lived credentials to act as that identity. 4. **The customer can revoke access** at any time by deleting the identity or removing the trust. No passwords, API keys, or long-lived secrets cross between the two parties. The cloud provider sits in the middle and brokers the trust. Alien sets all of this up automatically during [initial setup](/docs/deploying/onboarding-customers). You define the stack; Alien generates the identity, trust configuration, and permission policies for each cloud. # Welcome (/docs) Alien provides infrastructure for deploying managed software into your customers' cloud accounts. AWS, GCP, Azure, Kubernetes, or restricted environments. Your code needs to run in the customer's environment when sensitive data can't leave, you need access to services behind their firewall, or their security team requires it. The usual answer is self-hosting — but then you lose control. No auto-updates, no logs, every customer on a different version. With Alien, your software runs in their environment while your team keeps the operating loop: releases, telemetry, remote commands, rollback, and revocation. Your handlers and telemetry configuration determine what leaves the environment; Alien does not infer which application data is sensitive. # Stack inputs (/docs/inputs) Most deployments need a few values that aren't known until setup: a database URL inside the customer's network, an API key for your control plane, a log level. **Stack inputs** let you declare those values in `alien.ts`, say who provides each one, and Alien handles collecting and validating them everywhere setup happens. In BYOC, setup has two sides. Some values are yours, like a control-plane key. Some belong to the customer's admin, like an endpoint that only exists inside their network. If you hardcode values or pass ad hoc env flags, the deployment portal, generated IaC, and white-labeled CLI don't know those values exist, so an admin can follow setup and still end up with a broken deployment. Stack inputs fix that: one declaration drives every surface. ## Declaring inputs [#declaring-inputs] Declare a set with `alien.inputs(...)` and attach it to your stack with `.inputs()`. Each key is the input's id. ```ts import * as alien from "@alienplatform/core" const inputs = alien.inputs({ databaseUrl: alien.string({ providedBy: "deployer", required: true, label: "Database URL", description: "Postgres connection string inside the customer's network.", pattern: "^postgres://", env: "DATABASE_URL", }), controlPlaneApiKey: alien.secret({ providedBy: "developer", required: true, label: "Control plane API key", description: "Authorizes this deployment with your control plane.", env: { name: "CONTROL_PLANE_API_KEY", targetResources: ["api"] }, }), logLevel: alien.enum(["debug", "info", "warn", "error"], { providedBy: "deployer", required: false, label: "Log level", description: "Verbosity for the deployment's logs.", default: "info", env: "LOG_LEVEL", }), }) export default new alien.Stack("acme-app").inputs(inputs).build() ``` Input ids are normal identifiers (letters, digits, underscores) and can't be all-caps env-var style. Every input needs a `label`, and a `description` is required whenever `required` is `true`. ## Who provides a value [#who-provides-a-value] `providedBy` decides which side is asked for a value, and on which surfaces it appears. | `providedBy` | Collected on | Notes | | --------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `"developer"` | Your surfaces: `alien onboard`, the dashboard generate-link flow, deployment-group APIs | Never shown to the customer and never emitted into generated IaC or the portal | | `"deployer"` | The customer's surfaces: deployment portal, generated CloudFormation, Terraform, Helm, and the white-labeled CLI | Asked of the customer's admin at install time | | `["developer", "deployer"]` | Both | You can prefill it; if you don't, the admin is asked at install time | This is what keeps your secrets on your side: a `developer` input is collected only where you control it, so it never reaches the customer or lands in anything they can read. ## Input types [#input-types] ```ts alien.string({ ... }) // free text alien.secret({ ... }) // masked, encrypted, no default alien.number({ ... }) // any number alien.integer({ ... }) // whole number alien.boolean({ ... }) // true / false alien.enum(["a", "b"], { ... }) // one of a fixed set alien.stringList({ ... }) // list of strings ``` Every type takes the common options `providedBy`, `required`, `label`, `description`, and optionally `placeholder`, `default` (not for `secret`), `platforms`, and `env`. Use `platforms` to scope an input to specific targets, so a local-only value isn't requested for an AWS-only deployment link. ## Validation [#validation] Validation is a small portable set, declared right on the input, so it's enforced identically in TypeScript, the deployment portal, CloudFormation, and Terraform. | Type | Options | | ------------------- | --------------------------------------------- | | `string`, `secret` | `minLength`, `maxLength`, `pattern`, `format` | | `number`, `integer` | `min`, `max` | | `enum` | the allowed `values` | | `stringList` | `minItems`, `maxItems` | `pattern` is matched against the whole value. Keep it portable: no regex delimiters, backreferences, or lookarounds, since the same pattern has to run in CloudFormation rules and Terraform validation blocks, not just JavaScript. ```ts webhookUrl: alien.string({ providedBy: "deployer", required: true, label: "Webhook URL", description: "HTTPS endpoint the deployment calls back.", pattern: "^https://", maxLength: 2048, env: "WEBHOOK_URL", }), ``` ## Environment variables [#environment-variables] `env` maps an input into the deployment's runtime environment. Pass a variable name, or an object to target specific resources: ```ts env: "DATABASE_URL" env: { name: "DATABASE_URL", targetResources: ["api", "worker"] } ``` Variable names are upper snake case. By default an input is available to every resource; `targetResources` narrows it to the listed ones. ## Secrets [#secrets] `alien.secret` values are masked in the dashboard and portal, encrypted at rest, and kept out of generated IaC state, logs, and launch URLs. They can't declare a `default`, and they flow through Alien's runtime secret vault rather than plain environment snapshots. ## Gating resources [#gating-resources] A boolean deployer input can decide whether a resource exists at all. Call `.enabled(input)` on the resource builder — every resource type supports it, except the reserved `secrets` vault and infrastructure Alien derives from the stack itself (networks, service accounts, registries): ```ts const io = alien.inputs({ analyticsEnabled: alien.boolean({ providedBy: "deployer", required: false, label: "Enable analytics", description: "Provision the analytics store.", default: false, }), }) const analytics = new alien.Kv("analytics").enabled(io.analyticsEnabled).build() ``` A deployer who answers no never gets the resource: it isn't provisioned, and it's absent from the generated IaC and IAM policies. What happens on later edits depends on the resource's [frozen or live](/docs/frozen-and-live) mode: * **Frozen** — the answer is fixed when the deployment is created and can't change afterwards. * **Live** — the resource follows later edits to the input. Turning it off deletes the resource together with its data; turning it back on recreates it empty. The input must be a deployer-provided boolean, and it must be required or declare a default. Compute resources (Workers, Containers, Daemons) can be gated too: declining a live workload follows the same removal path as deleting it from a release, and accepting later brings it back. ## What's next [#whats-next] # Local Development (/docs/local-development) Run your entire stack locally with one command. ## Starting a Dev Session [#starting-a-dev-session] ```bash alien dev ``` This is the all-in-one local workflow. It starts a local server, builds your app, and deploys it. Every cloud resource gets a local equivalent: | Cloud Resource | Local Equivalent | | ------------------------------------ | --------------------------------------------- | | S3 / Cloud Storage / Blob Storage | Filesystem | | DynamoDB / Firestore / Table Storage | SQLite (embedded database) | | SQS / Pub/Sub / Service Bus | SQLite (persistent queue) | | Workers / Daemons | Native process extracted from the built image | | Containers | Docker container | | Postgres | Embedded native Postgres | | Vault | Plaintext local files | No cloud credentials needed. No accounts. Docker is required only for builds and Container resources. ## Server-Only Mode [#server-only-mode] If you want just the local server without the full build-deploy-watch cycle: ```bash alien dev server ``` Then drive it manually with explicit commands: ```bash alien dev release alien dev deploy --name preview alien dev deployments ls ``` This is useful when you want more control over the build-release-deploy cycle, or when integrating with other tools. ## Agent-Friendly [#agent-friendly] For AI agents and automated tooling that need to read state programmatically: ```bash alien dev --status-file .alien/dev-status.json ``` The status file is a JSON document that updates as the dev session progresses — no terminal output parsing required. Agents can poll this file to know when resources are ready, get endpoint URLs, and monitor health. # Networking (/docs/networking) Networking is configured at deploy time, not in `alien.ts`. The CLI exposes this as `--network` flags, and setup files carry the same choice as `StackSettings.network`. Alien turns those settings into the generated [Network resource](/docs/infrastructure/network) in stack state. Use this page for deployment commands. Use [Network API Reference](/docs/infrastructure/network/api), [Behavior & Limits](/docs/infrastructure/network/behavior), and [Pricing](/docs/infrastructure/network/pricing) for the full Network reference. ## Modes [#modes] ### Auto (default) [#auto-default] ```bash alien deploy --name acme --platform aws ``` The system decides. If your stack has resources that need cloud networking, such as containers, Alien creates the default network shape required by the target platform. Otherwise, no deployment network resource is created. ### Use Default [#use-default] ```bash alien deploy --name acme --platform aws --network use-default ``` Alien uses the cloud provider's default network where the provider has one. Azure has no default VNet, so `use-default` creates VNet infrastructure for the deployment. Good for development and testing. Not recommended for production. ### Create [#create] ```bash alien deploy --name acme --platform aws --network create ``` Alien creates an isolated VPC with private subnets and a managed NAT gateway. VMs use private IPs only — all outbound traffic routes through NAT. **Recommended for production.** The CIDR block is auto-generated from the stack ID to reduce conflicts, or you can specify one: ```bash alien deploy --name acme --platform aws \ --network create \ --network-cidr 10.42.0.0/16 \ --availability-zones 3 ``` ### Bring Your Own VPC [#bring-your-own-vpc] Use an existing VPC/VNet. Alien stores and validates the references but creates no network infrastructure. The customer handles routing, egress, subnet layout, and security posture. ```bash alien deploy --name acme --platform aws \ --network byo \ --vpc-id vpc-0abc123 \ --public-subnet-ids subnet-pub1,subnet-pub2 \ --private-subnet-ids subnet-priv1,subnet-priv2 \ --security-group-ids sg-0abc123 ``` ```bash alien deploy --name acme --platform gcp \ --network byo \ --network-name my-vpc \ --subnet-name my-subnet \ --network-region us-central1 ``` ```bash alien deploy --name acme --platform azure \ --network byo \ --vnet-resource-id /subscriptions/.../vnet \ --public-subnet-name pub-subnet \ --private-subnet-name priv-subnet ``` BYO VPC is supported on AWS, GCP, and Azure only. ## Flags Reference [#flags-reference] | Flag | Mode | Description | | ------------------------------ | ----------- | --------------------------------------------- | | `--network ` | all | `auto`, `use-default`, `create`, or `byo` | | `--network-cidr ` | create | VPC CIDR block (auto-generated if omitted) | | `--availability-zones ` | create | Number of AZs (default: 2) | | `--vpc-id ` | byo (AWS) | Existing VPC ID | | `--public-subnet-ids ` | byo (AWS) | Comma-separated public subnet IDs | | `--private-subnet-ids ` | byo (AWS) | Comma-separated private subnet IDs | | `--security-group-ids ` | byo (AWS) | Comma-separated security group IDs (optional) | | `--network-name ` | byo (GCP) | Existing VPC network name | | `--subnet-name ` | byo (GCP) | Subnet name | | `--network-region ` | byo (GCP) | Subnet region | | `--vnet-resource-id ` | byo (Azure) | Existing VNet resource ID | | `--public-subnet-name ` | byo (Azure) | Public subnet name | | `--private-subnet-name ` | byo (Azure) | Private subnet name | # Patterns (/docs/patterns) Most Alien use cases follow one of three patterns. The pattern determines how work is split between your environment and the customer's environment. ## Remote Command Target [#remote-command-target] A command-enabled Worker, Container, or Daemon runs in the customer's environment. Your control plane targets that resource by name, the application executes an explicit handler, and the result comes back through Alien. ``` ┌─────────────────────────┐ ┌─────────────────────────┐ │ Your Cloud │ │ Customer Environment │ │ │ │ │ │ ┌─────────────────┐ │ │ ┌─────────────┐ │ │ │ Control Plane │────┼─command─┼───▶│ Target │ │ │ │ │◀───┼─result──┼────│ Worker / │ │ │ └─────────────────┘ │ │ │ Container / │ │ │ │ │ │ │ Daemon │ │ │ │ │ │ └──────┬──────┘ │ │ │ │ │ │ │ │ ┌────────▼────────┐ │ │ ┌──────▼──────┐ │ │ │ Dashboard │ │ │ │ Private │ │ │ │ Processing │ │ │ │ Resources │ │ │ └─────────────────┘ │ │ └─────────────┘ │ └─────────────────────────┘ └─────────────────────────┘ ``` Most logic lives in your cloud. The command target provides access to private resources the outside world cannot reach — databases behind VPCs, Active Directory, internal APIs, or local AI models. **Use cases:** AI workers (tool calls in customer's VPC), data connectors, security scanners, browser automation for internal apps (Jira Data Center, SAP, GitLab), cloud remediation agents. **Typical implementation:** use a `Worker` with `command()` handlers for bounded, stateless work that can scale to zero. Use a `Container` for a service process, or a `Daemon` when one receiver must run on every eligible machine. Containers and Daemons run their own receiver from `@alienplatform/commands`. ```typescript import { command, storage } from "@alienplatform/sdk" const ws = storage("workspace") command("get-active-users", async ({ limit }: { limit: number }) => { const pool = new Pool(await getCustomerCredentials()) const { rows } = await pool.query( "SELECT id, name FROM users WHERE active = true LIMIT $1", [limit ?? 100] ) return { rows, count: rows.length } }) command("read-file", async ({ path }: { path: string }) => { const object = await ws.get(path) return object.data.toString("utf8") }) ``` The sender names the resource explicitly: ```typescript import { CommandsClient } from "@alienplatform/commands" const commands = new CommandsClient({ managerUrl, deploymentId, token }) const result = await commands.target("customer-tools").invoke("get-active-users", { limit: 100, }) ``` ## Control Plane / Data Plane [#control-plane--data-plane] You run the control plane. A stateful data plane runs in the customer's environment with persistent storage and compute. ``` ┌─────────────────────────┐ ┌─────────────────────────┐ │ Your Cloud │ │ Customer Environment │ │ │ │ │ │ ┌─────────────────┐ │ │ ┌─────────────┐ │ │ │ Control Plane │────┼─updates─┼───▶│ Data Plane │ │ │ │ - Lifecycle │ │ │ │ - Storage │ │ │ │ - Updates │◀───┼telemetry┼────│ - Compute │ │ │ │ - Monitoring │ │ │ │ - Database │ │ │ └─────────────────┘ │ │ └─────────────┘ │ └─────────────────────────┘ └─────────────────────────┘ ``` The data plane is the product. It handles the actual workload and stores data. Your control plane manages the lifecycle — ships updates, monitors health, handles incidents. **Use cases:** managed databases (BYOC), AI gateways, observability platforms, any product where the compute/storage component runs in the customer's cloud. **Typical implementation:** Alien `Container` resources with persistent storage, autoscaling, and internal networking. ## Full App [#full-app] A complete application runs in the customer's environment. You ship updates and monitor, but all data and logic stays remote. ``` ┌─────────────────────────┐ ┌─────────────────────────┐ │ Your Cloud │ │ Customer Environment │ │ │ │ │ │ ┌─────────────────┐ │ │ ┌─────────────┐ │ │ │ Ship Updates │────┼─────────┼───▶│ App │ │ │ └─────────────────┘ │ │ │ - API │ │ │ │ │ │ - Workers │ │ │ ┌─────────────────┐ │ │ │ - Queue │ │ │ │ Monitor │◀───┼─────────┼────└──────┬──────┘ │ │ │ (logs/metrics) │ │ │ │ │ │ └─────────────────┘ │ │ ┌──────▼──────┐ │ │ │ │ │ Storage │ │ │ │ │ │ Database │ │ │ │ │ └─────────────┘ │ └─────────────────────────┘ └─────────────────────────┘ ``` The app is self-contained. Multiple workers, storage, queues, databases — all inside the customer's environment. You ship updates and have full visibility, but no data leaves. **Use cases:** enterprise SaaS with self-hosted requirements, AI agents that need extensive private data access, internal tools deployed across many customer environments, cloud action platforms with HTTP APIs and event triggers. **Typical implementation:** multiple Alien resources — `Worker`, `Container`, `Storage`, `Kv`, `Queue` — working together. ## Choosing a pattern [#choosing-a-pattern] | | Remote Command Target | Control / Data Plane | Full App | | --------------------------------- | ------------------------------ | -------------------- | ---------------------- | | Where is most logic? | Your cloud | Split | Customer's environment | | Is the remote component stateful? | Usually no; optional | Yes | Yes | | Primary purpose? | Execute commands | Run infrastructure | Run application | | Typical Alien resources | 1 Worker, Container, or Daemon | Multiple Containers | Multiple of any type | **Decision:** * Need persistent storage or heavy compute in the customer's environment? → **Control Plane / Data Plane** * Running a full application (API, workers, storage)? → **Full App** * Just need access to private resources? → **Remote Command Target** ## What all patterns share [#what-all-patterns-share] * **No inbound control-plane access** — cloud-provider delivery or the in-environment Operator pushes to Workers; app-owned Container/Daemon receivers lease targeted work over outbound HTTPS. No VPN. * **Release control** — `alien release` records a target release, and eligible deployments converge according to their policy. * **Scoped telemetry** — logs, metrics, and traces are collected per deployment under the telemetry contract the customer approved. * **Multi-cloud** — the same stack can target AWS, GCP, Azure, Kubernetes, or restricted environments. # Permissions (/docs/permissions) Alien derives three layers of permissions from your stack definition: 1. **Provisioning** — Creates resources during initial setup. The customer's admin runs this once. Alien never holds these. 2. **Management** — What Alien uses to reconcile deployments. [Live resources](/docs/frozen-and-live) receive ongoing provisioning permissions; frozen resources remain owned by setup. Optional features can add narrower permissions for health checks, telemetry, Commands, or Remote Bindings. 3. **Application runtime** — What your deployed code can access. Controlled by permission profiles. **This page covers this layer.** Resources are isolated by default. A worker can't read from storage, write to a queue, or access a vault unless you explicitly grant it access. This is enforced at the cloud level — IAM policies on AWS, service account bindings on GCP, role assignments on Azure. ## Permission Profiles [#permission-profiles] A permission profile defines what a worker can access. Define them in your stack: ```typescript export default new alien.Stack("my-app") .add(data, "frozen") .add(cache, "frozen") .add(api, "live") .permissions({ profiles: { execution: { data: ["storage/data-read", "storage/data-write"], cache: ["kv/data-read", "kv/data-write"], }, }, }) .build() ``` Then assign it to a worker: ```typescript const api = new alien.Worker("api") .permissions("execution") .build() ``` Under the hood, each profile becomes a cloud identity: * **AWS**: IAM Role * **GCP**: Service Account * **Azure**: User-assigned Managed Identity ## Scopes [#scopes] The keys inside a profile control the scope: ```typescript profiles: { execution: { "*": ["storage/data-read"], // all storage in the stack "logs-storage": ["storage/data-write"] // only this specific bucket } } ``` * `"*"` — **stack-level**: applies to all resources of that type with the stack prefix * `"resource-name"` — **resource-scoped**: applies to only that specific resource ## Permission Sets [#permission-sets] Permission sets are named bundles of cloud permissions that work across all platforms: | Resource | Available Sets | | -------- | --------------------------- | | Storage | `data-read`, `data-write` | | KV | `data-read`, `data-write` | | Queue | `data-read`, `data-write` | | Vault | `data-read`, `data-write` | | Worker | `execute`, `invoke` | | AI | `invoke` | | Postgres | `data-access` | | Email | `send`, `manage-identities` | Each set maps to specific cloud actions. For example, `storage/data-read` translates to: * **AWS**: `s3:GetObject`, `s3:GetObjectVersion`, `s3:ListBucket` * **GCP**: `storage.objects.get`, `storage.objects.list`, `storage.buckets.get` * **Azure**: `Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read` You don't need to know the cloud-specific actions — Alien handles the translation. ## How It Works [#how-it-works] When you deploy, Alien: 1. Creates a cloud identity for each profile (IAM Role, Service Account, Managed Identity) 2. Generates a permission policy from the declared permission sets 3. Attaches the policy to the identity 4. Configures workers to run with that identity For resource-scoped permissions, each resource controller applies additional policies after the resource is created — scoped to the exact resource ARN, GCS bucket, or Azure resource ID. ## Least-Privilege by Default [#least-privilege-by-default] Nothing is granted unless declared. A worker with the `execution` profile can only do what the profile explicitly allows. If you don't grant `storage/data-write`, writes will fail with a permission error at runtime. This matters especially when deploying to customer environments. The customer's security team can audit exactly what your software is allowed to do — and verify it matches what you've declared. The same least-privilege principle applies to management permissions. Alien auto-derives provisioning permissions for [live resources](/docs/frozen-and-live), including the actions required to create, update, replace, or remove them. Frozen resources do not receive those permissions. Health checks, telemetry, Commands, and other enabled features are derived separately and may add narrower access to a specific resource. Build management can read build metadata such as build status and history. Log and artifact access is modeled separately through explicit build log/artifact permissions. Sensitive management access must be explicit. Some grants are written by preflights: Alien creates one internal vault named `secrets` for deployment secret environment variables, and the secrets preflight adds `vault/data-read` / `vault/data-write` on that concrete vault so deployment secrets can be synced. When commands are enabled, the commands preflight adds `worker/dispatch-command` on the concrete worker. User-declared vaults remain customer-managed unless you opt in: ```typescript .permissions({ management: { extend: { "customer-secrets": ["vault/data-write"], }, }, }) ``` Grant `vault/data-read` only when the management identity must read secret values. Grant `vault/data-write` only when it must write secret values. On GCP, Alien generates custom IAM roles from the exact permissions declared in the permission sets, so the resulting roles and any IAM Conditions are visible in the reviewed role plan. ## Custom Permission Sets [#custom-permission-sets] For edge cases not covered by built-in sets, define inline permission sets: ```typescript const assumeAnyRole: PermissionSet = { id: "assume-any-role", platforms: { aws: [{ grant: { actions: ["sts:AssumeRole"] }, binding: { stack: { resources: ["*"], condition: { StringEquals: { "sts:ExternalId": "my-ext-id" } } } } }] } } .permissions({ profiles: { execution: { "*": ["storage/data-read", assumeAnyRole], } } }) ``` ## Platform Differences [#platform-differences] | Aspect | AWS | GCP | Azure | | -------------- | ------------- | ----------------------------- | --------------------------- | | Identity | IAM Role | Service Account | Managed Identity | | Stack scope | ARN wildcards | Resource-level IAM | Resource group scope | | Resource scope | Specific ARN | `setIamPolicy` on resource | Role assignment on resource | | Cross-account | AssumeRole | Service account impersonation | OIDC via Federated Identity | # Private manager (/docs/private-manager) Private manager moves the manager’s operational state and telemetry into infrastructure you operate. Hosted Alien can still participate in project, release, setup, and fleet workflows. Start with [Private manager configuration](/docs/self-hosting/configuration). Generate the current standalone template with `alien serve --init` and treat that generated file as the source of truth. ```bash alien serve --init alien serve --config alien-manager.toml ``` # Releases (/docs/releases) A release is an immutable snapshot of your built app — compiled code, packaged images, and stack metadata. A channel selects which release a set of deployments should follow. Every project starts with a `production` channel, preserving the default behavior: `alien release` advances production and production deployments update automatically. Releases view in the dashboard — version history with rollout status and the resources packaged in each release ## Build [#build] ```bash alien build --platform aws ``` This compiles your stack for the specified platform. Alien reads `alien.ts`, runs the appropriate toolchains (TypeScript, Rust, etc.), and produces build outputs. You can build for multiple platforms at once: ```bash alien build --platforms aws,gcp ``` ### Build steps [#build-steps] 1. **Read the stack** — Alien loads `alien.ts` and resolves all resources and their connections. 2. **Run toolchains** — each worker's code is compiled using the configured toolchain (TypeScript, Rust, etc.). 3. **Package outputs** — build outputs are packaged into container images or platform-specific runtime bundles. 4. **Write metadata** — the stack definition, resource graph, and image references are recorded. ### Content-hash dedup [#content-hash-dedup] Every build produces a content hash of the output. If your code hasn't changed, the build reuses the previous output instantly — no recompilation needed. This makes repeated builds and releases fast. ### Supported platforms [#supported-platforms] | Platform | What it targets | | -------- | ------------------------------------------- | | `aws` | Lambda, S3, DynamoDB, IAM | | `gcp` | Cloud Run, Cloud Storage, Firestore | | `azure` | Container Apps, Blob Storage, Table Storage | \| `kubernetes` | Kubernetes Pods and Services | \| `local` | Docker containers on the local machine | ## Release [#release] ```bash alien release ``` This builds your code, pushes images to the registry, and creates a release on the manager. Alien auto-discovers which platforms to release from your manager's artifact registry configuration. Use `--platforms` to override auto-discovery: ```bash alien release --platforms aws alien release --platforms aws,gcp ``` ### Test a release without updating production [#test-a-release-without-updating-production] Create a long-lived channel such as `staging`, configure the test deployment to follow it, then release directly to that channel: ```bash alien release --channel staging ``` Production stays on its current release. A later normal `alien release` advances only `production`, so it cannot overwrite staging. When the tested release is ready, promote the exact immutable release instead of rebuilding it: ```bash alien releases promote rel_… --channel production ``` Promotion uses the same artifacts that were tested. Channels are pointers, not copies or draft releases; the releases page labels which channels currently point at each release. A pinned deployment keeps running its pinned release even when its channel advances. Changing its channel changes where it resumes after it is unpinned. ### What happens during release [#what-happens-during-release] 1. **Discover platforms** — Alien queries the manager for configured platforms (or uses `--platforms` if specified). 2. **Build** — Alien rebuilds for every platform to ensure the release reflects your latest code. Content-hash dedup makes this instant when nothing changed. 3. **Push** — Images are pushed to the configured registry (embedded or external). If the same images were already pushed in a prior release, the push is skipped automatically. 4. **Record** — Alien records the release metadata (stack definition, image references, platform) on the manager. 5. **Update** — Deployments following the selected channel detect the new release and update automatically. Pinned deployments do not move. ### Using `--prebuilt` [#using---prebuilt] For CI/CD pipelines where build and push happen in separate steps, use `--prebuilt` to skip both: ```bash # In your CI build step: alien build --platforms aws,gcp # Push images to your registry externally... # In your CI release step: alien release --prebuilt ``` This requires that `stack.json` already contains remote image URIs (not local paths). ## Multi-Platform Releases [#multi-platform-releases] Each platform gets its own build output and deployment metadata, but the `alien.ts` manifest is the same. Workers, containers, and supporting infrastructure are packaged in the format Alien needs to update each deployment. Build and release multiple platforms in one command: ```bash alien build --platforms aws,gcp,azure alien release --platforms aws,gcp,azure ``` Configure multiple artifact registries in your `alien-manager.toml` and `alien release` handles them all in a single command. ## How Updates Propagate [#how-updates-propagate] Once a release is pushed, Alien's deployment loop moves each eligible deployment toward the recorded target: * **Push model**: the manager calls cloud APIs to update workers and services directly. Updates are immediate. * **Pull model**: the Operator polls for target releases (\~30 seconds) and applies updates locally. * **Gated or disconnected deployments**: deployments that require approval, are pinned, or use airgapped sync do not update until their deployment policy allows it. The dashboard shows current version, target version, rollout state, last heartbeat, and failures. Each release has a detail page showing rollout status across every deployment: which are updated, updating, failed, still pending, pinned to another release, or superseded by a newer one. It includes per-deployment rollout durations, deployment group filters, an activity timeline of release events, and the stack contents packaged in the release. The page updates live while a rollout is in flight, so pushing a release and watching it land is one flow. Healthy deployments converge automatically; blocked deployments stay visible instead of becoming hidden version drift. To roll back, promote an earlier release back to the channel — `alien releases promote --channel production` — or pin an individual deployment to a known-good release from the CLI or the release page. Promotion reuses the exact immutable artifacts; nothing is rebuilt. A failed rollout can also roll forward: fix the problem and release again, and deployments converge through normal desired-state reconciliation. # Remote bindings (/docs/remote-bindings) Remote bindings let your hosted backend use a published Storage, Key, or AI resource without deploying that backend into the customer environment. ```text hosted backend → short-lived binding → published customer resource ``` ## Publish only the resource you need [#publish-only-the-resource-you-need] ```typescript title="alien.ts" import * as alien from "@alienplatform/core" const uploads = new alien.Storage("uploads").build() export default new alien.Stack("customer-storage") .add(uploads, "frozen", { remoteAccess: true }) .build() ``` `remoteAccess: true` publishes that resource to the remote-binding registry. It does not publish every resource in the stack. ## Connect from your backend [#connect-from-your-backend] Use a project API key created for Remote Bindings: ```bash alien api-keys create --for remote-bindings ``` Then resolve the customer by the same external ID used during setup: ```typescript import { Bindings } from "@alienplatform/bindings" const bindings = await Bindings.forRemoteCustomer({ project: "my-project", externalId: customer.id, token: process.env.ALIEN_API_TOKEN!, }) const uploads = bindings.storage("uploads") await uploads.put("report.txt", Buffer.from("hello")) const result = await uploads.get("report.txt") console.log(result.data.toString("utf8")) ``` You can also address a known deployment with `Bindings.forRemoteDeployment({ deploymentId, token })`. Remote Storage exposes `get`, `put`, `delete`, `list`, and `head`. Remote Key exposes `encrypt` and `decrypt`. AI returns a short-lived provider configuration through `bindings.ai()`. The operation still crosses environments. Review the request and response data exactly as you would for any hosted API integration. See the complete [Resource APIs](/docs/resource-apis) and the runnable [customer storage example](/docs/examples/customer-storage). # Resource APIs (/docs/resource-apis) Your [stack](/docs/stacks) defines resources like storage buckets, queues, and vaults. To use them in your application code, link them to your worker and import the SDK. **1. Link resources to your worker** ```typescript title="alien.ts" const data = new alien.Storage("data").build() const api = new alien.Worker("api") .code({ type: "source", src: "./src", toolchain: { type: "typescript" } }) .link(data) .permissions("execution") .build() ``` **2. Access them in your code** ```typescript import { storage } from "@alienplatform/sdk" const store = storage("data") // same name as in alien.ts await store.put("reports/q1.json", Buffer.from(JSON.stringify(report))) ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let store = bindings.storage("data").await?; // same name as in alien.ts store.put(&"reports/q1.json".into(), bytes).await?; ``` The binding factories come straight from the SDK — no runtime, no connection setup. Constructing a handle does no I/O; the first operation on it resolves the resource and reuses it after that. Credentials are injected automatically — IAM roles on AWS, Workload Identity on GCP, Managed Identity on Azure. No config files, no connection strings. `storage`, `kv`, `queue`, `vault`, `container`, `postgres`, and `sandbox` are re-exported by `@alienplatform/sdk` from `@alienplatform/bindings`. A Worker installs `@alienplatform/sdk` and gets them alongside the handler APIs. Container and Daemon apps can import the same factories directly from `@alienplatform/bindings`, whether Alien builds their source or they arrive as a pre-built image. Key bindings are available directly from `@alienplatform/bindings`. Either way the bindings are an in-process library, not a runtime channel. For a TypeScript Container or Daemon, install `@alienplatform/bindings`. Its prebuilt addon supports Node and Bun on macOS (arm64/x64) and glibc Linux (x64/arm64). `alien build` can embed Linux x64/arm64 and macOS arm64 targets. Windows and musl Linux are not currently supported. If the addon is missing, make sure optional dependencies were installed or install the matching `@alienplatform/bindings-` package. One build-shape caveat for source-built apps: the bundle embeds the native bindings addon as CommonJS, which forbids **top-level `await`**. Wrap async startup in an entry function (`async function main() { ... }` then `void main()`); the snippets on this page show statements inside such a body, not at module top level. This applies to source-built Workers as well — the SDK embeds the same addon. In Rust, a Container or Daemon resolves the same bindings without a worker context via `alien_bindings::Bindings::from_env()`: ```rust let bindings = alien_bindings::Bindings::from_env()?; let store = bindings.storage("data").await?; // also: bindings.kv("cache"), bindings.queue("tasks"), bindings.vault("secrets") ``` `from_env()` reads the injected binding configuration synchronously and does no I/O; the first call on a returned handle resolves the resource. ## Storage [#storage] Object storage — S3 on AWS, Cloud Storage on GCP, Blob Storage on Azure. ```typescript import { storage } from "@alienplatform/sdk" const store = storage("files") // Write (bytes) await store.put("reports/q1.json", Buffer.from(JSON.stringify(report))) // Read — get() returns data plus object metadata const result = await store.get("reports/q1.json") const content = result.data.toString("utf8") console.log(result.meta.eTag, result.attributes.contentType) // List — resolves to an array of object metadata for (const entry of await store.list("reports/")) { console.log(entry.location, entry.size) } // Delete await store.delete("reports/old.json") // Presigned request for direct browser uploads/downloads const req = await store.signedUrl({ method: "GET", path: "reports/q1.json", expiresIn: 3600, }) ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let store = bindings.storage("files").await?; // Write store.put(&"reports/q1.json".into(), bytes).await?; // Read let result = store.get(&"reports/q1.json".into()).await?; let content = result.bytes().await?; // List let mut stream = store.list(Some(&"reports/".into())); while let Some(meta) = stream.next().await { let meta = meta?; println!("{} {}", meta.location, meta.size); } // Delete store.delete(&"reports/old.json".into()).await?; // Presigned request let req = store.presigned_get( &"reports/q1.json".into(), Duration::from_secs(3600), ).await?; ``` Full reference: [Storage API](/docs/infrastructure/storage/api) | [Behavior & limits](/docs/infrastructure/storage/behavior) ## Key [#key] Provider-backed encryption for small values. Import `key` directly from `@alienplatform/bindings`: ```typescript import { key } from "@alienplatform/bindings" const encryptionKey = key("customer-key") const plaintext = new TextEncoder().encode("small secret") const ciphertext = await encryptionKey.encrypt(plaintext) const decrypted = await encryptionKey.decrypt(ciphertext) ``` The binding accepts values up to 128 bytes and supports authenticated context. See [Key](/docs/infrastructure/key). ## KV [#kv] Key-value store — DynamoDB on AWS, Firestore on GCP, Table Storage on Azure. ```typescript import { kv } from "@alienplatform/sdk" const cache = kv("cache") // Write (with optional TTL in seconds) await cache.setJson("user:123", { name: "Alice" }) await cache.set("session:abc", token, { ttl: 3600 }) // Read — entries carry the value plus an opaque version for conditional writes const user = await cache.getJson<{ name: string }>("user:123") console.log(user?.value.name, user?.version) const session = await cache.getText("session:abc") console.log(session?.value) // Scan by prefix — resolves to a page of items plus a cursor const page = await cache.scan("user:") for (const entry of page.items) { console.log(entry.key, entry.value.toString("utf8")) } // Delete await cache.delete("user:123") ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let cache = bindings.kv("cache").await?; // Write (with optional TTL) cache.put("user:123", value.into(), None).await?; cache.put("session:abc", token.into(), Some(PutOptions { ttl: Some(Duration::from_secs(3600)), ..Default::default() })).await?; // Read if let Some(entry) = cache.get("user:123").await? { println!("{}", String::from_utf8_lossy(&entry.value)); } // Scan by prefix let result = cache.scan_prefix("user:", Some(100), None).await?; for (key, value) in result.items { println!("{}: {}", key, String::from_utf8_lossy(&value)); } // Delete cache.delete("user:123").await?; ``` The TypeScript handle also has `get` (raw bytes), `exists`, `setJson`, and paginates through `nextCursor`. Full reference: [KV API](/docs/infrastructure/kv/api) | [Behavior & limits](/docs/infrastructure/kv/behavior) ## Queue [#queue] Message queue — SQS on AWS, Pub/Sub on GCP, Service Bus on Azure. ```typescript import { queue } from "@alienplatform/sdk" const q = queue("tasks") // bind the queue once by name await q.send({ type: "process", id: "abc" }) // serialized as JSON await q.sendText("raw text message") ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let q = bindings.queue("tasks").await?; // bind the queue once by name q.send(MessagePayload::Json(serde_json::json!({ "type": "process", "id": "abc" }))).await?; ``` To **receive** messages on a Worker, use event handlers — see [Responding to Events](#responding-to-events) below. Full reference: [Queue API](/docs/infrastructure/queue/api) | [Behavior & limits](/docs/infrastructure/queue/behavior) ## Vault [#vault] Secret storage — SSM Parameter Store on AWS, Secret Manager on GCP, Key Vault on Azure. ```typescript import { vault } from "@alienplatform/sdk" const secrets = vault("credentials") // Read a secret (get() resolves to a string; getJson() parses it) const config = await secrets.getJson<{ host: string; database: string; password: string }>("database") // Connect to the customer's database using their own credentials const pool = new Pool({ host: config.host, database: config.database, password: config.password, // read at runtime from the customer vault }) ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let secrets = bindings.vault("credentials").await?; // Read a secret let raw = secrets.get_secret("database").await?; let config: DbConfig = serde_json::from_str(&raw)?; // Connect to customer's database using their own credentials let pool = PgPool::connect_with( PgConnectOptions::new() .host(&config.host) .database(&config.database) .password(&config.password) // read at runtime from the customer vault ).await?; ``` Secrets are stored in the customer's cloud vault and read by your deployed code at runtime. Keep them out of generated config, logs, and telemetry. Full reference: [Vault API](/docs/infrastructure/vault/api) | [Behavior & limits](/docs/infrastructure/vault/behavior) ## Container [#container] Service discovery for a linked [Container](/docs/infrastructure/container) — resolve its URLs instead of hard-coding cloud-specific service names. ```typescript import { container } from "@alienplatform/sdk" const api = container("api") const internalUrl = await api.getInternalUrl() // reachable from the deployment's private network const publicUrl = await api.getPublicUrl() // null when the container has no public endpoint ``` ```rust let bindings = Bindings::from_env()?; let api = bindings.container("api").await?; let internal_url = api.get_internal_url(); // reachable from the deployment's private network let public_url = api.get_public_url(); // None when the container has no public endpoint ``` ## Sandbox [#sandbox] Create an isolated session and stream a command. Resource configuration such as CPU, memory, lifetime, and egress lives in `alien.ts`; runtime code cannot raise those limits. ```typescript import { sandbox } from "@alienplatform/sdk" const box = sandbox("code-runner") const session = await box.create({ sessionId: "turn-123" }) for await (const frame of box.runCommand( session.sessionId, ["python3", "main.py"], { deadlineMs: 30_000 }, )) { if (frame.kind === "stdout") process.stdout.write(frame.data) if (frame.kind === "exit") console.log(frame.exitCode) } ``` Check `await box.capabilities()` before using platform-dependent file, reconnect, preview, or suspend/resume operations. See [Sandbox API](/docs/infrastructure/sandbox/api). ## Binding errors [#binding-errors] Binding failures are `AlienError` values with stable metadata: ```typescript import { AlienError } from "@alienplatform/bindings" try { await storage("files").get("missing.txt") } catch (error) { if (error instanceof AlienError) { console.error(error.code, error.retryable, error.hint) } } ``` The binding layer preserves `code`, `context`, `retryable`, `internal`, `httpStatusCode`, and `hint` when the backend supplies them. Handle the code; show the human message or hint instead of parsing it. ## Responding to Events [#responding-to-events] Workers can react to events — queue messages, file uploads, and cron schedules. Register a handler, and Alien wires the trigger. Event delivery is a Worker capability — Containers and Daemons don't receive events. ### Queue Messages [#queue-messages] ```typescript import { kv, onQueueMessage } from "@alienplatform/sdk" onQueueMessage("*", async (message) => { const store = kv("events") await store.setJson(`queue:${message.id}`, { source: message.source, payload: message.payload, processedAt: new Date().toISOString(), }) }) ``` ```rust let bindings = ctx.get_bindings(); ctx.on_queue_message("*", move |message| { let bindings = bindings.clone(); async move { let store = bindings.kv("events").await?; store.put( &format!("queue:{}", message.id), serde_json::to_vec(&serde_json::json!({ "source": message.source, "processedAt": chrono::Utc::now().to_rfc3339(), }))?, None, ).await?; Ok(()) } }); ``` Use `"*"` to handle messages from any linked queue, or pass a specific queue name. ### Storage Events [#storage-events] ```typescript import { onStorageEvent } from "@alienplatform/sdk" onStorageEvent("*", async (event) => { console.log(event.eventType, event.objectKey, event.size) // "created", "uploads/photo.jpg", 1048576 }) ``` ```rust ctx.on_storage_event("*", |event| async move { println!("{} {} {}", event.event_type, event.key, event.size); Ok(()) }); ``` ### Cron / Scheduled Events [#cron--scheduled-events] ```typescript import { onCronEvent } from "@alienplatform/sdk" onCronEvent("*", async (event) => { console.log(event.scheduleName, event.timestamp) // run cleanup, generate reports, sync data... }) ``` ```rust ctx.on_cron_event("*", |event| async move { println!("{} {}", event.schedule_name, event.scheduled_time); // run cleanup, generate reports, sync data... Ok(()) }); ``` The schedule is defined in `alien.ts` via `.trigger({ type: "schedule", cron: "0 * * * *" })`. See [Events & Triggers](/docs/infrastructure/worker/events-and-triggers) for trigger configuration. ## Remote Commands [#remote-commands] Define callable handlers that your control plane can invoke remotely — no inbound networking, no open ports. On a Worker, register handlers with `command()`: ```typescript import { command, vault, kv } from "@alienplatform/sdk" import { z } from "zod" command( "query", z.object({ sql: z.string(), useCache: z.boolean() }), async ({ sql, useCache }) => { const secrets = vault("credentials") const cache = kv("cache") if (useCache) { const cached = await cache.getJson(`query:${hash(sql)}`) if (cached) return { ...cached, cached: true } } const config = await secrets.getJson("database") const result = await runQuery(config, sql) if (useCache) { await cache.setJson(`query:${hash(sql)}`, result) } return { ...result, cached: false } }, ) ``` ```rust let bindings = ctx.get_bindings(); ctx.on_command("query", move |params: QueryParams| { let bindings = bindings.clone(); async move { let secrets = bindings.vault("credentials").await?; let cache = bindings.kv("cache").await?; if params.use_cache { if let Some(cached) = cache.get(&format!("query:{}", hash(¶ms.sql))).await? { let mut result: serde_json::Value = serde_json::from_slice(&cached)?; result["cached"] = serde_json::json!(true); return Ok(result); } } let config: DbConfig = serde_json::from_str( &secrets.get_secret("database").await? )?; let result = run_query(&config, ¶ms.sql).await?; if params.use_cache { cache.put( &format!("query:{}", hash(¶ms.sql)), serde_json::to_vec(&result)?, None, ).await?; } Ok(result) } }); ``` A Container or Daemon receives commands through an explicit pull receiver instead. See [Remote Commands](/docs/commands) for the full guide, including the sender and the Container/Daemon receiver. ## Using Native Cloud SDKs [#using-native-cloud-sdks] Every linked resource is also available as a JSON environment variable. Use any language, any SDK: ```typescript const binding = JSON.parse(process.env.ALIEN_DATA_BINDING!) if (binding.service === "s3") { const s3 = new S3Client({}) await s3.send(new GetObjectCommand({ Bucket: binding.bucketName, Key: "reports/q1.json", })) } ``` ```python import json, os, boto3 binding = json.loads(os.environ["ALIEN_DATA_BINDING"]) s3 = boto3.client("s3") s3.get_object(Bucket=binding["bucketName"], Key="reports/q1.json") ``` The environment variable name follows the pattern `ALIEN_{NAME}_BINDING` — uppercased, hyphens become underscores. The JSON contains resource identifiers, never credentials. Cloud credentials are injected automatically (IAM roles, Workload Identity, Managed Identity). Use native SDKs when you need platform-specific features like DynamoDB streams, S3 Select, or Pub/Sub ordering keys. They coexist with the Alien SDK in the same app. # Stacks (/docs/stacks) A **stack** is the complete set of infrastructure Alien provisions in each customer's environment. You define it in `alien.ts` — Alien translates each resource to the native cloud service at deploy time (S3 on AWS, Cloud Storage on GCP, Blob Storage on Azure, and so on). ## Defining Resources [#defining-resources] Each resource type maps to a native cloud service on every platform. ### Workers [#workers] Serverless compute. Deploys as Lambda on AWS, Cloud Run on GCP, Container Apps on Azure. Point at a source directory with a toolchain — Alien compiles and packages for the target platform. ```typescript import * as alien from "@alienplatform/core" const api = new alien.Worker("api") .code({ type: "source", src: "./api", toolchain: { type: "typescript" } }) .publicEndpoint("api") .permissions("execution") .build() ``` See [Worker reference](/docs/infrastructure/worker) for configuration options, [Events & Triggers](/docs/infrastructure/worker/events-and-triggers) for queue, storage, and cron triggers, and [Environment Variables](/docs/infrastructure/worker/environment-variables) for runtime configuration. ### Storage [#storage] Object storage — S3 on AWS, Cloud Storage on GCP, Blob Storage on Azure. ```typescript const data = new alien.Storage("data") .publicRead(false) .versioning(false) .build() ``` See [Storage reference](/docs/infrastructure/storage) for configuration options and behavior. ### KV [#kv] Key-value store — DynamoDB on AWS, Firestore on GCP, Table Storage on Azure. ```typescript const cache = new alien.Kv("cache").build() ``` See [KV reference](/docs/infrastructure/kv) for behavior and limits. ### Queue [#queue] Message queue — SQS on AWS, Pub/Sub on GCP, Service Bus on Azure. ```typescript const tasks = new alien.Queue("tasks").build() ``` See [Queue reference](/docs/infrastructure/queue) for behavior and limits. ### Vault [#vault] Secret storage — SSM Parameter Store on AWS, Secret Manager on GCP, Key Vault on Azure. ```typescript const secrets = new alien.Vault("credentials").build() ``` See [Vault reference](/docs/infrastructure/vault) for behavior and limits. ### Artifact Registry [#artifact-registry] Container image registry — ECR on AWS, Artifact Registry on GCP, ACR on Azure. ```typescript const images = new alien.ArtifactRegistry("images").build() ``` See [Artifact Registry reference](/docs/infrastructure/artifact-registry) for configuration options. ## The Stack Object [#the-stack-object] A `Stack` groups your resources and declares whether each one is frozen or live: ```typescript export default new alien.Stack("my-app") .add(data, "frozen") // owned by customer setup .add(cache, "frozen") // owned by customer setup .add(tasks, "frozen") // owned by customer setup .add(api, "live") // managed by Alien after setup .build() ``` ### Frozen vs. Live [#frozen-vs-live] The lifecycle says who owns changes after setup: * **Frozen** — Customer setup owns the resource. A normal rollout cannot create, change, replace, or delete it. * **Live** — Alien's Deployment Manager can create, update, replace, or remove the resource during a rollout. Workloads such as Workers and Containers are live. Data resources can usually be frozen or live, depending on whether changes should require customer setup. Alien validates which lifecycles each resource supports and derives the corresponding management permissions. See [Frozen & Live](/docs/frozen-and-live) for the full two-phase deployment model. ### Stack inputs [#stack-inputs] Some values aren't known until setup: a database URL inside the customer's network, an API key for your control plane. Declare them as **stack inputs** in `alien.ts`, mark who provides each one, and Alien validates and collects them across the dashboard, deployment portal, CLI, CloudFormation, Terraform, and Helm. ```typescript import * as alien from "@alienplatform/core" const inputs = alien.inputs({ databaseUrl: alien.string({ providedBy: "deployer", required: true, label: "Database URL", description: "Postgres connection string inside the customer's network.", pattern: "^postgres://", env: "DATABASE_URL", }), }) export default new alien.Stack("my-app").inputs(inputs).add(worker, "live").build() ``` See [Stack inputs](/docs/inputs) for the full type and validation reference, secret handling, and the developer vs deployer model. ### Permission Profiles [#permission-profiles] Permission profiles control what your **application code** can access at runtime. They're separate from the management permissions Alien uses to deploy — those are [auto-derived](#frozen-vs-live). A profile is a named set of permissions that maps resource names to permission sets. Workers reference a profile by name with `.permissions("profile-name")`: ```typescript const api = new alien.Worker("api") .permissions("execution") // use the "execution" profile .build() ``` You define profiles in the stack's `.permissions()` block. Each key inside a profile is either a specific resource name or `"*"` (all resources of that type): ```typescript export default new alien.Stack("my-app") .add(data, "frozen") .add(cache, "frozen") .add(tasks, "frozen") .add(api, "live") .permissions({ profiles: { execution: { data: ["storage/data-read", "storage/data-write"], cache: ["kv/data-read", "kv/data-write"], tasks: ["queue/data-read", "queue/data-write"], }, }, }) .build() ``` Under the hood, each profile becomes a cloud identity — an IAM Role on AWS, a Service Account on GCP, or a Managed Identity on Azure. The permission sets (`storage/data-read`, `kv/data-write`, etc.) are translated to the correct cloud-specific policies automatically. See [Permissions](/docs/permissions) for the full reference on scopes, built-in permission sets, and custom permissions. ## Linking Resources [#linking-resources] Use `.link()` to connect a worker to a resource. This injects the connection parameters as environment variables at runtime — your code can then access the resource using the [Alien SDK](/docs/resource-apis) or native cloud SDKs: ```typescript const api = new alien.Worker("api") .link(data) .link(cache) .link(tasks) .build() ``` A worker can only access resources it's linked to. This is enforced at the cloud level through [permission profiles](/docs/permissions). ## Examples [#examples] ### AI Agent with Remote Commands [#ai-agent-with-remote-commands] An AI worker that runs in the customer's cloud, with storage for files and a vault for integration credentials. Remote commands let your control plane invoke tool calls without any inbound networking: ```typescript import * as alien from "@alienplatform/core" const workspace = new alien.Storage("workspace").build() const secrets = new alien.Vault("integrations").build() const agent = new alien.Worker("agent") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .link(workspace) .link(secrets) .commandsEnabled(true) .permissions("execution") .build() export default new alien.Stack("my-agent") .add(workspace, "frozen") .add(secrets, "frozen") .add(agent, "live") .permissions({ profiles: { execution: { workspace: ["storage/data-read", "storage/data-write"], integrations: ["vault/data-read"], }, }, }) .build() ``` ### Event-Driven Pipeline [#event-driven-pipeline] A worker that processes messages from a queue, with storage for results: ```typescript import * as alien from "@alienplatform/core" const results = new alien.Storage("results").build() const jobs = new alien.Queue("jobs").build() const worker = new alien.Worker("worker") .code({ type: "source", src: "./worker", toolchain: { type: "typescript" } }) .link(results) .link(jobs) .trigger({ type: "queue", queue: jobs.ref() }) .permissions("processing") .build() export default new alien.Stack("pipeline") .add(results, "frozen") .add(jobs, "frozen") .add(worker, "live") .permissions({ profiles: { processing: { results: ["storage/data-read", "storage/data-write"], jobs: ["queue/data-read"], }, }, }) .build() ``` ### Public API with Cache [#public-api-with-cache] A public-facing API with a KV cache and vault for API keys: ```typescript import * as alien from "@alienplatform/core" const cache = new alien.Kv("cache").build() const secrets = new alien.Vault("api-keys").build() const api = new alien.Worker("api") .code({ type: "source", src: "./api", toolchain: { type: "typescript" } }) .link(cache) .link(secrets) .publicEndpoint("api") .memoryMb(512) .timeoutSeconds(30) .permissions("execution") .build() export default new alien.Stack("my-api") .add(cache, "frozen") .add(secrets, "frozen") .add(api, "live") .permissions({ profiles: { execution: { cache: ["kv/data-read", "kv/data-write"], "api-keys": ["vault/data-read"], }, }, }) .build() ``` ## What's Next [#whats-next] # Testing (/docs/testing) Alien provides a testing framework that deploys your stack and runs assertions against it. ## Local Testing [#local-testing] The default mode. Runs `alien dev` as a child process, deploys your stack locally, and exposes the same API as a cloud deployment: ```typescript import { deploy, type Deployment } from "@alienplatform/testing" let deployment: Deployment beforeAll(async () => { deployment = await deploy({ app: ".", platform: "local" }) }, 120_000) afterAll(async () => { await deployment.destroy() }) test("health check", async () => { const res = await fetch(`${deployment.url}/health`) expect(res.ok).toBe(true) }) test("storage works", async () => { const res = await fetch(`${deployment.url}/storage-test/data`) const body = await res.json() expect(body.status).toBe("ok") }) ``` Local mode uses the same bindings and resource implementations as production — SQLite for KV/Queue, filesystem for Storage, plaintext files for Vault. Your application code doesn't know the difference. ## Cloud Testing [#cloud-testing] For integration tests against real cloud services, set `ALIEN_API_KEY` and specify a platform: ```typescript const deployment = await deploy({ app: ".", platform: "aws", // or "gcp", "azure" }) ``` Cloud mode builds your app, creates a release, provisions real infrastructure, and deploys. Tests run against actual S3, DynamoDB, Lambda, etc. ## CI Pipeline [#ci-pipeline] A typical CI setup: ```yaml # .github/workflows/test.yml jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: bun install - run: bun test ``` Local tests require no cloud credentials — they run entirely on the CI machine. # Start with the requirement (/docs/what-to-move) Start with what must be close to the private service, data, model, key, or hardware. Then choose the smallest useful setup: ```text one model or key gateway one published resource remote binding one operation Worker command several services Stack Helm-owned application Remote Operator ``` * one operation: a Worker and a Command; * one customer-owned model or key: [AI Gateway](/docs/ai-gateway) or [Encryption Gateway](/docs/encryption-gateway); * one customer-owned resource: [Remote bindings](/docs/remote-bindings); * an application and its resources together: [Stacks](/docs/stacks); * an existing Kubernetes installation: [Remote operator](/docs/remote-operator). # Where a deployment runs (/docs/where-it-runs) Alien can create resources in a supported AWS, Google Cloud, or Azure account, install into an existing Kubernetes environment, or connect to a machine-based setup. The exact options depend on the resources in your Stack and the generated setup. ```bash # See the platforms supported by the current project configuration. alien projects capabilities status --json ``` See [Deployment models](/docs/deploying/deployment-models) and [Networking](/docs/networking). # Overview (/docs/ai-gateway) AI Gateway lets every customer run your product's model requests through an AI provider account they already control (e.g. their OpenAI, Anthropic, AWS Bedrock, etc.). Your backend keeps one OpenAI-compatible integration; Alien handles provider-specific credentials, IAM roles, regions, and routing for each customer. For each customer, model requests use only the models they have approved and count against their existing provider or cloud commitment—not your AI bill. Your application never receives their provider credentials. This is BYO-LLM. Without AI Gateway, every request uses provider credentials your company owns. You pay the inference bill, and customers cannot require your product to use the providers, cloud environments, or models they have approved. Enterprise buyers push back for two reasons: * **They already have committed AI spend.** Large companies negotiate commitments with OpenAI, Anthropic, AWS, Google Cloud, or Microsoft. They want usage from your product to count toward that spend. * **They have AI governance requirements.** Security or company policy may allow only specific providers, cloud environments, regions, or models. With AI Gateway, your backend uses one endpoint. It identifies the customer and model on each request; Alien selects the correct provider connection. ## Add it to an OpenAI client [#add-it-to-an-openai-client] Point an existing OpenAI client at Alien: change the base URL, send the customer ID as a header, and use a model ID that Alien lists for that customer. ```typescript import OpenAI from "openai" const ai = new OpenAI({ baseURL: "https://ai.alien.dev/v1", apiKey: process.env.ALIEN_AI_KEY, defaultHeaders: { "X-Alien-External-ID": customer.id, // [!code highlight] }, }) const response = await ai.chat.completions.create({ model: "byo/claude-opus-5", // [!code highlight] messages: [{ role: "user", content: "Hello" }], }) ``` Three values in that example decide where the request goes: | Value | What it selects | | --------------------- | ----------------------------------------------------------- | | Alien API key | Your Alien project | | `X-Alien-External-ID` | Which of your customers the request is for | | `model` | Which model to use from that customer's provider connection | Alien AI Gateway accepts requests shaped as OpenAI Chat Completions, OpenAI Responses, or Anthropic Messages, plus model discovery. ## It routes by customer [#it-routes-by-customer] Most AI gateways decide which provider or model should handle a request using connections that you configured. AI Gateway answers a different question first: which customer's provider connection should handle it? `X-Alien-External-ID` selects the customer connection. `model` selects a model available through that connection. You can use AI Gateway alongside a model router; they solve different problems. | | Provider connection used | Model used | | ---------------- | ---------------------------------------------- | ------------------------------------------------------------------------------- | | Model router | One of the provider connections you configured | Chosen by your routing rules | | Alien AI Gateway | The connection configured by that customer | Requested by your application from the models available through that connection | ## How this relates to the rest of Alien [#how-this-relates-to-the-rest-of-alien] Alien can deploy your application into a customer's cloud so your code runs next to their data. AI Gateway does not move your application: your backend stays where it is, and only model requests are routed through the provider connection the customer supplied. Use AI Gateway when customers need their approved providers and models but do not need to host your application. If a customer later needs your code running beside their data, [How Alien works](/docs/how-alien-works) covers the deployment models that do that. It forwards model requests through the provider connection your customer supplied. To run your application code inside a customer's environment, start with the [Alien framework](/docs/alien-framework). ## How customers connect a provider [#how-customers-connect-a-provider] Each customer connects their provider through a setup link your backend generates and shows inside your own product. The page carries your [portal branding](/docs/deploying/deployment-portal), and your application never receives the credentials the customer provides. Setup depends on the provider: | Provider | What your customer grants | | --------------------------- | ------------------------------------------------------------------------------- | | OpenAI, Anthropic | An API key from their provider account | | AWS Bedrock | An IAM role in their AWS account that Alien [impersonates](/docs/impersonation) | | Vertex AI, Azure AI Foundry | A cloud identity in their Google Cloud project or Azure tenant | Only the first row is a plain API key. Each cloud provider requires its own trust setup, which the customer completes using their own cloud access — that is why connecting runs as a guided flow rather than a text field on your settings page. See [Integrate with your product](/docs/ai-gateway/integrate-with-your-product). ## What Alien records [#what-alien-records] Gateway diagnostics record routing and outcome metadata for each request: model, provider, status, latency, and token counts. They do not record prompt or response bodies. Usage reporting aggregates that same metadata. See [Requests and models](/docs/ai-gateway/routing). # Integrate with your product (/docs/ai-gateway/integrate-with-your-product) Add AI Gateway to your product as an integrations flow: ```text your settings page → customer connects a provider → your backend sends model requests ``` Alien stores the provider credentials. Your frontend only opens setup and displays connection status; model requests come from your backend. ## 1. Choose an external ID [#1-choose-an-external-id] An `externalId` is your stable identifier for a customer or tenant, such as `org_123`. Alien uses it to connect three things: * the setup link you give that customer; * the provider connection shown in your integrations UI; and * model requests made on that customer's behalf. Use an immutable database ID, not a display name or a value supplied by the browser. Resolve it from the authenticated customer on your backend. ## 2. Configure the models you offer [#2-configure-the-models-you-offer] In **Infrastructure → Models**, select the models your product may call and the client APIs it uses. Alien shows customers the providers that can serve that configuration. A required model must be available before setup is complete. Optional models do not block setup. For example: ```bash alien projects capabilities enable ai \ --model byo/claude-opus-5 ``` ## 3. Add a Connect button [#3-add-a-connect-button] When the customer clicks **Connect AI provider**, create a setup link on your backend and redirect them to `deploymentLink`. Setup links require a server-side **Alien Platform API key** (`ALIEN_API_KEY`). The **AI Gateway key** (`ALIEN_AI_KEY`) is for model requests and cannot manage connections. ```typescript import { Alien } from "@alienplatform/platform-api" const alien = new Alien({ apiKey: process.env.ALIEN_API_KEY }) const setup = await alien.setupLinks.create({ project: "my-project", externalId: customer.id, name: customer.slug, setupItems: [{ item: "models", required: true }], }) return setup.deploymentLink ``` ```bash curl --fail-with-body \ "https://api.alien.dev/v1/deployment-groups/setup-links?workspace=my-workspace" \ -H "Authorization: Bearer $ALIEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "my-project", "externalId": "org_123", "name": "acme", "setupItems": [{"item": "models", "required": true}] }' ``` Redirect the customer to `deploymentLink` from the response. Calling this endpoint again with the same project and `externalId` reuses the customer record and returns a fresh link. Create links when the customer clicks Connect or Reconnect, not while rendering the page. ### Open a specific provider [#open-a-specific-provider] For a single **Connect AI provider** button, omit `entryPoint` and let the customer choose. If your UI has one card per provider, you can open that provider first: ```typescript const setup = await alien.setupLinks.create({ project: "my-project", externalId: customer.id, name: customer.slug, setupItems: [{ item: "models", required: true }], entryPoint: { item: "models", provider: "databricks" }, }) ``` The customer can still go back and choose another compatible provider. `entryPoint` controls the first screen, not what they are allowed to use. Only add a policy when your product must prevent a choice. For example, this opens Databricks and permits only Databricks: ```json { "deploymentSetupConfig": { "policy": {"allowedAIProviders": ["databricks"]} }, "entryPoint": {"item": "models", "provider": "databricks"} } ``` Omit `deploymentSetupConfig` for the normal flow. See [Setup Links API](/docs/reference/api/setup-links) for advanced restrictions. ## 4. Send a model request [#4-send-a-model-request] After the customer connects a provider, send requests through AI Gateway with the same customer ID in `X-Alien-External-ID`: ```typescript import OpenAI from "openai" const ai = new OpenAI({ baseURL: "https://ai.alien.dev/v1", apiKey: process.env.ALIEN_AI_KEY, defaultHeaders: { "X-Alien-External-ID": customer.id, }, }) const response = await ai.chat.completions.create({ model: "byo/claude-opus-5", messages: [{ role: "user", content: "Hello" }], }) ``` The AI Gateway key identifies your project. `X-Alien-External-ID` selects the customer connection, and `model` selects a model available through it. Keep both the gateway key and the external ID decision on your backend. See [Routing requests](/docs/ai-gateway/routing) for other protocols and routing behavior. ## 5. Show connection status [#5-show-connection-status] Read the capability overview from your backend and find the record with the same `externalId`: ```typescript const overview = await alien.projects.getCapabilityOverview({ idOrName: "my-project", }) const group = overview.groups.find( group => group.externalId === customer.id, ) const models = group?.capabilities.models const provider = models?.directProvider?.provider ?? models?.observation?.provider ``` Use `models.state` as the top-level UI state: | State | Show in your product | | ----------------- | --------------------------------- | | `not-connected` | Connect AI provider | | `setting-up` | Setup in progress | | `connected` | Connected, with the provider name | | `needs-attention` | Reconnect or fix provider access | | `revoked` | Disconnected | `modelCoverage` shows which configured models are available, blocked, or not checked yet. A connected provider does not guarantee access to every model because regions, model activation, and quotas can differ. The REST equivalent is: ```bash curl --fail-with-body \ "https://api.alien.dev/v1/projects/my-project/project-capabilities/overview?workspace=my-workspace" \ -H "Authorization: Bearer $ALIEN_API_KEY" ``` ## 6. Reconnect, change, or disconnect [#6-reconnect-change-or-disconnect] To reconnect or change providers, create another setup link with the same `externalId`. The customer can resume incomplete cloud setup, replace a direct provider credential, or choose another available provider. For OpenAI, Anthropic, and Databricks, remove the stored credential through setup or from your backend: ```typescript await alien.deploymentGroups.deleteExternalAIBinding({ id: group.deploymentGroupId, }) ``` ```bash curl --fail-with-body -X DELETE \ "https://api.alien.dev/v1/deployment-groups/dg_123/ai/external?workspace=my-workspace" \ -H "Authorization: Bearer $ALIEN_API_KEY" ``` Bedrock, Vertex AI, and Azure AI Foundry use infrastructure in the customer's cloud account. The customer disconnects them by returning to setup and deleting the cloud stack or resources. Continue reading the capability overview until the state becomes `revoked`. Gateway replicas may finish requests already admitted under a lease of up to five minutes. ## Test the complete flow [#test-the-complete-flow] For every provider you offer: 1. connect an account you control and confirm the state reaches `connected`; 2. call every required model through every client API your product uses; 3. test streaming and tool calls when applicable; 4. revoke access and confirm your UI reports the failure; and 5. reconnect and confirm requests recover. Model protocols do not map perfectly. AI Gateway rejects cross-protocol fields it cannot represent instead of silently dropping them. # Quickstart (/docs/ai-gateway/quickstart) By the end of this quickstart, a model request from your backend will run on a provider account connected through Alien rather than on your own. Every step uses the CLI so it is copyable. Run them from a directory linked to your Alien project. ## 1. Enable AI Gateway [#1-enable-ai-gateway] Choose one model to test: ```bash alien projects capabilities enable ai \ --model byo/claude-opus-5 ``` Create an API key for requests from your backend: ```bash alien api-keys create \ --for ai-gateway \ --description local-quickstart ``` The secret is shown once. Save it as `ALIEN_AI_KEY`. ```bash export ALIEN_AI_KEY="..." ``` ## 2. Connect your test account [#2-connect-your-test-account] Create a setup link for a customer your application calls `org_123`: ```bash alien onboard "Test customer" \ --external-id org_123 \ --setup-items models ``` Open the returned link and connect a provider account you control. Use the same customer ID in every request for this connection: ```bash export CUSTOMER_ID="org_123" ``` ## 3. Send a request [#3-send-a-request] The CLI can print a request for the active Alien environment: ```bash alien examples ai-gateway \ --protocol openai-chat \ --model byo/claude-opus-5 ``` Generated examples currently reference `$ALIEN_AI_API_KEY`. Substitute the value you saved as `ALIEN_AI_KEY`, or export it under both names while running this quickstart. Run the printed command. It is equivalent to: ```bash curl "https://ai.alien.dev/v1/chat/completions" \ -H "Authorization: Bearer $ALIEN_AI_KEY" \ -H "X-Alien-External-ID: $CUSTOMER_ID" \ -H "Content-Type: application/json" \ -d '{ "model": "byo/claude-opus-5", "messages": [{"role": "user", "content": "Say hello in five words."}] }' ``` Use the model ID returned by your project if it differs from this example. ## 4. Put it in your backend [#4-put-it-in-your-backend] ```typescript import OpenAI from "openai" const ai = new OpenAI({ baseURL: "https://ai.alien.dev/v1", // [!code highlight] apiKey: process.env.ALIEN_AI_KEY, defaultHeaders: { "X-Alien-External-ID": customer.id, // [!code highlight] }, }) const response = await ai.chat.completions.create({ model: "byo/claude-opus-5", // [!code highlight] messages: [{ role: "user", content: "Hello" }], }) ``` Resolve `customer.id` from the authenticated server-side account. Do not accept it directly from browser input. ## If the request fails [#if-the-request-fails] Search AI Gateway diagnostics without exposing prompts or responses: ```bash alien logs --source ai-gateway --since 1h ``` Filter by model, provider, or outcome: ```bash alien logs --source ai-gateway \ --status provider-error \ --model byo/claude-opus-5 ``` # Requests and models (/docs/ai-gateway/routing) Every request has three routing inputs. | Input | Example | Purpose | | --------------------- | ---------------------- | -------------------------------- | | `Authorization` | `Bearer $ALIEN_AI_KEY` | Authenticates your Alien project | | `X-Alien-External-ID` | `org_123` | Selects the customer connection | | `model` | `byo/claude-opus-5` | Selects a configured model | ## Customer IDs [#customer-ids] Use a stable tenant or organization ID from your own database. Resolve it after authenticating the user. Do not let a browser choose an arbitrary `X-Alien-External-ID`. Anyone who can choose that value while using your server API key could target another customer’s connection. ## Model IDs [#model-ids] Use the exact ID shown in the dashboard or returned by: ```bash curl "https://ai.alien.dev/v1/models" \ -H "Authorization: Bearer $ALIEN_AI_KEY" \ -H "X-Alien-External-ID: $CUSTOMER_ID" ``` The list is specific to that customer’s connected provider and current configuration. ## What model status means [#what-model-status-means] Alien reports a model's status by reading the customer's provider configuration, never by sending a test request. Checking status does not consume the customer's quota or accept provider terms on their behalf. | Status | What Alien verified | | ------------ | --------------------------------------------------------------------------------------------- | | `configured` | The customer connected a provider, and this model is part of that connection's configuration. | | `available` | Alien read the provider's own configuration and found the model there. | Neither status promises the next request will succeed. Only a real completion or message request proves that, because provider quota, policy, capacity, and request-specific settings can still reject it. Treat `/v1/models` as discovery, not as a synthetic inference test. ## Client protocols [#client-protocols] | Client | Endpoint | | ----------------------- | ---------------------- | | OpenAI Chat Completions | `/v1/chat/completions` | | OpenAI Responses | `/v1/responses` | | Anthropic Messages | `/v1/messages` | When the provider speaks the same protocol, the gateway preserves the request body. When it must translate, unsupported provider-specific or stateful fields return an error. ## Static provider headers [#static-provider-headers] Open **Infrastructure → Models → Provider headers** to add fixed headers to every request sent to one provider. Use this for provider-required attribution or routing metadata—not per-customer secrets or values supplied by end users. ## Diagnose requests [#diagnose-requests] Gateway diagnostics record routing and outcome metadata, not prompt or response bodies. Search them from the CLI: ```bash alien logs --source ai-gateway --since 24h ``` Useful filters include `--status`, `--model`, `--provider`, and `--deployment-group`: ```bash alien logs --source ai-gateway \ --status provider-error \ --provider anthropic \ --json ``` ## Inspect usage [#inspect-usage] ```bash alien usage ai --range 24h alien usage ai --range 7d --json ``` Usage is a privacy-safe aggregate. It includes request counts, success and error totals, input and output tokens, estimated provider cost, and latency when the project’s metrics source is available. # Deployment models (/docs/deploying/deployment-models) These are separate choices: ```text push ── Alien's manager calls the environment pull ── environment component calls Alien existing installation ── current chart remains authoritative ``` ## Push [#push] Alien uses scoped access in the environment to create or update a deployment. ## Pull [#pull] A component in the environment opens the connection and uses the environment’s own identity. ## Existing installation [#existing-installation] [Remote operator](/docs/remote-operator) observes and operates an application whose Helm or other deployment definition already exists. Choose the method the environment owner can approve and operate. Read the generated setup and permissions; provider details vary by platform. See [Access](/docs/access), [Where a deployment runs](/docs/where-it-runs), and [Remote operator](/docs/remote-operator). # Deployment Portal (/docs/deploying/deployment-portal) The Deployment Portal is the setup page opened from a deployment link. It shows the platforms and setup methods allowed by that link, collects deployer-provided inputs, and displays the generated commands or templates. The environment owner performs the environment-owned setup with their own cloud or cluster access. The setup API checks the selected choices again; the browser is not the access-control mechanism. ```text your backend creates link │ ▼ environment owner opens portal │ chooses an allowed platform and setup method ▼ generated setup runs with the owner's credentials │ ▼ deployment checks in ``` ## What to configure [#what-to-configure] In the dashboard, configure the portal branding and support or documentation links under the project’s deployment settings. The exact fields shown in the portal come from the current project and release. ## Generate a deployment link [#generate-a-deployment-link] Create links from your backend with a server-side Alien Platform API key. The common case needs only the project and your stable ID and name for the environment: ```typescript import { Alien } from "@alienplatform/platform-api" const alien = new Alien({ apiKey: process.env.ALIEN_API_KEY }) const setup = await alien.setupLinks.create({ project: "my-project", externalId: environment.id, name: environment.slug, }) return setup.deploymentLink ``` ```bash curl "https://api.alien.dev/v1/deployment-groups/setup-links?workspace=my-workspace" \ -H "Authorization: Bearer $ALIEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "my-project", "externalId": "environment_123", "name": "production" }' ``` Read `deploymentLink` from the response. Reusing an `externalId` reuses the same deployment group; each request creates a new revocable link for it. By default, Alien derives the available platforms from the release and allows its supported setup methods. Add `deploymentSetupConfig.policy` only to restrict those choices. Add `setupItems: "all"` when one portal should include the application plus every capability enabled for the project. ## Test the handoff [#test-the-handoff] Before sending a link: 1. publish the release; 2. generate a link limited to the platforms and setup items you support; 3. open it in a clean browser profile; 4. complete setup in a test environment; 5. confirm the deployment checks in and one safe operation succeeds. See [Customer setup](/docs/deploying/onboarding-customers) and [Google Cloud setup](/docs/google-cloud-oauth). # Google Cloud OAuth (/docs/deploying/google-cloud-oauth) The GCP deployment portal can let a customer's admin deploy from the browser by signing in with Google. Alien uses the Google OAuth token only for the one-time bootstrap, then hands ongoing management to the scoped service account created during setup. ## Default Provider [#default-provider] Projects can use Alien's managed Google OAuth app by default. This is the fastest path when you do not need custom consent-screen branding. The app requests the `cloud-platform` scope because initial setup may need to enable APIs, create service accounts, add IAM bindings, and create frozen infrastructure resources. ## White-Labeled Provider [#white-labeled-provider] For a white-labeled deployment page, configure a custom Google OAuth provider on the project. Google shows the consent screen branding from the GCP project that owns the OAuth client, so a custom deployment page should use a dedicated GCP project with its own Auth Platform brand and OAuth web client. Create a Google OAuth web client with the redirect URI shown in the project's deployment page settings. The URI points at the selected manager: ```text https:///v1/gcp/oauth/callback ``` Then open the project's deployment page settings and select **Custom app** under **Google Cloud OAuth**. Enter the client ID and client secret from Google Cloud Console. ## Security Model [#security-model] Alien does not return the OAuth access token to the browser. The manager stores it only in a short-lived deployment session, uses it for the bootstrap, and deletes the session after one attempt. After bootstrap, ongoing deployment management uses the service account and impersonation model created inside the customer's GCP project. The OAuth client is only for the initial setup flow. Use a separate OAuth app for ordinary dashboard sign-in. The deployment bootstrap app needs broad Google Cloud permissions, while dashboard sign-in usually only needs identity scopes. # Customer setup (/docs/deploying/onboarding-customers) After publishing a release, create a deployment group and generate a setup link. Send the link to the person who owns the target environment. ```bash alien onboard "Acme" \ --external-id org_123 \ --setup-items application ``` In the dashboard, use **Deployments → New deployment → Generate deployment link**. Choose only the platforms and setup items your project supports. The environment owner opens the link, chooses a setup method, supplies their environment values, and runs the generated setup. Your application does not need their long-lived cloud credentials. ## Inputs [#inputs] * **Developer-provided** values are supplied when you create the link. * **Deployer-provided** values are supplied by the environment owner during setup. Declare which side supplies each value in `alien.ts`. Test the generated setup and permissions before sending the link. ## Verify it [#verify-it] Open the deployment in the dashboard and confirm its status and last check-in. Then run one safe Command or request that exercises the path your product depends on. For automation, wait for the deployment instead of writing your own polling loop: ```bash alien deployments wait acme/production \ --for ready \ --timeout 10m \ --json ``` See [Deployment Portal](/docs/deploying/deployment-portal), [Inputs](/docs/inputs), and [Releases](/docs/releases). # Overview (/docs/encryption-gateway) Encryption Gateway keeps customer data in your control plane while giving each customer a cryptographic kill switch. Each customer's data is protected by a key they control in AWS KMS, Google Cloud KMS, or Azure Key Vault. By disabling that key, a customer can cut off access to their data—even if your cloud is compromised. Your application never receives their cloud credentials or raw key material. This is BYOK: bring your own key. Your control plane still stores and serves the data. The customer-held key controls whether it can be read, turning trust in your access into a control the customer can exercise without your cooperation. The Encrypt/Decrypt API receives plaintext to encrypt and returns plaintext on decrypt. What the customer's key controls is whether Alien can load the encryption root protecting their data at all. [Alien Virtual Keys](/docs/encryption-gateway/virtual-keys) use a different data path, where AWS KMS calls Alien instead of your application sending values, so plaintext never reaches the gateway at all. See [Data and keys](/docs/encryption-gateway/security). ## Pick a path [#pick-a-path] There are three ways to use Encryption Gateway. Which one fits depends on what holds the data. | If the data lives in | Use | What your application changes | | ----------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------- | | Records your own application stores | [Encrypt/Decrypt API](#encrypt-application-records) | Calls Alien before writing and after reading | | A resource Alien built, such as `Storage` | [Native resource encryption](/docs/encryption-gateway/native-encryption) | Nothing — the key is declared in `alien.ts` | | An AWS resource in your own account — S3, Aurora, EBS | [Alien Virtual Key](/docs/encryption-gateway/virtual-keys) | Nothing — it passes an ordinary KMS key ARN | All three end in the same place: an encryption root protected by the key that customer connected. ## Encrypt application records [#encrypt-application-records] Your backend sends data to the Encrypt/Decrypt API. Alien encrypts it under a root protected by that customer's KMS key. ```typescript const encrypted = await fetch("https://encryption.alien.dev/v1/encrypt", { method: "POST", headers: { Authorization: `Bearer ${process.env.ALIEN_ENCRYPTION_KEY}`, "X-Alien-External-ID": customer.id, // [!code highlight] "Content-Type": "application/json", }, body: JSON.stringify({ key: { keyId: "customer-data" }, // [!code highlight] plaintext: Buffer.from(value).toString("base64"), }), }).then(response => response.json()) ``` Use this for fields, documents, credentials, or other data your application stores itself. `X-Alien-External-ID` selects whose key protects the value, and `keyId` names the cryptographic context it belongs to. ## Encrypt a resource built with Alien [#encrypt-a-resource-built-with-alien] Attach a `Key` to a supported resource in `alien.ts`. Alien carries the relationship into every customer deployment. ```typescript title="alien.ts" import * as alien from "@alienplatform/core" const customerKey = new alien.Key("customer-key").build() const data = new alien.Storage("customer-data") .encryptionKey(customerKey) // [!code highlight] .build() export default new alien.Stack("app") .add(customerKey, "frozen", { remoteAccess: true }) .add(data, "frozen") .build() ``` See [Native resource encryption](/docs/encryption-gateway/native-encryption). ## Encrypt AWS resources with an Alien Virtual Key [#encrypt-aws-resources-with-an-alien-virtual-key] For S3, Aurora, EBS, and other AWS services **running in your own AWS account**, create an ordinary AWS KMS key backed by Alien's External Key Store integration. The resource uses that KMS key ARN as usual, and your application keeps using the AWS SDK. Nothing moves into the customer's cloud on this path. Your infrastructure stays where it is; the customer holds only the key that makes their data readable. Their key does not have to live in AWS either — the S3 bucket you run for them can be encrypted under a key they hold in Google Cloud KMS or Azure Key Vault. See [Alien Virtual Keys](/docs/encryption-gateway/virtual-keys). Use this when AWS already knows how to encrypt the resource and you do not want your application calling a separate encryption API. ## Latency [#latency] Encryption sits in the path of every read and write of a protected field, so the cost has to stay small. Alien loads a customer's encryption root through their cloud KMS once, then caches it for five minutes. Encrypt and decrypt calls made against a loaded root do not reach the customer's cloud at all — they are local AES-GCM operations inside the gateway. Your steady-state cost is one HTTPS round trip to Alien, not a round trip to the customer's KMS. That cache is also why revocation is not instantaneous. The same design that keeps per-record encryption affordable is the reason a disabled key can take up to five minutes to take effect. For [Alien Virtual Keys](/docs/encryption-gateway/virtual-keys) the shape is different — AWS KMS calls Alien on the encryption path, and AWS asks that external key stores sit within about 35 ms of the Region. AWS services request a data key and reuse it while the resource is in use, so that cost lands on resource start-up rather than on every query. Disabling the KMS key stops Alien from loading that customer's encryption root. The Encrypt/Decrypt API caches a loaded root for up to five minutes, so revocation is not instantaneous, and it does not delete ciphertext your application already stored. Decide how your product behaves in that state — see [Data and keys](/docs/encryption-gateway/security). # Integrate with your product (/docs/encryption-gateway/integrate-with-your-product) Add Encryption Gateway to your product as a BYOK integration: ```text your settings page → customer connects a cloud key → your backend encrypts their data ``` The customer can use AWS KMS, Google Cloud KMS, or Azure Key Vault. Alien gets the access needed to use the key; your application never receives the customer's cloud credentials or raw key material. ## 1. Choose an external ID [#1-choose-an-external-id] An `externalId` is your stable identifier for a customer or tenant, such as `org_123`. Use the same value when you create their setup link and when you encrypt or decrypt data for them. Use an immutable database ID, not a display name or a value supplied by the browser. Resolve it from the authenticated customer on your backend. ## 2. Enable Encryption Gateway [#2-enable-encryption-gateway] Enable key setup for the project and create a gateway key for encrypt and decrypt requests: ```bash alien projects capabilities enable encryption alien api-keys create \ --for encryption-gateway \ --description production ``` Save the returned secret as `ALIEN_ENCRYPTION_KEY`. ## 3. Add a Connect key button [#3-add-a-connect-key-button] When the customer opens your BYOK settings, create a setup link on your backend and redirect them to `deploymentLink`. Setup links require a server-side **Alien Platform API key** (`ALIEN_API_KEY`). The **Encryption Gateway key** (`ALIEN_ENCRYPTION_KEY`) is for encrypt and decrypt requests and cannot manage connections. ```typescript import { Alien } from "@alienplatform/platform-api" const alien = new Alien({ apiKey: process.env.ALIEN_API_KEY }) const setup = await alien.setupLinks.create({ project: "my-project", externalId: customer.id, name: customer.slug, setupItems: [{ item: "keys", required: true }], }) return setup.deploymentLink ``` ```bash curl --fail-with-body \ "https://api.alien.dev/v1/deployment-groups/setup-links?workspace=my-workspace" \ -H "Authorization: Bearer $ALIEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "my-project", "externalId": "org_123", "name": "acme", "setupItems": [{"item": "keys", "required": true}] }' ``` Redirect the customer to `deploymentLink` from the response. Calling this endpoint again with the same project and `externalId` reuses the customer record and returns a fresh link. Create links when the customer clicks Connect or Reconnect, not while rendering the page. ### Open a specific cloud [#open-a-specific-cloud] For a generic **Connect key** button, omit `entryPoint` and let the customer choose. For a cloud-specific button, add: ```json { "entryPoint": {"item": "keys", "provider": "aws"} } ``` The customer can still go back and choose another supported cloud. To make the link AWS-only, also add: ```json { "deploymentSetupConfig": { "policy": {"allowedPlatforms": ["aws"]} }, "entryPoint": {"item": "keys", "provider": "aws"} } ``` Omit `deploymentSetupConfig` for the normal flow. See [Setup Links API](/docs/reference/api/setup-links) for advanced restrictions. ## 4. Encrypt customer data [#4-encrypt-customer-data] After setup completes, send the same external ID in `X-Alien-External-ID`. The API accepts and returns base64; `aGVsbG8=` is `hello`. ```typescript const encrypted = await fetch("https://encryption.alien.dev/v1/encrypt", { method: "POST", headers: { Authorization: `Bearer ${process.env.ALIEN_ENCRYPTION_KEY}`, "X-Alien-External-ID": customer.id, "Content-Type": "application/json", }, body: JSON.stringify({ key: { keyId: "customer-data" }, plaintext: Buffer.from("hello").toString("base64"), }), }).then(response => response.json()) ``` `keyId` names a cryptographic context within the customer connection. Use different values such as `documents`, `credentials`, or `customer-data` when those values should be cryptographically separated. The REST equivalent is: ```bash curl --fail-with-body \ "https://encryption.alien.dev/v1/encrypt" \ -H "Authorization: Bearer $ALIEN_ENCRYPTION_KEY" \ -H "X-Alien-External-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "key": {"keyId": "customer-data"}, "plaintext": "aGVsbG8=" }' ``` Store the returned ciphertext with the `externalId` and `keyId` needed to decrypt it. ## 5. Decrypt customer data [#5-decrypt-customer-data] Send the ciphertext back with the same external ID and key ID: ```bash curl --fail-with-body \ "https://encryption.alien.dev/v1/decrypt" \ -H "Authorization: Bearer $ALIEN_ENCRYPTION_KEY" \ -H "X-Alien-External-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "key": {"keyId": "customer-data"}, "ciphertext": "'$CIPHERTEXT'" }' ``` The response contains the plaintext as base64. See the [Encryption Gateway quickstart](/docs/encryption-gateway/quickstart) for a complete command-line test. ## 6. Show connection status [#6-show-connection-status] Use the capability overview to render the customer's BYOK status: ```typescript const overview = await alien.projects.getCapabilityOverview({ idOrName: "my-project", }) const group = overview.groups.find( group => group.externalId === customer.id, ) const keys = group?.capabilities.keys ``` Use `keys.state` as the top-level UI state: | State | Show in your product | | ----------------- | ------------------------------- | | `not-connected` | Connect key | | `setting-up` | Setup in progress | | `connected` | Key connected | | `needs-attention` | Reconnect or restore key access | | `revoked` | Key disconnected | To reconnect or change clouds, create another setup link with the same `externalId`. If the customer disables or deletes their cloud key, encrypt and decrypt fail after any cached key material expires. Continue reading the overview until the UI reflects the new state. ## Choose how to use the key [#choose-how-to-use-the-key] The examples above use the [Encrypt/Decrypt API](/docs/encryption-gateway/quickstart), which is best for fields, documents, credentials, and other data your application stores itself. To protect Aurora, S3, DynamoDB, or EBS without sending plaintext through the Encrypt/Decrypt API, use an [AWS Virtual Key](/docs/encryption-gateway/virtual-keys). Your AWS resource sees a normal KMS key while your customer controls the key material behind it. ## Test the complete flow [#test-the-complete-flow] For every cloud you offer: 1. connect a key from an account you control and confirm the state reaches `connected`; 2. encrypt a value and decrypt the returned ciphertext; 3. disable access to the cloud key and wait longer than the five-minute root cache; 4. confirm decrypt fails and your UI reports that the connection needs attention; and 5. restore access and confirm decrypt succeeds again. Tell customers what disabling or deleting their key does. Your application can retain ciphertext, but permanently losing the key can make that ciphertext unrecoverable. Inspect aggregate usage with: ```bash alien usage encryption --range 24h alien usage encryption --range 30d --json ``` # Native resource encryption (/docs/encryption-gateway/native-encryption) Use this path when Alien creates the resource that stores the data. You describe the key once in `alien.ts`; each customer connects the cloud KMS key for their deployment. ```typescript title="alien.ts" import * as alien from "@alienplatform/core" const customerKey = new alien.Key("customer-key").build() const data = new alien.Storage("customer-data") .encryptionKey(customerKey) .build() export default new alien.Stack("app") .add(customerKey, "frozen", { remoteAccess: true }) .add(data, "frozen") .build() ``` The important line is: ```typescript new alien.Storage("customer-data").encryptionKey(customerKey) ``` `encryptionKey` accepts an `alien.Key`, not a string or an arbitrary resource. Keep the key and storage in the same stack. ## What happens for each customer [#what-happens-for-each-customer] ```text alien.ts │ ├── Key("customer-key") └── Storage("customer-data") uses that key │ ▼ customer deployment │ ├── customer connects their cloud KMS key └── storage is encrypted with the connected key ``` The `remoteAccess: true` option lets your product connect the customer to this key through the [product integration flow](/docs/encryption-gateway/integrate-with-your-product). Use the same resource ID—`customer-key` in this example—when you create that handoff. For data stored by your own application, use the [Encrypt/Decrypt API](/docs/encryption-gateway/quickstart). For AWS services that already accept a KMS key ARN, use an [Alien Virtual Key](/docs/encryption-gateway/virtual-keys). # Quickstart (/docs/encryption-gateway/quickstart) By the end of this quickstart you will have encrypted a value under a cloud key you connected as a test customer, decrypted it again, and watched decryption fail once that key is disabled. Every step uses the CLI so it is copyable. Run them from a directory linked to your Alien project. ## 1. Enable Encryption Gateway [#1-enable-encryption-gateway] ```bash alien projects capabilities enable encryption alien api-keys create \ --for encryption-gateway \ --description local-quickstart ``` The secret is shown once. Save it with the customer ID you will use for this test: ```bash export ALIEN_ENCRYPTION_KEY="..." export CUSTOMER_ID="org_123" ``` ## 2. Connect your test key [#2-connect-your-test-key] ```bash alien onboard "Test customer" \ --external-id "$CUSTOMER_ID" \ --setup-items keys ``` Open the returned link and connect a key from an AWS, Google Cloud, or Azure account you control. ## 3. Encrypt one value [#3-encrypt-one-value] The CLI can print the request for the active Alien environment: ```bash alien examples encryption-gateway \ --operation encrypt \ --key-id customer-data ``` Generated examples currently reference `$ALIEN_ENCRYPTION_API_KEY`. Substitute the value you saved as `ALIEN_ENCRYPTION_KEY`, or export it under both names while running this quickstart. Run the printed command. The API accepts base64 and returns base64; `aGVsbG8=` is `hello`. ```bash curl "https://encryption.alien.dev/v1/encrypt" \ -H "Authorization: Bearer $ALIEN_ENCRYPTION_KEY" \ -H "X-Alien-External-ID: $CUSTOMER_ID" \ -H "Content-Type: application/json" \ -d '{ "key": {"keyId": "customer-data"}, "plaintext": "aGVsbG8=" }' ``` Save the returned ciphertext: ```bash export CIPHERTEXT="..." ``` ## 4. Decrypt it [#4-decrypt-it] ```bash alien examples encryption-gateway \ --operation decrypt \ --key-id customer-data ``` The printed request uses `ALIEN_ENCRYPTION_KEY`, `CUSTOMER_ID`, and `CIPHERTEXT`. The response contains the plaintext as base64. ## 5. Test disabled access [#5-test-disabled-access] Disable the test key in the customer cloud, wait longer than the five-minute root cache, then try decrypt again. The reload through the customer KMS should fail. Restore access and verify that decrypt works again. Search request diagnostics if the result is unexpected: ```bash alien logs --source encryption-gateway \ --operation decrypt \ --since 1h ``` Only after this test should you decide how your product behaves while the customer key is unavailable. # Data and keys (/docs/encryption-gateway/security) ## Encrypt/Decrypt API [#encryptdecrypt-api] Plaintext is sent to Encryption Gateway for encryption and returned by Encryption Gateway after decryption. BYOK does not mean the gateway never handles plaintext. The customer’s cloud KMS key protects the root used to encrypt their data. Raw KMS key material is not returned to your application. Each ciphertext is tied to its cryptographic context, including the Alien workspace, project, customer or deployment, and `keyId`. Changing that context causes decrypt to fail. ## Alien Virtual Keys [#alien-virtual-keys] With an Alien Virtual Key, the AWS resource in your account talks to AWS KMS, and AWS KMS calls Alien through XKS for cryptographic operations. This is a different data path from the public Encrypt/Decrypt API: AWS encrypts the data key with its own key material before sending it, so the gateway receives ciphertext and never handles plaintext on this path. ## Revocation [#revocation] The Encrypt/Decrypt API caches a loaded encryption root for five minutes. If the customer disables the KMS key or removes access, requests may continue to use that cached root until it expires. After expiry, Alien must load the root through the customer’s KMS again; that load fails while access remains disabled. ```text first request Alien loads the root through the customer's KMS next five minutes requests may use the cached root cache expires Alien asks the customer's KMS again access disabled that reload fails ``` Existing ciphertext remains in your storage. Revoking access does not delete it. Design and test your product’s behavior for that state. Do not describe revocation as instantaneous, deletion, or permanent inaccessibility. ## Application responsibilities [#application-responsibilities] * Keep the Alien API key in server-side secret storage. * Derive the customer ID from authenticated tenant context. * Use stable, intentional `keyId` values. * Store ciphertext, not plaintext, after encryption. * Do not log plaintext, decrypted responses, API keys, or customer credentials. # AWS Virtual Keys (/docs/encryption-gateway/virtual-keys) An Alien Virtual Key puts a resource in **your own AWS account** — an Aurora database, an S3 bucket, a DynamoDB table, an EBS volume — under a key that your customer owns and can revoke. Nothing moves into the customer's cloud. You keep running the infrastructure you already run, in the account you already run it in. The customer holds one thing: the key that makes their data readable. AWS services already know how to encrypt with KMS. An Alien Virtual Key gives them an ordinary KMS key to point at, while Alien serves the key material behind it through AWS KMS External Key Store (XKS). Your resource sees a normal key ARN. Your customer sees a key they can disable. Take the Aurora PostgreSQL database above as the example. When Aurora encrypts a page, it asks AWS KMS for a data key exactly as it always has. Because that KMS key is an Alien Virtual Key, KMS hands the data key to Alien over XKS, and Alien wraps it under the cloud key your customer connected. Aurora, your application, and your Terraform never learn that anything unusual happened. **Their key does not have to be in AWS.** It can live in their AWS KMS, Google Cloud KMS, or Azure Key Vault, and your AWS resource is encrypted under it either way. A customer standardized on Azure Key Vault can govern the Aurora database you run for them in your own account. AWS encrypts the data key with its own key material *before* sending it, so Alien receives ciphertext and returns ciphertext. The plaintext data key never leaves AWS. That makes this a stricter data path than the [Encrypt/Decrypt API](/docs/encryption-gateway/quickstart), where your application does send plaintext to be encrypted. Because neither party can decrypt alone, a customer who permanently revokes access leaves the data cryptographically unrecoverable — AWS calls this [double encryption](https://docs.aws.amazon.com/kms/latest/developerguide/keystore-external.html). This is the narrowest way to give a customer real control: your architecture is unchanged, your data stays in your account, and the only thing that moves to the customer is the key. If a customer needs your application itself running in their environment, that is [BYOC](/docs/how-alien-works) instead. ## Latency [#latency] The key is on the encryption path, so distance matters. AWS advises placing external key store components in the Region closest to the external key manager, and recommends a network round-trip time of **35 ms or less** between them. This is less alarming in practice than it sounds, because AWS services do not call KMS per row or per object. Aurora, S3, and EBS request a data key and reuse it while the resource is in use, so the XKS round trip happens on those key requests rather than on every read and write. Plan for it at resource start-up and failover rather than in your steady-state query path. AWS states plainly that external key stores carry greater availability and latency risk than standard KMS keys, and that for most workloads that cost exceeds the benefit. Offer Virtual Keys to customers whose requirements actually call for it, and keep standard encryption for the rest. ## Create the key [#create-the-key] Create it with the module shown in the dashboard, running Terraform with credentials for **your** AWS account. `deployment_id` names the customer whose key backs it: ```hcl module "encryption_key" { source = "pkg.alien.dev/alien/virtual-key/aws" version = "0.1.0" deployment_id = "dep_example" alias = "customer-data" } output "kms_key_arn" { value = module.encryption_key.kms_key_arn } ``` ## Point a resource at it [#point-a-resource-at-it] Pass the resulting KMS key ARN to an AWS resource exactly as you would any other customer-managed key: ```hcl resource "aws_s3_bucket_server_side_encryption_configuration" "documents" { bucket = aws_s3_bucket.documents.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" kms_master_key_id = module.encryption_key.kms_key_arn } } } ``` The same key ARN can be used by Aurora, DynamoDB, EBS, or a direct `aws kms encrypt` call. AWS KMS calls Alien on the encryption path; your application does not call `/v1/encrypt` for these resources. Alien Virtual Keys protect AWS resources, because they rely on AWS KMS XKS. There is no equivalent for Google Cloud or Azure resources yet. Your customer's own key is unrestricted — AWS KMS, Google Cloud KMS, and Azure Key Vault all work, here and in the Encrypt/Decrypt API. # Add AI to a Worker (/docs/examples/ai-quickstart) In this example, we are going to build an AI endpoint inside a customer's cloud. `GET /models` lists the models available through that cloud account, and `GET /ask` sends a question to one of them. The model call happens inside the Worker: Amazon Bedrock on AWS, Vertex AI on Google Cloud, or Azure AI Foundry on Azure. This lets an application process customer data with the model service in the same cloud account without putting a provider key in its source code or sending the request through your hosted backend. We will first describe the Worker and AI resource in `alien.ts`. Then we will implement both HTTP routes in `src/index.ts` and try them locally. The AI resource gives code in the deployment one model API across AWS Bedrock, Google Vertex AI, and Azure AI Foundry. `.link(assistant)` makes that API available to this Worker. In a cloud deployment, Alien uses the Worker's cloud identity; your source does not contain a provider API key. ## Describe the Worker and model access [#describe-the-worker-and-model-access] ```ts title="alien.ts" const assistant = new alien.AI("assistant").build() const api = new alien.Worker("api") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .publicEndpoint("api") .link(assistant) .permissions("execution") .build() export default new alien.Stack("ai-quickstart") .add(assistant, "live") .add(api, "live") .permissions({ profiles: { execution: { "*": ["ai/invoke"] } } }) .build() ``` Linking `assistant` makes it available inside the Worker. The permission profile allows the Worker to invoke it. ## Show the models this deployment can use [#show-the-models-this-deployment-can-use] ```ts title="src/index.ts" app.get("/models", async c => { const models = await ai("assistant").getAvailableModels() return c.json({ models }) }) ``` Use the returned IDs in a model picker or choose one in your application. ## Send a question [#send-a-question] ```ts title="src/index.ts" app.get("/ask", async c => { const question = c.req.query("q") if (!question) return c.json({ error: "pass ?q=..." }, 400) const assistant = ai("assistant") const models = await assistant.getAvailableModels() const model = c.req.query("model") ?? models[0]?.id if (!model) return c.json({ error: "no models available" }, 500) const completion = await assistant.chat.completions.create({ model, messages: [{ role: "user", content: question }], }) return c.json({ answer: completion.choices[0]?.message?.content ?? "" }) }) ``` ## Run it locally [#run-it-locally] ```bash alien init ai-quickstart-ts cd ai-quickstart-ts OPENAI_API_KEY=sk-... alien dev ``` Local development uses `OPENAI_API_KEY`. A cloud deployment uses the AI configuration available there. ```bash curl http://localhost:/models curl 'http://localhost:/ask?q=Reply+with+one+word:+pong' ``` ## Put it in a customer's cloud [#put-it-in-a-customers-cloud] ## What you built [#what-you-built] You built an AI endpoint whose compute, data handling, and model request can stay in the customer's cloud account. The same application code discovers and invokes the models available there without hard-coding provider credentials into the Worker. Complete source: [`ai-quickstart-ts`](https://github.com/alienplatform/alien/tree/main/examples/ai-quickstart-ts). To route requests from your hosted backend to a provider connected by each customer, follow [Use a customer's model provider](/docs/examples/customer-models). # Route Commands to multiple services (/docs/examples/command-routing) In this example, we are going to operate two services inside a customer deployment without giving either one a public admin endpoint. An API Worker and an indexer Daemon both expose a `status` Command; your product selects which service should receive each invocation. This is the pattern for health checks, diagnostics, synchronization, and other product operations that must execute where the service runs. The result returns to your product, but the customer's network does not accept an inbound connection from it. Command names only need to be unique within a target resource. This lets related services expose a consistent operation such as `status` without inventing names such as `api-status` and `indexer-status`. We will name both resources in `alien.ts`, register their handlers, and use `target()` in the calling application to choose one. Your control plane sends the operation through Alien and names the deployment and target resource. The API Worker and indexer Daemon do not need public endpoints for Commands. A long-running Daemon leases its work over outbound HTTPS; the customer's network accepts no inbound connection from your product. ## Describe both Command receivers [#describe-both-command-receivers] ```ts title="alien.ts" const api = new alien.Worker("api") .commandsEnabled(true) .link(index) .build() const indexer = new alien.Daemon("indexer-daemon") .commandsEnabled(true) .link(index) .build() ``` A Worker is event-driven compute. A Daemon is a long-running process for continuous work such as indexing or synchronization. Here the Daemon leases its Commands over outbound HTTPS, while the Worker receives Commands through the Alien Worker runtime. ## Choose the receiver when you invoke [#choose-the-receiver-when-you-invoke] ```ts title="services/sender/src/index.ts" const client = new CommandsClient({ managerUrl, deploymentId, token }) const apiStatus = await client.target("api").invoke("status", {}) const indexerStatus = await client.target("indexer-daemon").invoke("status", {}) ``` The Command name is identical. `target()` determines which resource receives it. ## Run the complete example [#run-the-complete-example] ```bash cd examples/command-routing-ts alien dev ALIEN_MANAGER_URL= \ ALIEN_DEPLOYMENT_ID= \ ALIEN_TOKEN= \ bun services/sender/src/index.ts ``` The sender invokes `status` twice with different targets. The command name is the same, but the resource named in the request selects the handler. Read `services/api` for the Worker, `services/indexer` for the Daemon receiver, and `services/sender` for the client. ## Deploy both receivers together [#deploy-both-receivers-together] ## What you built [#what-you-built] You gave your product a narrow operational interface to two services inside a customer deployment. Neither service needs a public admin port, and adding more services does not require a global namespace of prefixed Command names: the caller selects the deployment, resource, and operation explicitly. Source: [`examples/command-routing-ts`](https://github.com/alienplatform/alien/tree/main/examples/command-routing-ts). Next: [Commands](/docs/commands), [Daemon](/docs/infrastructure/daemon). # Add BYOK to your product (/docs/examples/customer-keys) In this tutorial you add BYOK to an existing application: instead of sensitive fields being encrypted with a key you hold, each customer's data is encrypted under a key that customer owns and can revoke. We start with individual fields: your backend sends a value to Alien Encryption Gateway before storing it, the gateway encrypts it under a root protected by that customer's AWS KMS, Google Cloud KMS, or Azure Key Vault key, and your database stores the returned ciphertext. At the end we cover the other path — putting a whole AWS resource under the customer's key without touching application code at all. Your backend does not receive the customer's cloud credentials or raw key material. It does handle the plaintext it sends for encryption and receives after decryption. ## Choose one field to protect [#choose-one-field-to-protect] Start with a field your application already stores, such as an OAuth refresh token: ```typescript title="src/integrations.ts" await db.integration.create({ data: { customerId: customer.id, provider: "github", refreshToken, }, }) ``` We will replace `refreshToken` with ciphertext before the record reaches the database. ## Enable Encryption Gateway [#enable-encryption-gateway] ```bash alien projects capabilities enable encryption alien api-keys create \ --for encryption-gateway \ --description production-backend ``` The secret is shown once. Store it as `ALIEN_ENCRYPTION_KEY`. Never put it in browser code. ## Let the customer connect their key [#let-the-customer-connect-their-key] ```bash alien onboard "Acme" \ --external-id org_123 \ --setup-items keys ``` The customer opens this link and chooses a key from AWS KMS, Google Cloud KMS, or Azure Key Vault. Alien receives the access needed to protect that customer's encryption root. Your application receives none of the customer's cloud credentials. In a real product, create the link from your backend when the customer opens your BYOK settings. See [Integrate with your product](/docs/encryption-gateway/integrate-with-your-product) for the TypeScript SDK and REST API. ## Add a small encryption client [#add-a-small-encryption-client] ```typescript title="src/encryption.ts" const endpoint = "https://encryption.alien.dev/v1" async function callEncryptionGateway( path: "encrypt" | "decrypt", customerId: string, body: object, ): Promise { const response = await fetch(`${endpoint}/${path}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.ALIEN_ENCRYPTION_KEY}`, "X-Alien-External-ID": customerId, "Content-Type": "application/json", }, body: JSON.stringify(body), }) if (!response.ok) { throw new Error(`Encryption Gateway returned ${response.status}`) } return response.json() as Promise } ``` The API key selects your Alien project. `X-Alien-External-ID` selects the customer's connected key. Always derive the customer ID from the authenticated server-side account. Do not accept an arbitrary value from browser input. ## Encrypt before writing to the database [#encrypt-before-writing-to-the-database] The API accepts and returns base64: ```typescript title="src/encryption.ts" export async function encryptSecret(customerId: string, value: string) { const result = await callEncryptionGateway<{ ciphertext: string }>( "encrypt", customerId, { key: { keyId: "integration-tokens" }, plaintext: Buffer.from(value, "utf8").toString("base64"), }, ) return result.ciphertext } ``` Use a stable `keyId` that describes the data, not the customer. The customer is already selected by the request header. Separate IDs such as `integration-tokens`, `documents`, and `credentials` create separate cryptographic contexts. Now store the ciphertext: ```typescript title="src/integrations.ts" const encryptedRefreshToken = await encryptSecret(customer.id, refreshToken) await db.integration.create({ data: { customerId: customer.id, provider: "github", encryptedRefreshToken, }, }) ``` The database no longer receives the plaintext refresh token. ## Decrypt when the application needs it [#decrypt-when-the-application-needs-it] Decrypt with the same customer ID and `keyId`: ```typescript title="src/encryption.ts" export async function decryptSecret(customerId: string, ciphertext: string) { const result = await callEncryptionGateway<{ plaintext: string }>( "decrypt", customerId, { key: { keyId: "integration-tokens" }, ciphertext, }, ) return Buffer.from(result.plaintext, "base64").toString("utf8") } ``` ```typescript title="src/integrations.ts" const refreshToken = await decryptSecret( customer.id, integration.encryptedRefreshToken, ) await refreshGithubToken(refreshToken) ``` Decrypt fails if the request uses another Alien project, another customer connection, another `keyId`, or different associated data. ## Bind ciphertext to a record [#bind-ciphertext-to-a-record] For especially sensitive fields, include associated data that must match at decrypt time. Encode it as base64 just like the plaintext: ```typescript const associatedData = Buffer.from( `integration:${integration.id}:refresh-token`, "utf8", ).toString("base64") const encrypted = await callEncryptionGateway<{ ciphertext: string }>( "encrypt", customer.id, { key: { keyId: "integration-tokens" }, plaintext: Buffer.from(refreshToken).toString("base64"), associatedData, }, ) ``` Send the exact same `associatedData` when decrypting. This prevents ciphertext copied from one record or purpose from being decrypted as another. ## Test customer control [#test-customer-control] Disable the test key in the customer's cloud, wait longer than the five-minute encryption-root cache, and try decrypting again. Encryption Gateway should fail when it has to reload the root through the disabled KMS or Key Vault key. Restore access and verify that decrypt works again: ```bash alien logs --source encryption-gateway \ --operation decrypt \ --since 1h ``` Disabling the cloud key does not delete ciphertext, and access is not guaranteed to stop immediately because a loaded root may remain cached for up to five minutes. Decide how your product behaves while the customer's key is unavailable. ## Encrypt a whole AWS resource instead [#encrypt-a-whole-aws-resource-instead] The Encrypt/Decrypt API is the right tool for individual fields. For a resource in **your own AWS account** that AWS already knows how to encrypt — an Aurora database, an S3 bucket, an EBS volume — an [Alien Virtual Key](/docs/encryption-gateway/virtual-keys) gets the same customer control with no application code at all. Your infrastructure does not move. Run the module below with credentials for your own AWS account. `deployment_id` names the customer whose key should protect the resource: ```hcl module "encryption_key" { source = "pkg.alien.dev/alien/virtual-key/aws" version = "0.1.0" deployment_id = "dep_example" # [!code highlight] alias = "acme-database" } ``` Then point the resource at the resulting ARN exactly as you would any customer-managed KMS key: ```hcl resource "aws_rds_cluster" "acme" { cluster_identifier = "acme" engine = "aurora-postgresql" storage_encrypted = true kms_key_id = module.encryption_key.kms_key_arn # [!code highlight] } ``` That is the entire change. The database stays in your account, Aurora encrypts as usual, AWS KMS routes the data key through Alien, and Alien wraps it under the key Acme connected — which can be in their AWS KMS, Google Cloud KMS, or Azure Key Vault. Acme can revoke it at any time without either of you moving infrastructure. Compare the two paths before choosing: | | Encrypt/Decrypt API | Alien Virtual Key | | ----------------------- | ---------------------------------------- | --------------------------------- | | Good for | Fields inside records you store | Whole AWS resources you run | | Application changes | Encrypt before write, decrypt after read | None | | Who sends data to Alien | Your backend, as plaintext | AWS KMS, as an encrypted data key | | Works with | AWS, Google Cloud, Azure resources | AWS resources only | Both accept a customer key in any of the three clouds. ## What you added [#what-you-added] Your application still owns its records, its storage, and its AWS account. What changed is who controls the key: each customer's data is now encrypted under a root only their own KMS or Key Vault key can unlock, and they can withdraw that at any time using the key system they already operate. You have both tools for it now — the Encrypt/Decrypt API for individual fields, and an Alien Virtual Key for a whole AWS resource. Neither one moves your infrastructure. Continue with [Data and keys](/docs/encryption-gateway/security), or [integrate the connection flow](/docs/encryption-gateway/integrate-with-your-product) into your product. # Add BYO-LLM to your product (/docs/examples/customer-models) In this tutorial you add BYO-LLM to an existing application: instead of every model call running on your AI provider account, each call runs on an account your customer owns. Your backend keeps using the OpenAI or Anthropic SDK. The difference is that each request goes through Alien AI Gateway carrying the ID of the customer it is for, and the gateway sends it to the provider account that customer connected. For example, the same request for `byo/claude-opus-5` can use: * Amazon Bedrock in one customer's AWS account; * Vertex AI in another customer's Google Cloud project; * Azure AI Foundry, Anthropic, or Databricks for another customer. Your backend does not need AWS, Google Cloud, Azure, or provider credentials. It keeps one Alien API key and one model integration. ## Start with the request your product already makes [#start-with-the-request-your-product-already-makes] Suppose your backend currently calls OpenAI like this: ```typescript title="src/ai.ts" import OpenAI from "openai" const ai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }) const response = await ai.chat.completions.create({ model: "gpt-5.6-sol", messages: [{ role: "user", content: "Summarize this document" }], }) ``` Adding AI Gateway changes three values: ```typescript title="src/ai.ts" const ai = new OpenAI({ baseURL: "https://ai.alien.dev/v1", // [!code highlight] apiKey: process.env.ALIEN_AI_KEY, defaultHeaders: { "X-Alien-External-ID": customer.id, // [!code highlight] }, }) const response = await ai.chat.completions.create({ model: "byo/gpt-5.6-sol", // [!code highlight] messages: [{ role: "user", content: "Summarize this document" }], }) ``` The Alien API key selects your project. `customer.id` selects the customer's connection. `byo/gpt-5.6-sol` selects the model. Resolve `customer.id` from the authenticated account in your backend. Do not accept an arbitrary customer ID from browser input. `byo/` means that the model runs through the customer's own provider account. The rest of the ID is the model your application wants. AI Gateway removes the `byo/` prefix before calling the connected provider. ## Choose the models your product supports [#choose-the-models-your-product-supports] Open **Infrastructure → Models** in the Alien dashboard. Select the models and client APIs your application uses. You can do the same from the CLI: ```bash alien projects capabilities enable ai \ --model byo/claude-opus-5 ``` Mark a model as required only when your product cannot work without it. A required model limits customer setup to providers that can serve it. The dashboard shows which providers can serve each model. That matters because a model publisher and the service running the model are not always the same thing: Claude may come from Bedrock, Vertex AI, Azure AI Foundry, Anthropic, or Databricks. ## Create the server API key [#create-the-server-api-key] ```bash alien api-keys create \ --for ai-gateway \ --description production-backend ``` The secret is shown once. Store it in your backend as `ALIEN_AI_KEY`. Never put it in browser code. ## Let the customer connect a provider [#let-the-customer-connect-a-provider] For a test, create a setup link for a customer your application already knows as `org_123`: ```bash alien onboard "Acme" \ --external-id org_123 \ --setup-items models ``` Open the returned link. If Acme chooses AWS, they connect the AWS account and region where Bedrock is available. If they choose Google Cloud, they connect a project where Vertex AI is enabled. Direct Anthropic, OpenAI, and Databricks connections use the corresponding provider account instead. In a real product, create this link from your backend when the customer opens your BYO-LLM settings. See [Integrate with your product](/docs/ai-gateway/integrate-with-your-product) for the complete connection lifecycle. ## See what this customer can use [#see-what-this-customer-can-use] Model availability belongs to the customer connection. Ask AI Gateway what `org_123` can use now: ```bash curl "https://ai.alien.dev/v1/models" \ -H "Authorization: Bearer $ALIEN_AI_KEY" \ -H "X-Alien-External-ID: org_123" ``` The returned IDs use the same `byo/` namespace your application sends in model requests. ## Send the first request [#send-the-first-request] ```bash curl "https://ai.alien.dev/v1/chat/completions" \ -H "Authorization: Bearer $ALIEN_AI_KEY" \ -H "X-Alien-External-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "model": "byo/claude-opus-5", "messages": [{"role": "user", "content": "Say hello in five words."}] }' ``` If `org_123` connected AWS, this request uses Claude through Bedrock in that AWS account. The same application request uses Vertex AI when another customer connects Google Cloud. AI Gateway also supports OpenAI Responses and Anthropic Messages. You can keep the client API your application already uses, as long as the selected model and customer provider support it. ## Handle unavailable models [#handle-unavailable-models] A connected account does not guarantee that every model is ready. The model may need one-time activation, may not exist in the selected region, or may not have usable quota. Check `/v1/models` after setup and before showing a model as available. If a request fails, inspect diagnostics without exposing prompts or responses: ```bash alien logs --source ai-gateway \ --status provider-error \ --model byo/claude-opus-5 \ --since 1h ``` ## What you added [#what-you-added] Your product now has one model integration and a different provider connection for each customer. Customers control where their model requests run and which provider account governs that usage. Your backend never receives their provider credentials. Continue with [AI Gateway requests and routing](/docs/ai-gateway/routing), or [integrate the connection flow](/docs/ai-gateway/integrate-with-your-product) into your product. # Write to a customer's storage (/docs/examples/customer-storage) In this example, we are going to give every customer object storage in their own cloud while keeping the application backend hosted in yours. The backend will write a file, read it, list the bucket, and delete the file through one scoped interface. Files stay in the customer's AWS, Google Cloud, or Azure account. Your backend does not receive a general cloud credential, and you do not need to deploy application compute beside the bucket when storage operations are all you need. Because the backend only needs storage operations, this stack does not need a Worker or Container. A Remote Binding gives the backend a scoped Storage API for the selected deployment. We will declare the Storage resource in `alien.ts`, enable remote access, and use it from `src/vendor.ts`. Alien creates the corresponding bucket for each deployment; the backend does not need a cloud-provider credential. A Remote Binding is a resource-level API, not a tunnel into the customer's network. Your backend receives the Storage methods allowed for this deployment; it does not receive an AWS, GCP, or Azure credential and cannot use the binding to reach unrelated customer infrastructure. ## Describe the bucket in `alien.ts` [#describe-the-bucket-in-alients] ```ts title="alien.ts" const uploads = new alien.Storage("uploads").build() export default new alien.Stack("byob-storage") .add(uploads, "frozen", { remoteAccess: true }) .build() ``` `remoteAccess: true` lets your hosted backend use this Storage resource through Alien. Storage is `frozen`, so customer setup owns the bucket: an ordinary rollout cannot replace or delete it. Changing its infrastructure configuration requires setup authority again. ## Use the same storage API from your backend [#use-the-same-storage-api-from-your-backend] ```ts title="src/vendor.ts" const bindings = await Bindings.forRemoteDeployment({ deploymentId: process.env.ALIEN_DEPLOYMENT_ID!, token: process.env.ALIEN_API_TOKEN!, }) const uploads = bindings.storage("uploads") await uploads.put("hello.txt", new TextEncoder().encode("hello")) const object = await uploads.get("hello.txt") console.log(new TextDecoder().decode(object.data)) ``` Add a Worker only when code also needs to run with the Storage resource. ## Try the complete example [#try-the-complete-example] ```bash cd examples/byob-storage-ts pnpm install alien release ``` Enable `remoteAccess` only for resources your backend actually needs, and keep the Alien credential in server-side secret storage. Read `alien.ts` for the Storage resource and `src/vendor.ts` for the Remote Binding. The example creates a bucket during customer setup; it does not attach an arbitrary existing bucket. ## What you built [#what-you-built] You provisioned customer-owned state without moving application compute. Customer files stay in their cloud account, while your hosted backend receives only the Storage operations for the selected deployment—not a reusable AWS, Google Cloud, or Azure credential. Source: [`examples/byob-storage-ts`](https://github.com/alienplatform/alien/tree/main/examples/byob-storage-ts). Next: [Remote Bindings](/docs/remote-bindings), [Storage](/docs/infrastructure/storage). # Process background work (/docs/examples/event-pipeline) In this example, we are going to process queue messages and new storage objects inside the customer environment where they are created. An hourly schedule runs there too, and each handler records its result in KV. The useful part is location: your hosted product does not poll customer queues, copy every object into your cloud, or keep credentials for those resources. The Worker receives each event locally and your product asks for the result through a Command. Alien connects infrastructure events to ordinary TypeScript handlers. The same handler APIs work during local development and when the stack is deployed to AWS, Google Cloud, or Azure. We will declare the Queue, Storage, KV, and three triggers in `alien.ts`. Then we will implement each handler in `src/index.ts` and send a test message locally. Queue, storage, and schedule triggers start the Worker inside the deployment. Your control plane does not poll those resources across clouds. When it needs the result, it invokes `get-events` or `get-stats` through Commands—without an inbound admin port or a network tunnel into the customer environment. ## Connect events to the Worker [#connect-events-to-the-worker] ```ts title="alien.ts" const processor = new alien.Worker("processor") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .link(inbox) .link(data) .link(events) .trigger({ type: "queue", queue: inbox.ref() }) .trigger({ type: "storage", storage: data.ref(), events: ["created"] }) .trigger({ type: "schedule", cron: "0 * * * *" }) .permissions("execution") .build() ``` Each `.trigger(...)` connects a deployment resource or schedule to this Worker. Each `.link(...)` gives the handler a typed binding to the named Queue, Storage, or KV resource. The permission profile limits which data operations the Worker may perform. ## Write the handlers [#write-the-handlers] ```ts title="src/index.ts" onQueueMessage("*", async message => { await kv("events").setJson(`queue:${message.id}`, { payload: message.payload, processedAt: new Date().toISOString(), }) }) onStorageEvent("*", async event => { await kv("events").setJson(`storage:${event.objectKey}`, { objectKey: event.objectKey, eventType: event.eventType, }) }) onCronEvent("*", async event => { await kv("events").setJson(`cron:${Date.now()}`, { scheduledTime: event.timestamp, }) }) ``` Queue and storage handlers must be safe to run more than once. Use stable event IDs where duplicate work would be harmful. ## Send some work locally [#send-some-work-locally] ```bash alien init event-pipeline-ts alien dev alien dev commands invoke --deployment default --command send-test-message --params '{"message":"hello"}' ``` Wait for the message, then invoke `get-events` or `get-stats`. The complete source also shows an HTTP ingestion route and `waitUntil` for background work. ## Run the pipeline on customer infrastructure [#run-the-pipeline-on-customer-infrastructure] ## What you built [#what-you-built] You connected three infrastructure event sources to ordinary TypeScript handlers in the customer's cloud. Customer payloads can be processed and stored there; your product receives only the inspection results it asks for instead of operating a cross-cloud polling and data-copying service. Source: [`examples/event-pipeline-ts`](https://github.com/alienplatform/alien/tree/main/examples/event-pipeline-ts). Next: [Events and schedules](/docs/events), [Queue](/docs/infrastructure/queue). # Complete multi-service application (/docs/examples/full-application) In this example, we are going to deploy the entire data plane of a support application into a customer's Kubernetes cluster. It has a dashboard, API, background worker, scheduler, Postgres, Redis, and object storage. Tickets, attachments, cached data, and background jobs stay in that cluster. Only one gateway is public; the databases, storage, and internal services remain on the customer's private network. You still ship the application as one versioned product rather than maintaining a separate deployment project for every customer. We keep the existing service split. A public gateway receives browser traffic, while the dashboard, API, worker, databases, and storage communicate over the deployment's private network. We will read `alien.ts` as the service map: it points to each source directory or image, declares ports and environment variables, and links services to Storage. The application code remains in its normal service directories. The Containers are `live`, which gives Alien the ongoing authority needed to create, update, replace, or remove them during a rollout. Storage is `frozen`, so it remains owned by customer setup and an ordinary rollout cannot replace or delete it. Links and permission profiles separately control which services can use Storage at runtime. Only the gateway calls `publicEndpoint(...)`. The other Containers use deployment-local service names such as `api:3000` and `postgres:5432`; the customer does not need to expose every service or create public load balancers for internal traffic. ## Read `alien.ts` as a service map [#read-alients-as-a-service-map] The public gateway routes browser requests to the dashboard and API. Everything else uses private service names inside the deployment. ```ts title="alien.ts" const api = new alien.Container("api") .code({ type: "source", src: "./services/api", toolchain: { type: "typescript" } }) .port(3000) .environment({ DATABASE_URL: "postgres://app:app@postgres:5432/app", REDIS_URL: "redis://redis:6379", }) .link(files) .permissions("app") .build() const gateway = new alien.Container("gateway") .code({ type: "source", src: "./services/gateway", toolchain: { type: "docker" } }) .port(8080) .publicEndpoint("web", 8080, "http") .build() ``` The complete `alien.ts` defines each service separately, then adds all of them to one Kubernetes stack. ## Keep one public entry point [#keep-one-public-entry-point] The gateway is a normal nginx Container. It sends `/api/*` to the API and everything else to the dashboard using the resource names from `alien.ts`: ```nginx title="services/gateway/nginx.conf" upstream api { server api:3000; } upstream dashboard { server dashboard:3000; } server { listen 8080; location /api/ { proxy_pass http://api/; } location / { proxy_pass http://dashboard; } } ``` `api` and `dashboard` resolve inside the deployment. They are not public DNS records and do not need their own ingress configuration. The same rule applies to Postgres and Redis: ```ts title="alien.ts" .environment({ DATABASE_URL: "postgres://app:app@postgres:5432/app?sslmode=disable", REDIS_URL: "redis://redis:6379", }) ``` This example deliberately models Postgres and Redis as Containers because it is showing how an existing service graph maps onto Alien. Postgres gets a persistent volume; Redis is used as an ordinary private service. ```ts title="alien.ts" const postgres = new alien.Container("postgres") .code({ type: "image", image: "postgres:16-alpine" }) .port(5432) .persistentStorage("10Gi") .environment({ POSTGRES_DB: "app", POSTGRES_USER: "app", POSTGRES_PASSWORD: "app", PGDATA: "/data/postgres", }) .build() ``` For a production application, move the password into a deployment secret or use Alien's first-class Postgres resource when its binding and lifecycle fit the application. ## Link object storage to the services that use it [#link-object-storage-to-the-services-that-use-it] The API accepts file uploads and writes their contents to the `files` Storage resource: ```ts title="services/api/src/index.ts" const files = storage(process.env.FILES_BUCKET ?? "files") app.post("/issues/:id/files", async c => { const id = c.req.param("id") const payload = await c.req.json<{ filename: string; content: string }>() const fileId = randomUUID() const objectKey = `issues/${id}/${fileId}-${payload.filename}` await files.put(objectKey, new TextEncoder().encode(payload.content)) await db.query( "insert into issue_files (id, issue_id, object_key, filename) values ($1, $2, $3, $4)", [fileId, id, objectKey, payload.filename], ) return c.json({ objectKey }, 201) }) ``` Both the API and worker call `.link(files)` and use the `app` permission profile. The dashboard, gateway, scheduler, Postgres, and Redis do not receive the Storage binding because they do not use it. ## Move background work through Redis [#move-background-work-through-redis] Processing an issue is asynchronous. The API records the job in Redis and pushes it onto a list: ```ts title="services/api/src/index.ts" await redis.hset(`issue:${id}:job`, { status: "queued", queuedAt: new Date().toISOString(), }) await redis.lpush("work:issues", JSON.stringify({ issueId: id, requestedAt: Date.now(), })) ``` The worker blocks on that private Redis list, reads the issue from Postgres, writes a summary to Storage, and marks the issue as processed: ```ts title="services/worker/src/index.ts" const item = await redis.brpop("work:issues", 0) const { issueId } = JSON.parse(item![1]) const issue = await db.query( "select id, title, body from issues where id = $1", [issueId], ) const row = issue.rows[0] const artifactKey = `artifacts/${row.id}/summary.txt` await files.put( artifactKey, new TextEncoder().encode(`Issue: ${row.title}\n\n${row.body}`), ) await db.query( "update issues set status = $1, updated_at = now() where id = $2", ["processed", row.id], ) ``` All of these calls stay inside the Kubernetes deployment. Browser traffic enters once through the gateway; it does not connect directly to Redis, Postgres, or Storage. ## Add a private operational Command [#add-a-private-operational-command] The worker also exposes `reprocess` as an Alien Command: ```ts title="services/worker/src/index.ts" const receiver = createCommandReceiver() receiver.command("reprocess", async input => { if ( typeof input !== "object" || input === null || !("issueId" in input) || typeof input.issueId !== "string" ) { throw new TypeError("issueId must be a string") } const { issueId } = input await redis.lpush("work:issues", JSON.stringify({ issueId, requestedAt: Date.now(), })) return { requeued: true, issueId } }) void receiver.run().catch(error => { console.error("command receiver stopped", error) }) ``` Because this is a long-running Container, it leases Commands over outbound HTTPS. You can add an operational action for your control plane without publishing another endpoint or opening an inbound admin port in the cluster. ## Run scheduled work as another service [#run-scheduled-work-as-another-service] The scheduler is intentionally simple. Every minute it calls a private API route using `http://api:3000`: ```ts title="services/scheduler/src/index.ts" await fetch(`${process.env.API_URL}/internal/maintenance`, { method: "POST", headers: { "x-app-secret": process.env.APP_SECRET! }, }) ``` This is a useful migration pattern for an existing cron process: keep it as a separate Container first. You can change the implementation later without changing the rest of the service graph. ## Assemble the stack [#assemble-the-stack] The final stack makes Storage setup-owned and lets Alien reconcile every running service: ```ts title="alien.ts" export default new alien.Stack("full-stack-microservices") .platforms(["kubernetes"]) .add(files, "frozen") .add(postgres, "live") .add(redis, "live") .add(api, "live") .add(worker, "live") .add(scheduler, "live") .add(dashboard, "live") .add(gateway, "live") .permissions({ profiles: { app: { files: ["storage/data-read", "storage/data-write"], }, }, }) .build() ``` ## Build and release the complete stack [#build-and-release-the-complete-stack] ```bash cd examples/full-stack-microservices alien build --platform kubernetes alien release --platform kubernetes ``` After it is running, follow one request from `gateway` to `api`, then inspect the worker and scheduler. That is easier than reading every service at once. For a customer-owned cluster, release the application and let the customer's admin install it from the deployment portal: ## What you built [#what-you-built] You mapped a real multi-service application into one customer deployment without collapsing its architecture. The customer owns the cluster and application data; your release channel still updates the services as one product. Source: [`examples/full-stack-microservices`](https://github.com/alienplatform/alien/tree/main/examples/full-stack-microservices). Next: [Stacks and resources](/docs/stacks), [Where it can run](/docs/where-it-runs). # Build a GitHub agent (/docs/examples/github-agent) In this example, we are going to build a GitHub analytics product that keeps its dashboard in your cloud and runs each customer's GitHub integration in an isolated customer deployment. The hosted dashboard owns user accounts, organizations, charts, and historical metrics. A small Worker runs for each customer and owns the GitHub integration: it stores the repository configuration, calls the GitHub API, classifies pull requests, and returns the results the dashboard needs. This split keeps GitHub configuration and execution out of the shared SaaS backend. It is useful when an integration needs customer-specific isolation, cloud identity, private network access, or separate operational ownership. You deploy only that small piece—not the whole product—to a customer cloud, dedicated account, Kubernetes cluster, or another isolated environment. We will follow one organization from setup to its first repository analysis: 1. The dashboard creates a deployment group for the organization. 2. The customer deploys the Worker. 3. The dashboard sends the repository configuration to that deployment. 4. A workflow invokes `analyze-repository` and stores the returned metrics. 5. The browser requests detailed pull-request data from the deployment's HTTPS endpoint. ## Choose what belongs in each place [#choose-what-belongs-in-each-place] The hosted application remains an ordinary Next.js product. It uses Postgres for product data and the Alien Platform API to manage customer deployments. Only the GitHub-specific execution moves into the customer deployment: * a Worker named `agent` * a Vault named `integrations` * three Commands for configuration and analysis * one HTTPS endpoint for detailed pull-request results This keeps the remote piece small. Authentication, billing, organization membership, charts, and durable metric history do not need to be duplicated for every customer. ## Describe the remote piece [#describe-the-remote-piece] The remote package has its own `alien.ts`: ```ts title="packages/remote-agent/alien.ts" const integrations = new alien.Vault("integrations").build() const agent = new alien.Worker("agent") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .link(integrations) .memoryMb(512) .commandsEnabled(true) .publicEndpoint("api") .permissions("execution") .build() export default new alien.Stack("github-agent") .platforms(["aws", "gcp", "azure", "kubernetes"]) .add(integrations, "frozen") .add(agent, "live") .permissions({ profiles: { execution: {} } }) .build() ``` The two entry points serve different jobs: * **Commands** carry private product operations from the hosted backend. The Worker does not need an inbound admin port, VPN, or VPC peering for these calls. * **The HTTPS endpoint** serves pull-request data to the browser. It is public because the browser must be able to reach it. Do not confuse “Commands need no inbound port” with “this Worker has no public endpoint.” This example intentionally uses both communication paths. The Worker is `live`, so Alien can update its code during normal rollouts. Vault is `frozen`, so its infrastructure remains owned by customer setup. Linking Vault to the Worker and granting runtime permissions are separate from that lifecycle choice. ## Create one deployment group per organization [#create-one-deployment-group-per-organization] The dashboard groups deployments by product organization. When an organization is first created, the backend creates an Alien deployment group and a token the customer can use for setup: ```ts title="packages/dashboard/lib/deployment-groups.ts" const deploymentGroup = await alien.deploymentGroups.createDeploymentGroup({ name, project: config.project, maxDeployments: 10, }) const tokenResponse = await alien.deploymentGroups.createDeploymentGroupToken({ id: deploymentGroup.id, createDeploymentGroupTokenRequest: { description: `Deployment token for ${organizationName}`, }, }) ``` The hosted database stores the deployment group ID and token with the organization. Later, the dashboard lists only deployments in that group: ```ts title="packages/dashboard/lib/alien.ts" const response = await alien.deployments.list({ deploymentGroup: metadata.deploymentGroupId, }) ``` This is the multi-tenant join: the product organization points to its Alien deployment group, and each repository integration points to one deployment inside that group. ## Deploy the Worker [#deploy-the-worker] The dashboard turns the deployment-group token into a setup link. The customer opens that link and chooses where the Worker should run. For local development, start the remote package directly: ```bash cd examples/github-agent/packages/remote-agent alien dev ``` `alien dev` provides local Worker, Commands, and Vault implementations, so you can develop the remote package without deploying a cloud stack. ## Store the GitHub integration in Vault [#store-the-github-integration-in-vault] When a user adds a repository, the dashboard constructs an integration ID and invokes `set-integration` against the selected deployment: ```ts title="packages/dashboard/app/api/integrations/route.ts" const integrationId = `github-${activeOrgId}-${owner}-${repo}` .toLowerCase() .replace(/[^a-z0-9-]/g, "-") await invokeCommand(agentId, "set-integration", { integrationId, config: { owner, repo, token, baseUrl: baseUrl || undefined, }, }) ``` The token is submitted to the hosted backend and sent through the Command to the selected deployment. The hosted product database does not persist it; it stores only repository metadata and whether a token was supplied. Inside the Worker, the Command writes the configuration to Vault: ```ts title="packages/remote-agent/src/commands.ts" command("set-integration", setIntegrationSchema, async ({ integrationId, config }) => { const normalized = normalizeConfig(config) await saveIntegrationConfig(integrationId, normalized) return { ok: true } }) ``` ```ts title="packages/remote-agent/src/integrations.ts" export async function saveIntegrationConfig( integrationId: string, config: IntegrationConfig, ) { const integrations = await vault("integrations") await integrations.set(integrationId, config) } ``` This is more precise than saying the dashboard “never sees” the credential: it does receive it during setup. The important property in this example is that the hosted database does not retain it and later GitHub calls load it from Vault in the remote deployment. ## Expose product operations as Commands [#expose-product-operations-as-commands] The Worker registers named operations rather than a generic remote shell: ```ts title="packages/remote-agent/src/commands.ts" command("analyze-repository", integrationIdSchema, async ({ integrationId }) => { const config = await loadIntegrationConfig(integrationId) const pullRequests = await fetchPullRequests(config) const classified = classifyPullRequests(pullRequests) return computeMetrics(classified) }) command("label-pull-requests", integrationIdSchema, async ({ integrationId }) => { const config = await loadIntegrationConfig(integrationId) const pullRequests = await fetchPullRequests(config) const openPullRequests = pullRequests.filter(pr => pr.state === "open") for (const { pr, classification } of classifyPullRequests(openPullRequests)) { await applyLabels(config, pr.number, [ `size:${classification.size}`, `risk:${classification.risk}`, ]) } return { labeled: openPullRequests.length } }) ``` The callable surface is visible in code and validated with Zod. Adding a new operation requires a reviewed code change and a rollout of the Worker. ## Invoke the correct customer deployment [#invoke-the-correct-customer-deployment] The hosted backend first resolves connection information for the deployment, then creates a Commands client: ```ts title="packages/dashboard/lib/arc.ts" const info = await alien.deployments.getInfo({ id: deploymentId, }) const commands = new CommandsClient({ managerUrl: info.arc?.url || config.alienApiUrl, deploymentId: info.arc?.deploymentId || deploymentId, token: config.alienToken, }) const metrics = await commands.invoke("analyze-repository", { integrationId, }) ``` Alien delivers the named Command to that deployment and returns the handler result. The dashboard does not open a connection to the customer's network or receive the Vault binding. ## Sync remote results into the hosted product [#sync-remote-results-into-the-hosted-product] The dashboard runs a durable workflow that invokes `analyze-repository` and stores the returned aggregate metrics in its own database: ```ts title="packages/dashboard/workflows/sync-metrics.ts" const metrics = await invokeCommand( agentId, "analyze-repository", { integrationId }, ) const now = new Date() await db.insert(metricsHistory).values({ id: `metrics_${integrationId}_${now.getTime()}`, integrationId, totalPRs: metrics.totalPRs, avgTimeToFirstReviewHours: metrics.avgTimeToFirstReviewHours, avgMergeTimeHours: metrics.avgMergeTimeHours, reviewThroughputScore: metrics.reviewThroughputScore, }) ``` The remote Worker owns GitHub access and analysis. The hosted application owns historical product data and presentation. Only the returned metrics need to cross between them. ## Serve detailed pull-request data over HTTPS [#serve-detailed-pull-request-data-over-https] The Worker also exposes `GET /prs`. It loads the integration from Vault, calls GitHub, classifies the pull requests, and returns the detailed list: ```ts title="packages/remote-agent/src/endpoints.ts" app.get("/prs", async c => { const integrationId = c.req.query("integrationId") if (!integrationId) { return c.json({ error: "integrationId is required" }, 400) } const config = await loadIntegrationConfig(integrationId) const pullRequests = await fetchPullRequests(config) const classified = classifyPullRequests(pullRequests) return c.json({ integrationId, pullRequests: classified }) }) ``` The browser obtains the Worker's `publicUrl` from deployment state, verifies that it is an HTTPS URL, and fetches this route directly. This endpoint is intentionally open in the example. A production version must authenticate the caller and authorize access to the requested integration. Commands solve private backend-to-deployment operations; they do not automatically secure public HTTP routes. ## What you built [#what-you-built] You built a product with a hosted control plane and one small remote component per customer: * Each organization owns a deployment group. * The customer chooses where its Worker and Vault run. * Repository configuration is stored in the deployment's Vault. * The hosted backend invokes explicit operations through Commands without opening an inbound admin port. * Aggregate metrics return to the hosted product database. * Detailed pull-request data is served separately over an HTTPS endpoint. The point is not GitHub specifically. The same structure works for database connectors, internal search, security scanners, and agents that need credentials or network access you do not want to centralize in the hosted application. Complete source: [`examples/github-agent`](https://github.com/alienplatform/alien/tree/main/examples/github-agent). Next: [Commands](/docs/commands), [Vault](/docs/infrastructure/vault), and [Onboarding customers](/docs/deploying/onboarding-customers). # Build your first Worker (/docs/examples/hello-world) In this example, we are going to build the smallest useful piece of an application that can run in a customer's cloud: a serverless service with one HTTP route and one private operation your product can invoke. The code is deliberately simple. The point is to see the complete path: develop one service locally, deploy it to AWS, Google Cloud, Azure, or Kubernetes, and call it from your product without opening an admin port into the customer's network. A Worker is Alien's stateless, event-driven compute resource. It runs as AWS Lambda on AWS, Google Cloud Run on GCP, Azure Container Apps on Azure, or a Deployment and Service on Kubernetes. The same Worker can receive HTTP requests, Commands, queue messages, storage events, and schedules. We will write the Worker in TypeScript or Rust. During development it runs on your computer; after deployment, Alien creates the corresponding compute in your cloud or a customer's environment. We will start with `alien.ts`, the file that describes what Alien should build and run. Then we will write the HTTP and Command handlers in an ordinary application file and call both locally. A Command is a named request from your control plane to one customer deployment. It travels through Alien and returns the handler result without exposing the Worker on a public application port. The customer's network does not need a VPN, VPC peering, or an inbound firewall rule for your product. Unlike a generic remote shell, the callable operations are the handlers you define in code. ## Describe the application in `alien.ts` [#describe-the-application-in-alients] ```ts title="alien.ts" const agent = new alien.Worker("agent") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .commandsEnabled(true) .publicEndpoint("api") .permissions("execution") .build() ``` ```ts title="alien.ts" const agent = new alien.Worker("agent") .code({ type: "source", src: "./", toolchain: { type: "rust", binaryName: "basic-worker" }, }) .commandsEnabled(true) .publicEndpoint("api") .permissions("execution") .build() ``` `publicEndpoint("api")` creates an HTTP endpoint. `commandsEnabled(true)` lets the Worker register Command handlers. The endpoint and the Command solve different problems. `/health` intentionally accepts inbound HTTPS. `echo` remains reachable through the Commands API even if you remove the public endpoint entirely. The `execution` permission profile is also explicit: Alien turns that profile into the cloud permissions assigned to this Worker. ## Write the application code [#write-the-application-code] ```ts title="src/index.ts" const app = new Hono() app.get("/health", c => c.json({ status: "ok" })) command("echo", async params => params) export default app ``` ```rust title="src/bin/main.rs" let ctx = AlienContext::from_env().await?; ctx.on_command("echo", |params: Value| async move { Ok(params) }); let app = Router::new().route( "/health", get(|| async { Json(json!({ "status": "ok" })) }), ); ``` ## Run it locally [#run-it-locally] ```bash alien init basic-worker-ts cd basic-worker-ts alien dev ``` ```bash alien init basic-worker-rs cd basic-worker-rs alien dev ``` In another terminal: ```bash alien dev commands invoke \ --deployment default \ --command echo \ --params '{"hello":"world"}' ``` Then open the URL printed by `alien dev` and request `/health`. ## Deploy it for a customer [#deploy-it-for-a-customer] Local development proves that the handlers work. A release makes the same Worker available for customer deployments: ## Call it from your control plane [#call-it-from-your-control-plane] The CLI is useful while testing a deployment: ```bash alien commands invoke \ --deployment acme-corp \ --command echo \ --params '{"hello":"from the control plane"}' ``` Your product can call the same API with the TypeScript client: ```ts title="control-plane.ts" import { CommandsClient } from "@alienplatform/commands" const commands = await CommandsClient.forDeployment({ deploymentId: customer.deploymentId, apiKey: process.env.ALIEN_API_KEY!, }) const result = await commands .target("agent") .invoke("echo", { hello: "from the control plane" }) ``` The request starts in your control plane, but the handler executes inside the customer's environment. For a real integration, replace `echo` with a narrow operation such as `search-documents` or `generate-report`. That lets sensitive data stay near the customer's database or storage and returns only the result your product needs. It can also avoid moving large inputs back to your cloud before the work begins. ## What you built [#what-you-built] You built the smallest complete control-plane-to-customer-cloud loop: release code once, let a customer deploy it, and invoke a named operation from your product. The `echo` handler is deliberately simple; the useful pattern is where it executes and how little access your control plane needs. Complete source: [`basic-worker-ts`](https://github.com/alienplatform/alien/tree/main/examples/basic-worker-ts) and [`basic-worker-rs`](https://github.com/alienplatform/alien/tree/main/examples/basic-worker-rs). # Featured (/docs/examples) # Deploy a Next.js application (/docs/examples/nextjs-application) In this example, we are going to deploy an existing Next.js application into a customer's cloud and give that customer deployment its own HTTPS endpoint. This is useful when the whole application—or a customer-facing part of it—needs to live with the customer's data, use services on their private network, or run under their cloud account. You keep the ordinary Next.js server and Dockerfile; Alien handles one deployment per environment. Containers are useful when you already have a web server with a Dockerfile. You do not need to rewrite the application as a Worker or change how Next.js handles requests. Alien builds the image and runs it in each deployment. We will use `alien.ts` to point Alien at the Dockerfile, declare port `3000`, and create the public endpoint. The rest of the project remains an ordinary Next.js application. A Container runs a normal long-lived process from your image. Use it for an existing web server, persistent connections, or software that expects an ordinary filesystem and process lifecycle. A Worker is stateless, event-driven compute. This example uses a Container because Next.js already provides the server. ## Describe the Container in `alien.ts` [#describe-the-container-in-alients] ```ts title="alien.ts" const app = new alien.Container("app") .code({ type: "source", src: ".", toolchain: { type: "docker", dockerfile: "Dockerfile" }, }) .cpu(0.5) .memory("512Mi") .port(3000) .publicEndpoint("web", 3000, "http") .environment({ PORT: "3000", HOSTNAME: "0.0.0.0" }) .permissions("app") .build() ``` Alien builds the Dockerfile, starts the container on port `3000`, and creates the `web` endpoint. `publicEndpoint("web", 3000, "http")` intentionally makes this application reachable over HTTPS. That is different from Commands, which can reach a named handler without publishing an application port. ## Run Next.js normally [#run-nextjs-normally] ```bash cd examples/nextjs-app npm install npm run dev ``` Open `http://localhost:3000` and `http://localhost:3000/api/health` first. When you are ready to deploy, run: ## Deploy the same application [#deploy-the-same-application] ```bash alien deploy production --platform aws # or gcp / azure ``` Alien builds the included Dockerfile and prints the endpoint from the deployment. To let a customer's admin create the deployment in their own environment, release the application and share its deployment link: ## What you built [#what-you-built] You took an ordinary containerized Next.js application and made it deployable into customer-owned infrastructure. Each customer receives an isolated application endpoint in their environment, ready to be linked to private databases and cloud services there. The application itself remains a normal Next.js server. Source: [`examples/nextjs-app`](https://github.com/alienplatform/alien/tree/main/examples/nextjs-app). Next: [Container](/docs/infrastructure/container), [Networking and endpoints](/docs/networking). # Connect to a private database (/docs/examples/private-database) In this example, we are going to build a connector for a database that your hosted product cannot reach directly. A Worker reads the database connection from Vault and exposes named operations as Commands. The Worker runs in the same environment as the database. Your product sends an operation and receives its result; it does not need network access to the database or a copy of its password. We will describe the Worker, Vault, and KV cache in `alien.ts`. Then we will read the connection and implement the database operations in `src/index.ts`. The database and Worker remain private. Your control plane submits `test-connection`, `query`, or another handler through Alien and waits for its result. It does not open an inbound port into the customer's VPC, join their network through a VPN, or receive a general database connection. ## Describe the connector in `alien.ts` [#describe-the-connector-in-alients] ```ts title="alien.ts" const credentials = new alien.Vault("credentials").build() const cache = new alien.Kv("cache").build() const connector = new alien.Worker("connector") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .commandsEnabled(true) .link(credentials) .link(cache) .permissions("execution") .build() ``` Vault is secret storage created in the deployment. `.link(credentials)` makes its binding available only to the connector, and the permission profile grants the connector the specific Vault and KV operations listed by the stack. ## Read the connection where it is used [#read-the-connection-where-it-is-used] ```ts title="src/index.ts" async function getConnectionConfig() { const credentials = await vault("credentials") const raw = await credentials.get("database") return JSON.parse(raw) } ``` Your product calls the Command and receives its result. It does not need the database connection itself. ## Choose the operations your product can call [#choose-the-operations-your-product-can-call] ```ts title="src/index.ts" command("test-connection", async () => { const config = await getConnectionConfig() return { connected: true, database: config.database, host: config.host } }) command("query", querySchema, async ({ sql, useCache }) => { await getConnectionConfig() return runQuery(sql, { useCache }) }) ``` The example uses sample data locally. Replace `runQuery` with your database client. For a production connector, prefer specific operations over accepting arbitrary SQL. ## Run it locally [#run-it-locally] ```bash alien init data-connector-ts alien dev alien dev commands invoke --deployment default --command test-connection --params '{}' ``` The complete source includes the Vault setup, cache, input validation, and sample data. ## Put the connector beside the database [#put-the-connector-beside-the-database] ## What you built [#what-you-built] You built a narrow API around a private database. The customer keeps the connection in Vault, the Worker opens it locally, and your control plane receives operation results rather than database credentials or general network access. Operations that make several database calls keep those calls on the customer's network, which can also reduce latency and data transfer. Source: [`examples/data-connector-ts`](https://github.com/alienplatform/alien/tree/main/examples/data-connector-ts). Next: [Vault](/docs/infrastructure/vault), [Commands](/docs/commands). # Chat with private Postgres (/docs/examples/private-postgres-chat) In this example, we are going to build a streaming chat application over private data in Postgres. The Next.js application, database, and model access run together in the customer's cloud, so database rows do not need to pass through your hosted backend. When someone asks, “Which enterprise customers have the most MRR?”, the model calls a `queryDatabase` tool. It chooses one of seven questions and supplies a few bounded filters. The application—not the model—owns the SQL that runs. Postgres has no public endpoint. The application image contains neither a database password nor a model-provider key. Alien links both resources to the Container and resolves their bindings while the application is running. The model request also stays with the deployment's cloud account. On AWS the application calls Amazon Bedrock. On GCP it calls Vertex AI. On Azure it calls Azure AI Foundry. The Container's cloud identity authorizes the request, and the cloud provider applies that account's enabled models, quotas, logging, and billing. We will build the stack, list the models available in its cloud, give the model a safe database tool, and stream the final answer. ## What `alien.AI` means in this application [#what-alienai-means-in-this-application] `alien.AI("llm")` does not create one Alien-hosted model shared by every deployment. It connects the application to the model service available in the environment where that application is running: | Deployment | Model service used by the application | | ---------- | ------------------------------------- | | AWS | Amazon Bedrock | | GCP | Vertex AI | | Azure | Azure AI Foundry | This is important for a private-data application. The database query runs inside the deployment, and the resulting rows are sent to a model through that deployment's cloud AI service. Your hosted control plane does not need the Postgres password, a route to the database, or a provider API key. ```text Customer's AWS account browser ──HTTPS──▶ Next.js Container ─────▶ Amazon Bedrock │ workload identity │ └───────────────▶ private Postgres ``` The same application code uses Vertex AI on GCP and Azure AI Foundry on Azure. Since each cloud exposes a different model catalog, the UI discovers models at runtime instead of assuming every deployment has the same ones. Local development is deliberately different. Your laptop has no AWS, GCP, or Azure workload identity, so `alien dev` uses `OPENAI_API_KEY` or an OpenAI-compatible endpoint you configure. That local binding lets you develop the application; a deployed cloud binding uses the model service in the deployment's account. ## Describe the application [#describe-the-application] `alien.ts` declares three resources: * `app` is the Next.js Container users open in their browser. * `db` is the private Postgres database. * `llm` gives the application access to models available in the deployment. ```ts title="alien.ts" const llm = new alien.AI("llm").build() const db = new alien.Postgres("db").build() const app = new alien.Container("app") .code({ type: "source", src: ".", toolchain: { type: "docker", dockerfile: "Dockerfile" }, }) .cpu(0.5) .memory("512Mi") .port(3000) .publicEndpoint("web", 3000, "http") .environment({ PORT: "3000", HOSTNAME: "0.0.0.0" }) .link(llm) .link(db) .permissions("app") .build() ``` The Container is public because it serves the chat UI. Postgres is not. `.link(db)` gives only this Container the information it needs to connect to `db`. The stack grants the application model invocation and database access: ```ts title="alien.ts" export default new alien.Stack("ai-chatbot") .platforms(["aws", "gcp", "azure"]) .add(llm, "live") .add(db, "live") .add(app, "live") .permissions({ profiles: { app: { "*": ["ai/invoke", "postgres/data-access"], }, }, }) .build() ``` All three resources are `live`, so Alien can create and reconcile them during a rollout. Runtime access is separate: the `app` permission profile controls what the Container can do once it is running. ## List the models this deployment can use [#list-the-models-this-deployment-can-use] Model availability differs by provider, account, and region. The application asks its linked AI resource instead of hard-coding a model list. ```ts title="app/api/models/route.ts" import { ai } from "@alienplatform/sdk" export async function GET() { const models = await ai("llm").getAvailableModels() return Response.json({ models: models.map(model => model.id) }) } ``` The model picker can now show only models available to this deployment. ## Resolve the model connection at request time [#resolve-the-model-connection-at-request-time] The AI binding exists in the running workload, not while Next.js builds the image. Resolve it inside the request handler: ```ts title="app/api/chat/route.ts" const modelId = model || (await ai("llm").getAvailableModels())[0]?.id if (!modelId) { return Response.json({ error: "the AI binding exposes no models" }, { status: 503 }) } const connection = await getAiConnection("llm") ``` In a cloud deployment, Alien's AI gateway uses the workload's cloud identity when it calls the provider. Locally, `alien dev` can create the same binding from your provider key. The gateway preserves the provider's wire format. Claude models therefore use the Anthropic client; other models in this example use an OpenAI-compatible client: ```ts title="app/api/chat/route.ts" function modelFor(modelId: string, connection: AiConnection) { if (modelId.startsWith("claude")) { const anthropic = createAnthropic({ baseURL: connection.baseURL, apiKey: connection.apiKey ?? "", }) return anthropic(modelId) } return createOpenAICompatible({ name: "alien", ...connection, })(modelId) } ``` ## Give the model questions, not SQL [#give-the-model-questions-not-sql] The tool input is a closed schema. The model can select a question, an optional plan or order status, and a result limit of at most 50 rows. ```ts title="app/queries.ts" export const QUESTIONS = [ "customer_count_by_plan", "customer_count_by_country", "total_mrr_by_plan", "top_customers_by_mrr", "orders_by_status", "recent_orders", "revenue_by_customer", ] as const export const askSchema = z.object({ question: z.enum(QUESTIONS), plan: z.enum(["enterprise", "pro", "starter"]).optional(), status: z.enum(["paid", "pending", "refunded"]).optional(), limit: z.number().int().min(1).max(50).default(10), }) ``` `plan()` turns that structured request into application-owned SQL. Values from the model are passed as query parameters: ```ts title="app/queries.ts" case "top_customers_by_mrr": return { text: `select name, plan, country, mrr_usd from customers where ($1::text is null or plan = $1) order by mrr_usd desc limit $2`, values: [planFilter ?? null, limit], } ``` The model cannot send a table name, SQL expression, or arbitrary statement. Adding a new kind of question is an ordinary code change: add it to the schema and write the query it owns. ## Open a bounded Postgres connection [#open-a-bounded-postgres-connection] The database binding resolves the connection inside the Container: ```ts title="app/db.ts" const conn = await postgres("db").connection() return new Pool({ host: conn.host, port: conn.port, database: conn.database, user: conn.username, password: conn.password, ssl: conn.ssl, options: "-c default_transaction_read_only=on -c statement_timeout=10000", }) ``` The pool is read-only and gives every statement a ten-second timeout. The password is resolved at runtime; it is not checked into the project or baked into the image. The demo seeds its sample tables through a separate write connection. It uses a transaction and an advisory lock, so concurrent Containers cannot partially seed the database. A production application would normally use its existing migration and ingestion path instead. ## Implement the tool [#implement-the-tool] The tool rejects filters that do not apply to the chosen question, ensures the demo data exists, plans the query, and returns the rows to the model. ```ts title="app/api/chat/route.ts" const queryDatabase = tool({ description: "Answer a question about the company's Postgres data.", inputSchema: askSchema, execute: async ask => { const ignored = unsupportedFilters(ask) if (ignored.length > 0) { return { error: `${ask.question} does not take ${ignored.join(" or ")}` } } await ensureSeeded() const { text, values } = plan(ask) const { rows } = await query(text, values) return { question: ask.question, rows, rowCount: rows.length } }, }) ``` Returning the rows as a tool result gives the model the evidence it needs to write the answer. The example UI also lets the user inspect the underlying tables and compare the answer with the source data. ## Stream the answer [#stream-the-answer] Finally, pass the selected model, conversation, and tool to the Vercel AI SDK: ```ts title="app/api/chat/route.ts" const result = streamText({ model: modelFor(modelId, connection), system: "Answer questions about the company's data. Use queryDatabase when data is needed. " + "If the tool cannot answer the question, explain what the data can answer.", messages: await convertToModelMessages(messages), tools: { queryDatabase }, stopWhen: stepCountIs(6), }) return result.toUIMessageStreamResponse() ``` The stop condition leaves room for the model to call the tool and then produce the user-facing response without allowing an unbounded tool loop. ## Run it locally [#run-it-locally] Locally there is no workload cloud identity, so provide a model-provider key to `alien dev`: ```bash cd examples/ai-chatbot-ts OPENAI_API_KEY=sk-... alien dev ``` Open the printed URL and ask: > How many enterprise customers do we have, and what is their total MRR? The first question creates the demo tables. Watch the model call `queryDatabase`, inspect the returned rows in the UI, and then try a question outside the seven supported operations. The model should explain that the available data cannot answer it instead of inventing a query. ## Deploy it [#deploy-it] ```bash alien deploy production --platform aws ``` You can also deploy the same stack to GCP or Azure. Alien builds the Next.js image and creates the Container, Postgres database, AI resource, and public endpoint in that environment. The example leaves the chat endpoint open so it is immediately usable. Before using this design in a product, add authentication and per-user rate limits; anyone who can reach an unprotected endpoint can consume the deployment's model quota. For a customer deployment, publish the application and create a setup link: ## What you built [#what-you-built] You built a complete AI application around the `alien.AI` resource: * The application discovers models available in its deployment. * The model connection is resolved at runtime without a provider key in the image. * Postgres remains private and its password is resolved only inside the linked Container. * The model selects from application-owned, parameterized queries instead of writing SQL. * The response streams back with the rows that support it. Complete source: [`examples/ai-chatbot-ts`](https://github.com/alienplatform/alien/tree/main/examples/ai-chatbot-ts). Next: [AI](/docs/infrastructure/ai), [Postgres](/docs/infrastructure/postgres), and [Permissions](/docs/permissions). # Run agent tools remotely (/docs/examples/remote-tools) In this example, we are going to let a hosted agent read and write customer files without giving the agent a customer cloud credential. The agent loop stays in your product; a Worker runs two reviewed tools against Storage in the selected customer deployment. This pattern is useful when an agent needs access to data or services in a customer's environment. Your product invokes a named Command, the Worker performs that operation locally, and Alien returns the result. We will create the Worker and Storage in `alien.ts`, implement the two tools in `src/index.ts`, and expose them as Commands. The Worker accepts this fixed tool list rather than arbitrary shell commands. The Worker does not need a public endpoint. Your agent submits a named tool call through Alien; the operation reaches the selected deployment and the result comes back. No inbound port, VPN, VPC peering, or storage credential is required in the hosted agent. Only tools registered in code can run. ## Describe the remote piece in `alien.ts` [#describe-the-remote-piece-in-alients] ```ts title="alien.ts" const files = new alien.Storage("files").build() const worker = new alien.Worker("worker") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .commandsEnabled(true) .link(files) .permissions("execution") .build() ``` `.link(files)` gives this Worker a binding to `files`; it does not publish the bucket or inject an AWS, GCP, or Azure credential into your code. The permission profile limits the Worker to the storage actions declared by the stack. ## Write the tools [#write-the-tools] ```ts title="src/index.ts" const tools = { "read-file": { execute: async ({ path }: { path: string }) => { const object = await storage("files").get(path) return { content: new TextDecoder().decode(object.data) } }, }, "write-file": { execute: async ({ path, content }: { path: string; content: string }) => { await storage("files").put(path, new TextEncoder().encode(content)) return { written: true, path } }, }, } ``` The Worker exposes this fixed list, not a shell. Adding another tool is a code change you can review. ## Make the tools callable [#make-the-tools-callable] ```ts title="src/index.ts" command("execute-tool", toolSchema, async ({ tool, params }) => { const handler = tools[tool] if (!handler) throw new Error(`Unknown tool: ${tool}`) return handler.execute(params) }) command("list-tools", async () => Object.keys(tools)) ``` ## Run it locally [#run-it-locally] ```bash alien init remote-worker-ts cd remote-worker-ts alien dev alien dev commands invoke --deployment default --command list-tools ``` Start with `list-tools`, then invoke `execute-tool` with `read-file` or `write-file`. Each local deployment gets its own Storage resource. ## Put the tools beside customer data [#put-the-tools-beside-customer-data] ## What you built [#what-you-built] You kept the agent loop in your product and moved only two reviewed tools beside customer-owned Storage. This is the core remote-tools pattern: send a small operation to the data instead of giving the hosted agent direct storage credentials or copying the entire dataset back. Source: [`examples/remote-worker-ts`](https://github.com/alienplatform/alien/tree/main/examples/remote-worker-ts). Next: [Commands](/docs/commands), [Remote Bindings](/docs/remote-bindings). # Store AI trace history with SlateDB (/docs/examples/trace-history) In this example, we are going to store AI traces in a customer's cloud. The familiar answer is to deploy a database such as ClickHouse. That is a good answer when you need fast aggregations across billions of events. But software you put in a customer's environment should be as small and boring as possible. Every database server becomes another thing to size, upgrade, monitor, back up, and explain during a security review. SlateDB gives us a smaller option. It is an embedded key-value database that stores its durable files in object storage. Your application opens it as a library; there is no separate database server to deploy. One process writes, while any number of API replicas can read. Here, we use [SlateDB](https://slatedb.io) to save traces, fetch one by ID, and browse by agent, status, model, and time. The API scales from two to four replicas. The writer stays at one replica because SlateDB accepts writes through a single writer. S3, Google Cloud Storage, or Azure Blob Storage holds everything durable. ## Why object storage is a useful foundation [#why-object-storage-is-a-useful-foundation] Object storage is already one of the easiest resources for a customer to own. It is durable across zones, inexpensive, encrypted by default, and does not have database nodes that need patching. Separating it from the containers also means you can replace every running process without replacing the data. ## Step 1: declare the four pieces [#step-1-declare-the-four-pieces] Start with Storage and Queue. They are frozen resources, so customer setup owns them and an ordinary application release cannot replace them. ```ts title="alien.ts" const data = new alien.Storage("data") .lifecycleRules([{ prefix: "staging/v1/", days: 7 }]) .build() const ingestion = new alien.Queue("ingestion").build() ``` The API is ordinary stateless compute, so it can have several replicas. The writer is explicitly fixed at one. ```ts title="alien.ts" const api = new alien.Container("api") .code({ type: "source", src: ".", toolchain: { type: "rust", binaryName: "slatedb-trace-store" }, }) .autoScale({ min: 2, desired: 2, max: 4, targetHttpInFlightPerReplica: 100 }) .publicEndpoint("api", 8080, "http") .link(data) .link(ingestion) .build() const writer = new alien.Container("writer") .code({ type: "source", src: ".", toolchain: { type: "rust", binaryName: "slatedb-trace-store" }, }) .replicas(1) .link(data) .link(ingestion) .build() ``` Both containers are live. Alien can ship new code and replace them without taking ownership of the customer's stored traces. ## Step 2: accept a trace quickly [#step-2-accept-a-trace-quickly] A trace can be larger than the portable 64 KiB Queue limit. The API therefore writes the body to Storage first, then puts a small pointer on Queue: ```rust title="src/service.rs" let encoded = serde_json::to_vec(&trace)?; let content_hash = hex::encode(Sha256::digest(&encoded)); let staging_path = format!( "staging/v1/{}/{}.json", hex::encode(&trace.trace_id), content_hash, ); storage .put(&Path::from(staging_path.as_str()), Bytes::from(encoded).into()) .await?; let pointer = IngestionPointer { trace_id: trace.trace_id.clone(), content_hash: content_hash.clone(), staging_path, }; queue .send(MessagePayload::Json(serde_json::to_value(pointer)?)) .await?; ``` The body is durable before the pointer is published. Keeping the queue message small also avoids copying a large trace through the queue service. Here is a complete request: ```bash curl -i http://localhost:8080/v1/traces \ -H 'content-type: application/json' \ -d '{ "traceId": "run-01", "agent": "researcher", "status": "completed", "model": "claude-sonnet", "startedAt": "2026-08-26T18:00:00Z", "finishedAt": "2026-08-26T18:00:04Z", "payload": {"events": [{"type": "tool", "name": "search"}]} }' ``` `202 Accepted` has a precise meaning: the trace body and its queue pointer are durable. The background writer may not have made the trace queryable yet. ## Step 3: commit once, even after redelivery [#step-3-commit-once-even-after-redelivery] The writer receives the pointer, loads the staged body, and verifies its hash. Only then does it commit the trace to SlateDB. The primary key is simple: ```text t/{encodedTraceId} ``` The list endpoint allows any combination of `agent`, `status`, and `model`. There are only eight combinations, so every trace gets eight small index entries ordered by `startedAt`. ```text i/{filterCombination}/{filterValues}/{startedAt}/{traceId} ``` That is the central tradeoff: a little more storage and write work in exchange for fast, predictable reads. We are choosing the questions up front instead of building a general-purpose analytics database. Generating those eight entries is just a loop over the three filter bits: ```rust title="src/keys.rs" const AGENT_BIT: u8 = 1; const STATUS_BIT: u8 = 2; const MODEL_BIT: u8 = 4; pub fn index_keys(trace: &Trace) -> impl Iterator + '_ { (0..8).map(|mask| { let prefix = index_prefix( mask, (mask & AGENT_BIT != 0).then_some(trace.agent.as_str()), (mask & STATUS_BIT != 0).then_some(trace.status.as_str()), (mask & MODEL_BIT != 0).then_some(trace.model.as_str()), ); format!( "{}{time:016x}/{}", prefix, hex::encode(&trace.trace_id), time = trace.started_at.timestamp_millis() as u64, ) }) } ``` The primary value and every index entry go into one SlateDB transaction. `await_durable` waits until the commit is safe in object storage before we acknowledge the queue message. ```rust title="src/store.rs" let transaction = db .begin(IsolationLevel::SerializableSnapshot) .await?; if let Some(existing) = transaction.get(primary_key.as_bytes()).await? { let existing: StoredTrace = serde_json::from_slice(&existing)?; if existing.content_hash == content_hash { return Ok(CommitResult::AlreadyExists); } return Err(ApiError::conflict(&trace.trace_id).into()); } let encoded_trace = serde_json::to_vec(&stored_trace)?; transaction.put(primary_key.as_bytes(), &encoded_trace)?; for index_key in index_keys(&trace) { transaction.put(index_key.as_bytes(), primary_key.as_bytes())?; } transaction .commit_with_options(&WriteOptions { await_durable: true, ..WriteOptions::default() }) .await?; ``` After that returns, the writer acknowledges the queue message and deletes the staging object. Queues can deliver a message more than once. The trace ID and content hash make that safe: * same ID and same content: already committed, acknowledge it; * same ID and different content: record a conflict and acknowledge it; * temporary Storage or SlateDB failure: release the message for another attempt. ## Step 4: read from any API replica [#step-4-read-from-any-api-replica] API replicas open SlateDB in read-only mode. They poll for new database files once per second and cache frequently used blocks in memory. ```rust title="src/store.rs" let db = DbReader::builder("db/v1", object_store) .with_options(DbReaderOptions { manifest_poll_interval: Duration::from_secs(1), ..DbReaderOptions::default() }) .build() .await?; // Direct lookup let trace = db.get(primary_key(trace_id)).await?; // Ordered, paginated listing let prefix = query_prefix(&query); let mut entries = db.scan_prefix(prefix.as_bytes()).await?; ``` `query_prefix` selects the index matching the supplied filters. The timestamp and trace ID at the end of each key keep results ordered and provide a stable pagination cursor. ```bash # Fetch one trace curl http://localhost:8080/v1/traces/run-01 # Browse a known index curl 'http://localhost:8080/v1/traces?agent=researcher&status=completed&limit=25' ``` New writes are eventually visible, normally within the one-second poll interval. Already committed traces remain readable while the writer is restarting. ## When this is—and is not—a good fit [#when-this-isand-is-nota-good-fit] This design works well when traces are append-heavy and the ways you read them are known ahead of time: look up an ID, filter on a few fields, and paginate through history. It is not a good fit for arbitrary SQL, joins, large aggregations, full-text search, or many concurrent writers. If those become core product features, keep object storage as the durable archive and add ClickHouse, OpenSearch, or another query system on top. You do not need to migrate the original trace data out of the customer's storage. ## What this example guarantees [#what-this-example-guarantees] | Property | Behavior | | ------------------- | ------------------------------------------------------------- | | Durable acceptance | `202` means the staged body and queue pointer were written | | Atomic commit | The trace and all indexes appear together | | Idempotency | Identical redeliveries do not create duplicate traces | | Read freshness | Readers normally observe commits within about one second | | Compute replacement | API and writer containers can be replaced without moving data | Malformed messages, hash mismatches, and ID conflicts are recorded under `failures/v1/` without copying the rejected payload. Successfully committed staging objects are deleted; the seven-day lifecycle rule cleans up anything left behind. ## Run it locally [#run-it-locally] The example includes a deployed test, not only unit tests: ```bash cd examples/slatedb-trace-store alien dev ``` Then submit the sample trace, wait briefly, and fetch it with the commands above. To run the automated version of the same flow: ```bash cargo nextest run -p slatedb-trace-store pnpm test ``` Before exposing this in production, add authentication at the deployment boundary. ## What Alien provides [#what-alien-provides] At this point, you have built a small data plane for AI trace history: object storage holds the durable data, a queue absorbs writes, one SlateDB writer commits them, and replicated API containers serve reads. Alien helps you deploy, monitor, and update that data plane inside each customer's AWS, Google Cloud, or Azure account. The same `alien.ts` creates the object storage, queue, API, and writer; keeps durable resources under customer setup ownership; and gives each container only the permissions it declares. The application code uses the same Storage and Queue bindings on every cloud. You can run the complete topology locally with `alien dev`, then ship it to customers without maintaining separate bucket SDKs, queue integrations, IAM policies, and deployment templates for every provider. Complete source: [`examples/slatedb-trace-store`](https://github.com/alienplatform/alien/tree/main/examples/slatedb-trace-store). Next: [Storage](/docs/infrastructure/storage), [Queue](/docs/infrastructure/queue), and [Frozen and live resources](/docs/frozen-and-live). # Build an object-storage-backed vector database (/docs/examples/vector-database) In this example, we are going to build a vector database that runs in a customer's cloud and keeps its vectors in customer-owned object storage. A public router fronts separate Rust readers and writers. The interesting part is not the similarity function. It is where the data lives and how it survives releases: vectors remain in the customer's S3, Google Cloud Storage, or Azure Blob Storage account. The Containers keep no durable state on local disk. A writer creates immutable segments in object storage; readers load them and answer queries. Restarting or replacing compute does not remove the database. We will first describe the service topology in `alien.ts`. Then we will follow an upsert into object storage, follow a query back out, and test that the data survives the compute that wrote it. Containers run the long-lived Rust and Nginx processes. Storage is a separate customer-owned resource with its own lifecycle and cloud identity. `.link(data)` gives the reader and writer a typed Storage binding without making the bucket public or placing a provider credential in either image. Internal names such as `writer.svc` connect services without public endpoints. ## Map the application in `alien.ts` [#map-the-application-in-alients] The stack begins with one Storage resource. Alien maps it to S3, Google Cloud Storage, Azure Blob Storage, or the local filesystem for development. ```ts title="alien.ts" const data = new alien.Storage("data").build() ``` The writer and reader use the same Rust binary. An environment variable selects which HTTP routes each process serves. ```ts title="alien.ts" const writer = new alien.Container("writer") .code({ type: "source", src: ".", toolchain: { type: "rust", binaryName: "byocdb" }, }) .port(8081) .environment({ BYOCDB_MODE: "writer", PORT: "8081" }) .link(data) .permissions("default") .build() const reader = new alien.Container("reader") .code({ type: "source", src: ".", toolchain: { type: "rust", binaryName: "byocdb" }, }) .port(8082) .environment({ BYOCDB_MODE: "reader", PORT: "8082" }) .link(data) .permissions("default") .build() ``` Only the Nginx router receives public traffic. The reader, writer, and bucket remain private to the deployment. ```ts title="alien.ts" const router = new alien.Container("router") .code({ type: "source", src: "./router", toolchain: { type: "docker" } }) .port(8080) .publicEndpoint("web", 8080, "http") .healthCheck({ path: "/health", method: "GET", timeoutSeconds: 1, failureThreshold: 3 }) .permissions("default") .build() export default new alien.Stack("byoc-database") .add(data, "frozen") .add(writer, "live") .add(reader, "live") .add(router, "live") .permissions({ profiles: { default: { data: ["storage/data-read", "storage/data-write"] }, }, }) .build() ``` The router, readers, and writer are `live`, so Alien can create, update, replace, or remove them during a rollout. Storage is `frozen`, so it remains owned by customer setup; an ordinary rollout cannot replace or delete it. This lets you ship new service code without giving ongoing deployment management authority over the bucket. ## Start the Rust process in reader or writer mode [#start-the-rust-process-in-reader-or-writer-mode] Both Containers start in `src/main.rs`. The mode determines which routes are registered: ```rust title="src/main.rs" let mode = Mode::from_str( &std::env::var("BYOCDB_MODE").expect("BYOCDB_MODE is required"), )?; let bindings = Bindings::from_env().expect("Alien bindings are required"); let storage = bindings.storage("data").await .expect("the data Storage binding is required"); let app = match mode { Mode::Writer => Router::new() .route("/health", get(health)) .route("/api/v1/namespaces/{namespace}/upsert", post(upsert)) .with_state(WriterState { writer: Arc::new(Writer::new(storage)), }), Mode::Reader => Router::new() .route("/health", get(health)) .route("/api/v1/namespaces/{namespace}/query", post(query)) .with_state(ReaderState { reader: Arc::new(Reader::new(storage)), }), }; ``` The Nginx router sends `/upsert` to `writer.svc:8081` and `/query` to `reader.svc:8082`. Those `.svc` names are available only inside the deployment. ```nginx title="router/nginx.conf.template" location ~ ^/api/v1/namespaces/.*/upsert$ { set $writer_backend writer.svc:8081; proxy_pass http://$writer_backend; } location ~ ^/api/v1/namespaces/.*/query$ { set $reader_backend reader.svc:8082; proxy_pass http://$reader_backend; } ``` ## Store vectors as immutable segments [#store-vectors-as-immutable-segments] An upsert becomes a new JSON segment. The object layout for a namespace looks like this: ```text demo/ ├── metadata.json └── segments/ ├── 2fb1….json └── 81ac….json ``` `metadata.json` records the vector dimension and the segment IDs. Each segment contains the vectors written by one upsert. ```rust title="src/writer.rs" let segment_id = Uuid::new_v4().to_string(); let segment = Segment::new(segment_id.clone(), request.vectors.clone()); let segment_path = Path::from(format!( "{namespace}/segments/{segment_id}.json" )); self.storage .put(&segment_path, Bytes::from(serde_json::to_vec(&segment)?).into()) .await?; ``` The segment is immutable once written. The writer then appends its ID to `metadata.json`. ## Coordinate writers with ETags [#coordinate-writers-with-etags] Two writers may read the same metadata and try to append different segment IDs. The writer uses the object's ETag as an optimistic lock: ```rust title="src/writer.rs" let (mut metadata, etag) = self.read_metadata_with_etag(&metadata_path).await?; metadata.segments.push(segment_id.clone()); let mode = match etag { Some(version) => PutMode::Update(version), None => PutMode::Create, }; match self.storage.put_opts( &metadata_path, Bytes::from(serde_json::to_vec(&metadata)?).into(), PutOptions { mode, ..Default::default() }, ).await { Ok(_) => break, Err(object_store::Error::Precondition { .. }) => continue, Err(error) => return Err(Error::Storage(error.to_string())), } ``` If another writer changed the file first, the conditional write fails and this writer reads the new metadata before trying again. The example needs no separate lock service or coordination database. ## Read segments and rank the vectors [#read-segments-and-rank-the-vectors] A query starts from `metadata.json`, loads each referenced segment, and combines its vectors: ```rust title="src/reader.rs" let metadata = self.read_metadata(&metadata_path).await?; let mut vectors = Vec::new(); for segment_id in &metadata.segments { let segment = self.read_segment(namespace, segment_id).await?; vectors.extend(segment.vectors); } ``` The example computes cosine similarity directly and returns the highest-scoring vectors. A production database would normally cache or build an index, but the storage and deployment model would remain the same. ```rust title="src/reader.rs" let mut scored: Vec<_> = vectors .iter() .enumerate() .map(|(index, vector)| { (index, cosine_similarity(&request.vector, &vector.values)) }) .collect(); scored.sort_by(|a, b| { b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal) }); ``` ## Run an upsert and query [#run-an-upsert-and-query] Start the complete stack: ```bash cd examples/byoc-database alien dev ``` Write three vectors to the `demo` namespace: ```bash curl -X POST http://localhost:8080/api/v1/namespaces/demo/upsert \ -H 'content-type: application/json' \ -d '{ "vectors": [ {"id":"doc1","values":[0.1,0.2,0.3,0.4],"metadata":{"title":"Hello"}}, {"id":"doc2","values":[0.2,0.3,0.4,0.5],"metadata":{"title":"World"}}, {"id":"doc3","values":[0.9,0.8,0.7,0.6],"metadata":{"title":"Other"}} ] }' ``` Ask for the two nearest vectors: ```bash curl -X POST http://localhost:8080/api/v1/namespaces/demo/query \ -H 'content-type: application/json' \ -d '{"vector":[0.1,0.2,0.3,0.4],"topK":2}' ``` The exact match, `doc1`, should be first with a score near `1.0`. `doc2` should follow. ## Prove that compute is disposable [#prove-that-compute-is-disposable] The repository's integration test writes a vector, queries it, and queries it again independently of the process that handled the write: ```bash npm test ``` When running in a cloud deployment, replace or restart a reader and send the same query again. The result remains because the namespace metadata and segments live in Storage, not in the Container filesystem. ## Put the database in a customer's cloud [#put-the-database-in-a-customers-cloud] ## What you built [#what-you-built] You built one HTTPS vector API from three stateless services and one durable customer-owned resource. Writes become immutable objects, ETags coordinate concurrent metadata updates, and readers reconstruct the current namespace from Storage. That is the larger Alien pattern: keep releaseable compute separate from customer-owned state. Your control plane can ship new reader and writer versions, while the customer's vectors remain in their cloud account. Complete source: [`examples/byoc-database`](https://github.com/alienplatform/alien/tree/main/examples/byoc-database). Next: [Storage](/docs/infrastructure/storage), [Frozen and live resources](/docs/frozen-and-live), and [Releases](/docs/releases). # Receive webhooks (/docs/examples/webhook-api) In this example, we are going to give each customer an HTTPS endpoint that receives and stores webhooks in their own deployment. A Command lets your product read the events it needs without exposing a second admin API. This is useful when webhook payloads contain customer data that should be processed and retained in the customer's environment. GitHub, Stripe, or another sender still uses an ordinary public URL; your hosted backend does not need the KV credential or a route into the customer's network. The two entry points serve different callers. GitHub, Stripe, or another external service sends an ordinary HTTP request. Your backend invokes a Command for a specific deployment when it needs the recorded events. We will describe the Worker, public endpoint, and KV store in `alien.ts`. Then we will implement the webhook route and the Command in `src/index.ts`. The webhook route is public because an outside service must call it. Reading the stored events is different: your control plane uses a Command, so the Worker does not need a second admin endpoint, inbound firewall rule, VPN, or VPC peering. Public traffic and control-plane operations stay separate. ## Describe the endpoint and storage [#describe-the-endpoint-and-storage] ```ts title="alien.ts" const events = new alien.Kv("events").build() const api = new alien.Worker("api") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .commandsEnabled(true) .publicEndpoint("api") .link(events) .permissions("execution") .build() ``` `.link(events)` gives this Worker access to the deployment's KV resource. The `execution` permission profile becomes the cloud permissions for that access; the hosted control plane never receives the KV credential. KV is a small key-value store created for each deployment. This example uses keys such as `github:1724450000000`, which makes recent events easy to scan by source without adding a database service. ## Receive and store a webhook [#receive-and-store-a-webhook] ```ts title="src/index.ts" app.post("/webhooks/:source", async c => { const source = c.req.param("source") const body = await c.req.json() const key = `${source}:${Date.now()}` await kv("events").setJson(key, { source, body, receivedAt: new Date().toISOString(), }) return c.json({ received: true, key }) }) ``` The full example also defines a signing-secret input. Verify the sender's signature before storing real webhooks. ## Let your product read the events [#let-your-product-read-the-events] ```ts title="src/index.ts" command("get-events", querySchema, async ({ source, limit }) => { const prefix = source ? `${source}:` : "" return scanEvents(kv("events"), { prefix, limit }) }) ``` ## Run it locally [#run-it-locally] ```bash alien init webhook-api-ts alien dev curl -X POST http://localhost:/webhooks/github -H 'content-type: application/json' -d '{"action":"opened"}' ``` The port is printed by `alien dev`. Read the complete source for cursor-based KV scanning, input validation, and webhook configuration. ## Give each customer their own webhook endpoint [#give-each-customer-their-own-webhook-endpoint] ## What you built [#what-you-built] You built a customer-specific webhook receiver whose payloads are accepted and stored in the customer's cloud. Outside senders use HTTPS, while your product uses a narrow Command interface to inspect results without receiving storage credentials or exposing an administrative port. Source: [`examples/webhook-api-ts`](https://github.com/alienplatform/alien/tree/main/examples/webhook-api-ts). Next: [Networking and endpoints](/docs/networking), [Events and schedules](/docs/events). # Overview (/docs/infrastructure) Your customers are on AWS, GCP, and Azure. Without Alien, you'd need separate integrations for each one — different SDKs, different IAM models, different deployment scripts. With Alien, you declare what you need once. Alien provisions the **native cloud service** in each customer's environment at deploy time. ```typescript title="alien.ts" import * as alien from "@alienplatform/core" const data = new alien.Storage("data").build() const cache = new alien.Kv("cache").build() const tasks = new alien.Queue("tasks").build() const secrets = new alien.Vault("credentials").build() const api = new alien.Worker("api") .code({ type: "source", src: "./api", toolchain: { type: "typescript" } }) .link(data) .link(cache) .link(tasks) .link(secrets) .build() ``` ``` ┏━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━┓ ┃ Storage ┃ ┃ Queue ┃ ┗━━━━━┯━━━━━━┛ ┗━━━━━┯━━━━━━┛ │ │ ├── AWS ───▶ S3 ├── AWS ───▶ SQS ├── GCP ───▶ Cloud Storage ├── GCP ───▶ Pub/Sub └── Azure ─▶ Blob Storage └── Azure ─▶ Service Bus ``` No per-provider SDKs. No branching logic. One codebase. ## Using resources in your code [#using-resources-in-your-code] In your application code, import the SDK and pass the resource name. The name you use (`"data"`, `"cache"`) matches the name from your `alien.ts`: ```typescript import { storage, kv } from "@alienplatform/sdk" const data = storage("data") // S3 on AWS, Cloud Storage on GCP, Blob Storage on Azure const cache = kv("cache") // DynamoDB on AWS, Firestore on GCP, Table Storage on Azure await data.put("report.json", Buffer.from(JSON.stringify(report))) const value = await cache.getJson("user:123") ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let data = bindings.storage("data").await?; let cache = bindings.kv("cache").await?; data.put(&"report.json".into(), bytes).await?; let value = cache.get("user:123").await?; ``` Credentials are injected automatically — IAM roles on AWS, Workload Identity on GCP, Managed Identity on Azure. No config files, no connection strings. See [Resource APIs](/docs/resource-apis) for the full SDK guide. ## Drop to native SDKs anytime [#drop-to-native-sdks-anytime] The Alien SDK is a convenience layer, not a lock-in. Every linked resource is also available as an environment variable with the native identifiers: ```typescript const binding = JSON.parse(process.env.ALIEN_DATA_BINDING!) // { service: "s3", bucketName: "acme-data-a1b2c3", region: "us-east-1" } const s3 = new S3Client({}) await s3.send(new GetObjectCommand({ Bucket: binding.bucketName, Key: "report.json", })) ``` Use this when you need platform-specific features like DynamoDB streams, S3 Select, or Pub/Sub ordering keys. Both approaches work in the same app. See [Using Native Cloud SDKs](/docs/resource-apis#using-native-cloud-sdks). ## Works locally [#works-locally] Run `alien dev` and everything works on your machine — no cloud credentials needed. Alien provides local equivalents for every resource: storage on the filesystem, queues in memory, secrets in an embedded store. Same APIs as production. ```bash alien dev ``` See [Local Development](/docs/local-development). ## Resources [#resources] | Resource | What it does | AWS | GCP | Azure | | ----------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------- | ----------------- | -------------- | | [Container](/docs/infrastructure/container) | Services that run continuously, including stateful or GPU-backed services | EC2 + disks | GCE + disks | VMs + disks | | [Worker](/docs/infrastructure/worker) | Request-response code | Lambda | Cloud Run | Container Apps | | [Network](/docs/infrastructure/network) | VPC/VNet infrastructure | VPC | VPC | VNet | | [Storage](/docs/infrastructure/storage) | Files and objects | S3 | Cloud Storage | Blob Storage | | [KV](/docs/infrastructure/kv) | Key-value lookups | DynamoDB | Firestore | Table Storage | | [Queue](/docs/infrastructure/queue) | Async messaging | SQS | Pub/Sub | Service Bus | | [AI](/docs/infrastructure/ai) | Model inference | Bedrock | Vertex AI | AI Foundry | | [Vault](/docs/infrastructure/vault) | Secrets | SSM Parameter Store | Secret Manager | Key Vault | | [Artifact Registry](/docs/infrastructure/artifact-registry) | Container images | ECR | Artifact Registry | ACR | | [Daemon](/docs/infrastructure/daemon) | One resident process per machine or node | Alien Machines | Alien Machines | Alien Machines | On **Kubernetes / on-prem**, infrastructure resources are provided externally by the cluster operator. Alien deploys only the compute. ## What's next [#whats-next] # Key (/docs/infrastructure/key) A `Key` gives code in a deployment two operations: encrypt and decrypt. The provider owns the key material; the binding accepts values up to 128 bytes. ```text code in deployment → key("customer-key") → AWS KMS / Cloud KMS / Key Vault ``` ## Define the resource [#define-the-resource] ```typescript title="alien.ts" import * as alien from "@alienplatform/core" const key = new alien.Key("customer-key").build() export default new alien.Stack("app") .add(key, "frozen") .build() ``` ## Use it in the deployment [#use-it-in-the-deployment] ```typescript import { key } from "@alienplatform/bindings" const customerKey = key("customer-key") const plaintext = new TextEncoder().encode("small secret") const ciphertext = await customerKey.encrypt(plaintext, { context: { recordType: "credential" }, }) const decrypted = await customerKey.decrypt(ciphertext, { context: { recordType: "credential" }, }) ``` Use the same authenticated `context` at decrypt time. Changing it makes the decrypt operation fail. For a hosted backend, either publish the Key as a [Remote binding](/docs/remote-bindings) or use [Encryption Gateway](/docs/encryption-gateway). Use [Vault](/docs/infrastructure/vault) for named secrets that the application reads by name. # Observability (/docs/observability) Alien gives you two separate views of a deployment: ```text deployment state release, rollout, health, last check-in, resources logs application, system, AI Gateway, Encryption Gateway ``` Start with deployment state. Move to logs when you know which deployment or request path needs inspection. ## Check deployment state [#check-deployment-state] ```bash # All deployments in the linked project alien status # One deployment alien status acme/production # Stable output for an agent or script alien status acme/production --json ``` For a safe resource summary without resource configuration or secrets: ```bash alien deployments resources acme/production --json ``` For machine-based deployments: ```bash alien deployments machines acme/production --json ``` ## Search application logs [#search-application-logs] ```bash alien logs --deployment acme/production --since 1h alien logs --deployment acme/production --follow alien logs --deployment acme/production --level error --json ``` `alien logs` hides Alien system records by default. Add `--system` when you are debugging the platform components themselves. Application log bodies remain application log bodies. Alien can detect common structured severity fields without replacing the body with a nested `message` or `msg` field. See [Structured log severity](/docs/observability/log-parsing). ## Search gateway diagnostics [#search-gateway-diagnostics] Gateway diagnostics are routing and outcome metadata. They do not contain AI prompts, AI responses, encryption plaintext, or ciphertext. ```bash alien logs --source ai-gateway \ --status provider-error \ --since 24h alien logs --source encryption-gateway \ --operation decrypt \ --status failed ``` ## Send telemetry elsewhere [#send-telemetry-elsewhere] Self-hosted managers can forward telemetry through their configured OpenTelemetry pipeline. The exact logs, metrics, and traces available depend on the workload and exporter configuration; do not assume that a managed cloud service exposes a portable metric unless its resource page documents it. See [Self-hosting configuration](/docs/self-hosting/configuration) for exporter settings. # Structured log severity (/docs/observability/log-parsing) Alien can normalize severity from common structured application logs captured from container stdout and stderr. This makes level filters work for applications that emit JSON without requiring every framework to produce OpenTelemetry logs directly. Parsing is intentionally limited to well-known severity conventions. Alien does not run user-defined Grok patterns, regular expressions, remappers, or per-service pipelines. No configuration is required. Alien applies this normalization automatically when it captures application stdout and stderr. System logs and logs sent directly over OTLP are not parsed. ## What Alien parses [#what-alien-parses] The entire application payload, after surrounding whitespace and container-runtime framing are removed, must be a JSON object. Each captured line is considered independently. Non-JSON lines take a fast path and retain the existing stdout/stderr fallback. String levels are recognized in these locations: * `level`, `severity`, `severityText`, `severity_text` * `levelname`, `level_name`, `logLevel`, `log_level` * ECS `log.level`, as either a literal key or a nested field * Loguru `record.level.name` Values are trimmed and matched case-insensitively: | Result | Accepted values | | ------ | ----------------------------------------------------------------------------- | | TRACE | `trace` | | DEBUG | `debug`, `verbose`, `silly` | | INFO | `info`, `information`, `informational`, `notice`, `http`, `success` | | WARN | `warn`, `warning` | | ERROR | `error`, `err`, `exception` | | FATAL | `fatal`, `critical`, `crit`, `alert`, `emergency`, `emerg`, `panic`, `dpanic` | This covers JSON output from common Winston, structlog, Go `slog`, zap, zerolog, logrus, Rust `tracing-subscriber`, ECS-compatible, and Caddy configurations when they use one of these fields. ### OpenTelemetry severity numbers [#opentelemetry-severity-numbers] Numeric `severityNumber` and `severity_number` follow the OpenTelemetry ranges: | Values | Result | | ------ | ------ | | 1–4 | TRACE | | 5–8 | DEBUG | | 9–12 | INFO | | 13–16 | WARN | | 17–20 | ERROR | | 21–24 | FATAL | Values outside 1–24 are ignored. ### Python logging numbers [#python-logging-numbers] Numeric `levelno` and `level_number` recognize Python's common levels: 5 TRACE, 10 DEBUG, 20 INFO, 30 WARN, 40 ERROR, and 50 FATAL. Custom numeric levels are ignored. ### Pino and Bunyan [#pino-and-bunyan] Pino and Bunyan use a numeric `level`: 10 TRACE, 20 DEBUG, 30 INFO, 40 WARN, 50 ERROR, and 60 FATAL. Because a bare numeric `level` is ambiguous, Alien accepts it only when the record also has a recognizable logger signature: * Pino: string `msg`, numeric `pid`, and a numeric or string `time`. * Bunyan: string `msg`, `v: 0`, and string `name` and `hostname` fields. ```json title="Pino" {"level":30,"time":1573664685466,"pid":78742,"hostname":"api","msg":"ready"} ``` ```json title="Python JSON formatter" {"levelname":"ERROR","levelno":40,"message":"request failed"} ``` ```json title="Rust tracing JSON" {"timestamp":"2026-08-21T12:00:00Z","level":"WARN","fields":{"message":"retrying"},"target":"service"} ``` ## Conflicts and fallback [#conflicts-and-fallback] Alien inspects every recognized severity location in the object: * If all recognized candidates agree, that level is used. * If recognized candidates disagree, none wins by precedence. Alien uses the existing stdout/stderr fallback. * An unknown value does not override a separate recognized value. * If no supported value is present, existing capture behavior is unchanged. For example, `{"level":"warn","severity":"warning"}` is WARN, while `{"level":"error","severity":"info"}` falls back. Alien does not infer severity from `message`, `msg`, exception objects, HTTP status codes, or fields such as `status` and `statusCode`. A numeric `level` without a Pino or Bunyan signature is also ignored. ## Body, timestamps, and attributes [#body-timestamps-and-attributes] Severity parsing changes only severity metadata: * The application JSON remains the log body. Alien does not replace it with `message` or `msg`. * JSON properties are not promoted into indexed log attributes. * The container capture timestamp remains the event timestamp; embedded JSON timestamps are not substituted. * Multiline output and stack traces are handled one captured line at a time. * Parsing does not add exposure for fields inside the JSON body; apply the same sensitive-data rules you already use for application logs. Logs sent directly over OTLP bypass this parser because their telemetry producer is responsible for setting native OpenTelemetry severity fields. ## Unsupported text output [#unsupported-text-output] Common default text output from Node `console`, Next.js, Hono's logger, Morgan, Python's standard formatter, Uvicorn/FastAPI, Flask/Werkzeug, Rust `env_logger`, Nginx, and Apache is not structurally parsed. Configure a JSON formatter or emit native OpenTelemetry logs when reliable level filtering is required. # Operate existing environments (/docs/operate) Use [Remote operator](/docs/remote-operator) when your Helm chart or another Kubernetes deployment definition already owns the application. Alien adds an operator to that installation. It reports the resources and health its local permissions allow, and can expose the named operations you enable. Your existing deployment process remains responsible for the application. Start with the [Remote Operator quickstart](/docs/remote-operator/quickstart), then review [access](/docs/remote-operator/access) and [security](/docs/remote-operator/security). # Inventory (/docs/operate/inventory) Operate inventory is a read-only snapshot of resources that already exist in a customer environment. ## Kubernetes [#kubernetes] The Operator runs inside the customer's cluster and reports selected workloads: * Deployments, StatefulSets, DaemonSets, Jobs, and Pods * replica counts and rollout status * container images and versions * CPU and memory samples when metrics are available * last heartbeat and connectivity state The selector comes from setup: namespace, workload names, labels, or a combination. Alien only reports resources that match. ## AWS, GCP, and Azure [#aws-gcp-and-azure] For cloud-only environments, Alien uses read-only inventory APIs scoped to the customer's boundary: * **AWS:** a dedicated account * **GCP:** a project * **Azure:** a resource group Inventory includes provider-native identity, resource type, location, tags or labels, and health signals where the provider exposes them. ## Alien-Created Resources [#alien-created-resources] If Alien also built part of the environment, inventory can link the raw provider resource to the Alien resource that created it. The raw record is still inventory. Build state remains separate from Operate inventory. ## Freshness [#freshness] Inventory is replace-latest. Dashboards should treat stale inventory as a freshness problem first, not proof that the resource is unhealthy. # Operate Permissions (/docs/operate/permissions) Operate needs enough access to list resources and read health. It does not need create, update, delete, secret-read, object-read, queue-read, or database-read permissions. ## Kubernetes [#kubernetes] The Operator needs read permissions for the selected namespaces and workload types: * `get`, `list`, and `watch` on workloads and Pods * read access to metrics APIs when CPU and memory are shown For namespace-scoped installs, bind those permissions only in the selected namespace. Use cluster-scoped access only when one Operator must observe workloads across namespaces. When log collection is enabled, Alien uses a Fluent Bit DaemonSet that reads node log files and posts to the Operator's in-cluster service. The Operator does not need `pods/log` API access for that path. ## AWS [#aws] Use a dedicated account for Operate when possible. AWS resource discovery uses account-level tagging APIs, so discovery cannot be fully constrained by tag or prefix. Operate does not require object data access to S3, message access to SQS, secret value access, or database reads. ## GCP [#gcp] Scope access to the project that contains the customer's BYOC deployment. Operate uses inventory and monitoring reads. It does not need permissions to read object contents, secret values, Pub/Sub messages, or database rows. ## Azure [#azure] Scope access to the resource group that contains the deployment. Operate uses Resource Graph and metrics reads. It does not need data-plane permissions for Blob Storage, Key Vault secret values, Service Bus messages, or databases. ## Revocation [#revocation] The customer can uninstall the Operator or remove the read-only identity at any time. Alien should show the deployment as disconnected once heartbeats stop. # From Local to Cloud (/docs/quickstart/from-local-to-cloud) In the [quickstart](/docs/quickstart), you built an AI worker and tested it locally. Now let's deploy the same code into a real cloud account — AWS, GCP, or Azure. Deploying to a customer's cloud is a **two-step process** with two people involved: 1. **You (the developer)** sign in, push a release, and onboard a customer 2. **The customer's admin** runs a single command in their cloud account — this creates the infrastructure and deploys your worker After that, the admin is done. You push updates from your machine — the customer never needs to do anything again. For this guide, you'll play both roles using a cloud account you control. The examples below use AWS, but the same flow works on GCP (`--platform gcp`) and Azure (`--platform azure`) — only the underlying resources differ. ## Sign in [#sign-in] Install the CLI if you haven't already, then sign in. The browser opens for OAuth — pick the workspace you want to use. ```bash title="macOS / Linux" curl -fsSL https://alien.dev/install | sh export PATH="$HOME/.local/bin:$PATH" alien login ``` ```powershell title="Windows" irm https://alien.dev/install.ps1 | iex alien login ``` ## Link the project [#link-the-project] From inside the `my-worker` directory you created in the quickstart: ```bash alien link ``` This creates a project on alien.dev (if it doesn't exist) and links this directory to it. Releases and deployments now show up in the dashboard. ## Build and release [#build-and-release] ```bash alien release ``` Alien builds your worker, pushes the image to a managed registry, and creates a release. Takes \~30s. Nothing is deployed to a customer's cloud yet — the release is just sitting on the platform, ready to be pulled. ## Onboard a customer [#onboard-a-customer] You onboard each customer by minting a **deployment link**. In production, you'd send this to the customer's cloud admin. ```bash alien onboard acme-corp ``` ``` ✓ Ready to deploy. Customer acme-corp Share with the customer's admin: https://alien.dev/deploy/#dg_abc123... ``` The link opens a **white-labeled deployment portal** branded for your project. Alien auto-generates a project-specific CLI, Terraform module, CloudFormation template, and Helm chart from your stack — the portal lets the admin pick whichever fits their workflow. Prefer clicks? The same flow lives in the dashboard under **Deployments → New deployment → Generate deployment link**. Configure the portal's name, accent color, and logo in the **Portal Studio** under **Settings → Deployment Portal**. ## The customer deploys [#the-customer-deploys] In production, the customer's admin opens the link in a browser and gets a project-branded portal with four deployment methods: * **CLI** — a project-branded binary (e.g. `acme-deploy`) that runs the setup in one command * **Terraform** — a module they drop into their existing Terraform workspace * **CloudFormation** — a one-click stack launch for AWS * **Helm** — for installing into an existing Kubernetes cluster The portal renders project-branded instructions for each. For this guide, pick **CLI** — it's the most concise. Since you're playing both roles, run it yourself in a terminal with credentials for your target cloud (swap `aws` for `gcp` or `azure` to deploy elsewhere): ```bash # Copy the install command shown in the portal. curl -fsSL https://pkg.alien.dev///install.sh | bash export PATH="$HOME/.local/bin:$PATH" acme-deploy deploy \ --token \ --name acme-corp \ --platform aws ``` This provisions the compute, storage, and IAM resources native to that cloud — auto-generated from your stack definition with least-privilege roles. On AWS that means **Lambda**, **S3**, and **IAM roles**; on GCP it's **Cloud Run**, **GCS**, and **service accounts**; on Azure it's **Container Apps**, **Blob Storage**, and **managed identities**. Open the dashboard's **Deployments** tab to watch it come up. Once it shows `running`, the worker is live in that cloud account. You can also check from the CLI: ```bash alien deployments ls ``` ## Send a command [#send-a-command] Send a command to the worker running in the customer's cloud: ```bash alien commands invoke \ --deployment acme-corp \ --command execute-tool \ --params '{"tool": "write-file", "params": {"path": "hello.txt", "content": "Hello from the cloud!"}}' ``` ```json { "written": true, "path": "hello.txt" } ``` Same command you used in local dev — but this time it executed on the customer's cloud (Lambda on AWS, Cloud Run on GCP, Container Apps on Azure), writing to a real bucket in their account. ## Push an update [#push-an-update] This is where it clicks. Remember `alien dev release` from local dev? This is the production version. Change your worker code — add a new tool to `src/index.ts`: ```ts title="src/index.ts" "list-files": { description: "List all files in the customer's workspace", execute: async () => { const store = storage("files") const objects = await store.list() return { files: objects.map(obj => obj.location) } }, }, ``` Release it: ```bash alien release ``` The platform picks up the new release and rolls it out to every deployment subscribed to this project — no redeployment, no customer involvement, no downtime. Watch it in the dashboard, or: ```bash alien deployments ls ``` Once it's `running`, try the new tool: ```bash alien commands invoke \ --deployment acme-corp \ --command execute-tool \ --params '{"tool": "list-files", "params": {}}' ``` ```json { "files": ["hello.txt"] } ``` You changed code on your machine, ran one command, and the worker running in the customer's cloud account was updated. ## Clean up [#clean-up] How you tear down depends on how the customer provisioned. Pick the tab matching the deployment method used in step 3: ```bash acme-deploy destroy --name acme-corp ``` Delete the CloudFormation stack the portal created (named after the deployment, e.g. `acme-corp`) from the AWS console, or: ```bash aws cloudformation delete-stack --stack-name acme-corp ``` From the workspace where the module was applied: ```bash terraform destroy ``` ```bash helm uninstall acme-corp ``` This removes all cloud resources created for this deployment. *** ## What's next [#whats-next] You deployed code into a customer's cloud, sent commands to it, and pushed a live update — all without the customer doing anything after the initial setup. The same stack deploys across AWS, GCP, and Azure with no code changes. # Quickstart (/docs/quickstart) In this guide, you'll build an **AI worker** that runs inside a customer's cloud. Your AI does the reasoning in your cloud; the worker does the actions in theirs — reading files, writing results, querying data — without any of it leaving their network. ``` ╔═ Your Cloud ════════════╗ ╔═ Customer's Cloud ═════════════════╗ ║ ║ ║ ║░ ║ ┏━━━━━━━━━━━━━━━━┓ ║ tool calls ║ ┏━━━━━━━━━━━━━━━━┓ ║░ ║ ┃ AI Agent ┃────╬─────────────▶──╬──┃ AI Worker ┃ ║░ ║ ┃ (reasoning) ┃◀───╬────────────────╬──┃ (actions) ┃ ║░ ║ ┗━━━━━━━━━━━━━━━━┛ ║ results ║ ┗━━━━━━┯━━━━━━━━━┛ ║░ ║ ║ ║ │ ║░ ╚═════════════════════════╝ ║ read files, query data, ║░ ║ write results, ... ║░ ║ ║░ ╚════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` Copy and paste this prompt: ## Install [#install] ```bash title="macOS / Linux" curl -fsSL https://alien.dev/install | sh export PATH="$HOME/.local/bin:$PATH" ``` ```powershell title="Windows" irm https://alien.dev/install.ps1 | iex ``` ## Create the project [#create-the-project] ```bash alien init ``` Select **remote-worker-ts**. This creates: Let's look at the two important files. ### `alien.ts` — what to deploy [#alients--what-to-deploy] This file describes the infrastructure each customer gets: ```ts title="alien.ts" import * as alien from "@alienplatform/core" // Private file storage for each customer // Becomes S3 on AWS, Cloud Storage on GCP, Blob Storage on Azure const files = new alien.Storage("files").build() // Your code — deployed as a serverless worker in the customer's cloud // Becomes Lambda on AWS, Cloud Run on GCP, Container Apps on Azure const worker = new alien.Worker("worker") .code({ type: "source", src: "./", toolchain: { type: "typescript" } }) .commandsEnabled(true) .link(files) .permissions("execution") .build() export default new alien.Stack("remote-worker") .add(files, "frozen") .add(worker, "live") .permissions({ profiles: { execution: { "*": ["storage/data-read", "storage/data-write"], }, }, }) .build() ``` A Stack is the complete description of what gets deployed into each customer's cloud. It lists every resource (storage, cache, workers) and how they connect. You define it once — Alien handles translating it to the right cloud services for each customer. A Worker is your code, packaged and deployed as bounded request-response compute. On AWS it becomes a Lambda function, on GCP a Cloud Run service, on Azure a Container App. `.link(files)` gives it access to the customer's storage at runtime — credentials are injected automatically. This is how you talk to the worker remotely. Your backend sends a command ("read this file", "write this result"), the Alien server dispatches it to the worker running inside the customer's cloud, and the result comes back. Zero inbound networking. Zero open ports. The customer's network stays completely closed. It says who owns changes after setup. **Frozen** means customer setup owns the resource. A normal rollout cannot create, change, replace, or delete it. **Live** means Alien's Deployment Manager can create, update, replace, or remove the resource during a rollout. Workers are live so a new release can deploy their new code without asking the customer to run setup again. ### `src/index.ts` — the code that runs in the customer's cloud [#srcindexts--the-code-that-runs-in-the-customers-cloud] The template includes two tools. Here's the core pattern: ```ts title="src/index.ts" import { command, storage } from "@alienplatform/sdk" // Each tool runs inside the customer's cloud. // The command result is returned to the caller. Keep it to the data you intend to return. const tools: Record Promise }> = { "read-file": { description: "Read a file from the customer's private workspace", execute: async ({ path }: { path: string }) => { const store = storage("files") const object = await store.get(path) return { content: object.data.toString("utf8") } }, }, "write-file": { description: "Write a file to the customer's private workspace", execute: async ({ path, content }: { path: string; content: string }) => { const store = storage("files") await store.put(path, Buffer.from(content)) return { written: true, path } }, }, } command("execute-tool", async ({ tool, params }: { tool: string; params: any }) => { const handler = tools[tool] if (!handler) throw new Error(`Unknown tool: ${tool}`) return handler.execute(params) }) command("list-tools", async () => Object.entries(tools).map(([name, t]) => ({ name, description: t.description, })) ) ``` `command()` registers handlers, and `storage()` gives each command access to the customer's private storage. *** ## Local development [#local-development] ### Start local dev [#start-local-dev] ```bash alien dev ``` ``` Local Development Project remote-worker-ts ✔ Build local release ✔ Start local deployment ╭─ default ────────── ● running ───╮ │ worker running (private) │ │ files local filesystem │ ╰──────────────────────────────────╯ alien dev release → push changes alien dev deploy → new deployment Ctrl+C → stop ``` Everything runs on your machine. Storage is on the local filesystem. Same APIs as production — no cloud credentials needed. `default` is your first deployment — it simulates deploying into a customer's cloud. In production, this would be a real AWS account with a real S3 bucket. Right now, everything runs locally on your machine. ### Send a command [#send-a-command] Commands let your backend call workers on the worker without any inbound networking. No open ports, no VPN, no VPC peering — the customer's network stays completely closed. In local dev, you target the `default` deployment. In production, the exact same command reaches a real customer deployment — from the CLI or [from your code via the API](/docs/commands#invoke-from-code). Open a second terminal and list the tools the worker exposes: ```bash alien dev commands invoke --deployment default --command list-tools ``` ```json [ { "name": "read-file", "description": "Read a file from the customer's private workspace" }, { "name": "write-file", "description": "Write a file to the customer's private workspace" } ] ``` Write a file to the customer's storage: ```bash alien dev commands invoke \ --deployment default \ --command execute-tool \ --params '{"tool": "write-file", "params": {"path": "hello.txt", "content": "Hello!"}}' ``` ```json { "written": true, "path": "hello.txt" } ``` Read it back: ```bash alien dev commands invoke \ --deployment default \ --command execute-tool \ --params '{"tool": "read-file", "params": {"path": "hello.txt"}}' ``` ```json { "content": "Hello!" } ``` ### Simulate multiple customers [#simulate-multiple-customers] You have one customer. Let's add another. In production, each customer has their own AWS account with their own S3 bucket — completely separate from each other. Locally, Alien simulates this with isolated directories: ```bash alien dev deploy --name acme-corp --platforms local ``` Back in the first terminal, both customers appear: ``` ╭─ default ─────────────────────────── ● running ─╮ │ worker running (private) │ │ files local filesystem │ ╰─────────────────────────────────────────────────╯ ╭─ acme-corp ───────────────────────── ● running ─╮ │ worker running (private) │ │ files local filesystem │ ╰─────────────────────────────────────────────────╯ ``` The isolation is real even locally — files written by `default` are invisible to `acme-corp`, just like they would be in separate AWS accounts. ### Push an update [#push-an-update] Change your code — add a tool, fix a bug, anything. Then: ```bash alien dev release ``` This creates a new local release and updates the tracked deployments to point at it. If you want to verify the new code path locally right away, restart `alien dev` after the release so the worker process reloads the new build. Press `Ctrl+C` to stop. *** ## Next step [#next-step] You built a multi-tenant worker, tested it locally with zero cloud setup, simulated multiple customers with isolated data, and pushed a live update to all of them at once. Ready to deploy it into a real AWS account? # CLI Reference (/docs/reference/cli) ## alien upgrade [#alien-upgrade] Upgrade the Alien CLI to the latest stable release: ```bash alien upgrade ``` `alien update` is an alias. The command verifies standalone downloads before replacing the executable. npm and Homebrew installations are upgraded through their package manager so the CLI does not overwrite package-managed files. Use `alien upgrade --dry-run` to check what would happen without making changes, or `alien upgrade --force` to reinstall the current stable version. ## alien login [#alien-login] Authenticate in the browser and select a default workspace: ```bash alien login ``` New accounts do not receive an automatically named workspace. Create one explicitly in the dashboard or with `alien workspaces create`. ## alien workspaces [#alien-workspaces] Create, list, and select workspaces: ```bash alien workspaces create acme-prod alien workspaces ls alien workspaces set acme-prod alien workspaces current ``` The workspace name is permanent and appears in dashboard URLs and CLI commands. Use 4–100 lowercase letters, numbers, and hyphens. ## alien init [#alien-init] Add an Alien workspace to an existing repository: ```bash alien init ``` Choose a small architectural starting point. Inside an existing repository, Alien creates an `alien/` directory containing `alien.ts`, workload code, and its dependencies. Inside an empty directory, Alien initializes the current directory. Choose the template and destination explicitly for automation or a custom repository layout: ```bash alien init remote-worker-ts packages/private-runtime cd packages/private-runtime ``` The interactive catalog is intentionally small: private workers, data connectors, event pipelines, HTTPS services, and minimal TypeScript workers. Complete applications and provider-specific examples remain available in the [tutorials](/docs/examples), with links to their source on GitHub. ## alien dev [#alien-dev] Start the local development environment: ```bash alien dev ``` Provisions all resources locally (embedded SQLite for KV/Queue, filesystem for Storage) and starts your workers as native processes. Hot-reloads on code changes. **Example:** ```bash alien dev # ✓ worker (worker) → http://localhost:3001 # ✓ data (storage) → local filesystem # ✓ Server → http://localhost:9090 ``` ## alien serve [#alien-serve] Start the standalone manager for production deployments: ```bash alien serve ``` Starts an HTTP server backed by SQLite. Manages deployments, dispatches commands, collects telemetry, and hosts an embedded artifact registry. **Options:** | Flag | Description | | ----------------------- | --------------------------------------------------- | | `--init` | Generate a starter `alien-manager.toml` config file | | `--config `, `-c` | Path to config file (default: `alien-manager.toml`) | | `--port ` | Override the HTTP server port | | `--host ` | Override the HTTP server bind address | **Example:** ```bash # Generate config alien serve --init # Start with defaults alien serve # Start with custom config alien serve --config /etc/alien/manager.toml ``` On first run, generates an admin API key. See [Private manager](/docs/private-manager) for manager setup instructions. ## alien build [#alien-build] Build your stack into deployable output: ```bash alien build --platform alien build --platforms , ``` Compiles your TypeScript code or container image, packages the outputs where needed, and validates the stack. Builds are content-hashed — if your code hasn't changed, the build completes instantly by reusing the previous output. **Options:** | Flag | Description | | -------------------------------------- | ----------------------------------------------------------------------------------------------- | | `--platform `, `--platforms` | Target platform(s): `aws`, `gcp`, `azure` (comma-separated for multiple) *(required)* | | `--config `, `-c` | Path to `alien.ts`/`alien.js`/`alien.json` file or directory | | `--output-dir `, `-o` | Output directory for build files | | `--targets ` | Target OS/architecture combinations (comma-separated) | | `--cache-url ` | Cache URL for build caching (e.g., `s3://bucket/path`) | | `--json` | Emit structured JSON output | **Examples:** ```bash alien build --platform aws alien build --platforms aws,gcp alien build --platform aws --targets linux-arm64 ``` ## alien release [#alien-release] Create a new release: ```bash alien release ``` Builds your code, pushes images to the registry, and creates a release. Active deployments pick up new releases automatically. Alien rebuilds during release so the release reflects the code you publish. If nothing changed, content-hash deduplication can reuse the previous output. Pushed images are reused when the same image is already in the registry. **Options:** | Flag | Description | | ------------------------- | -------------------------------------------------------------------------------------------------- | | `--platforms ` | Comma-separated list of platforms to release (auto-discovers from manager config if not specified) | | `--project ` | Project name or ID (skips project linking) | | `--channel ` | Channel to advance (defaults to `production`) | | `--prebuilt` | Skip build and push — uses pre-pushed image URIs from `stack.json` | | `--no-git` | Skip git metadata collection | | `--json` | Emit structured JSON output | **Examples:** ```bash alien release alien release --platforms aws,gcp alien release --prebuilt alien release --channel staging ``` ## alien releases promote [#alien-releases-promote] Promote an existing immutable release to a channel without rebuilding it: ```bash alien releases promote --channel production ``` Promotion is concurrency-safe: the CLI reads the channel's current release and the API only moves the channel if it still points there, so a stale promotion cannot overwrite a newer one. Promoting an earlier release is the standard rollback — the same tested artifacts, no rebuild. Create and inspect channels with: ```bash alien releases create-channel staging alien releases channels alien releases delete-channel staging ``` ## alien deployments set-channel [#alien-deployments-set-channel] Change the channel an unpinned deployment follows: ```bash alien deployments set-channel staging ``` If the deployment is pinned, it stays on the pinned release and starts following the new channel only after it is unpinned. ## alien onboard [#alien-onboard] Onboard a new customer and generate a deployment token: ```bash alien onboard ``` Creates a deployment group and returns a token the customer uses to set up their environment. **Options:** | Flag | Description | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `--platform `, `--platforms ` | Limit the deployment link to selected platforms; defaults to all platforms in the active release | | `--input ` | Provide a non-secret developer stack input | | `--secret-input ` | Provide a secret developer stack input; redacted in output | | `--max-deployments ` | Maximum deployments allowed for this deployment group | | `--json` | Emit structured JSON output | **Example:** ```bash alien onboard acme-corp \ --platforms aws \ --secret-input controlPlaneApiKey=sk_live_... # Deployment token: ax_dg_abc123... # Send this to the customer's admin. ``` If required developer-provided stack inputs are missing, `alien onboard` validates them and prompts in interactive terminals. Platform-scoped inputs are required only when the selected platforms need them; use `--platforms aws` to avoid collecting local-only values for an AWS-only link. Deployer-provided inputs are collected later by the deployment portal or project-branded deploy CLI. ## alien deploy [#alien-deploy] Deploy a release to a cloud platform: ```bash alien deploy --name --platform ``` **Options:** | Flag | Description | | ------------------------------ | ----------------------------------------------------------------------- | | `--name ` | Deployment name for identification *(required)* | | `--platform ` | Target platform: `aws`, `gcp`, `azure` *(required)* | | `--token ` | Deployment API key for authentication | | `--channel ` | Release channel followed by a new deployment (defaults to `production`) | | `--no-heartbeat` | Disable heartbeat capability | | `--monitoring ` | Telemetry mode: `auto` (default) or `off` | | `--network ` | Network mode: `auto` (default), `use-default`, `create`, or `byo` | | `--network-cidr ` | CIDR block for `--network create` | | `--availability-zones ` | Number of zones for `--network create` | | `--vpc-id ` | Existing AWS VPC ID for `--network byo` | | `--public-subnet-ids ` | Comma-separated AWS public subnet IDs for `--network byo` | | `--private-subnet-ids ` | Comma-separated AWS private subnet IDs for `--network byo` | | `--security-group-ids ` | Comma-separated AWS security group IDs for `--network byo` | | `--network-name ` | Existing GCP VPC network name for `--network byo` | | `--subnet-name ` | Existing GCP subnet name for `--network byo` | | `--network-region ` | GCP subnet region for `--network byo` | | `--vnet-resource-id ` | Existing Azure VNet resource ID for `--network byo` | | `--public-subnet-name ` | Azure public subnet name for `--network byo` | | `--private-subnet-name ` | Azure private subnet name for `--network byo` | **Example:** ```bash alien deploy --name acme-production --platform aws alien deploy --name bear-test --platform aws --channel staging alien deploy --name acme-production --platform aws --network create --availability-zones 3 ``` Network flags are converted to deployment stack settings. See [Networking](/docs/networking) and [Network](/docs/infrastructure/network). ## alien deployments ls [#alien-deployments-ls] List all active deployments: ```bash alien deployments ls ``` Shows deployment status, platform, current release, and a separate desired release when a rollout has not converged. Use `--json` for the generated manager API records without interactive prompts. ## alien deployments get [#alien-deployments-get] Show one deployment, its current and desired releases, stack resources, and timestamped provider observations for container and daemon images: ```bash alien deployments get alien deployments get --json ``` An observed image is what the provider last reported, not the desired stack configuration. A mismatch is shown as `rollout pending`; a stale observation is labeled separately. ## alien deployments retry, redeploy, and pin [#alien-deployments-retry-redeploy-and-pin] ```bash alien deployments retry [--json] alien deployments redeploy [--json] alien deployments pin [release-id] [--json] ``` * `retry` resumes the failed operation toward the existing desired release. * `redeploy` starts a fresh rollout of the current release and is intended for a running deployment. * `pin` selects a release explicitly; omit `release-id` to unpin and return to the current release of the deployment's channel. All three commands preserve structured API error codes, remediation hints, retryability, and request IDs. `--json` emits only the API response and does not prompt. ## alien releases ls [#alien-releases-ls] List releases selected by `production`, with immutable commit SHA and source ref. Select another channel explicitly, or opt into release history across all channels: ```bash alien releases ls alien releases ls --channel staging alien releases ls --all-channels alien releases ls --json ``` ## alien releases get [#alien-releases-get] Show an immutable release and correlate it with deployments that are targeting or already running it: ```bash alien releases get alien releases get --json ``` ## alien vault [#alien-vault] Manage vault secrets for a deployment: ```bash alien vault ``` See [Environment Variables](/docs/infrastructure/worker/environment-variables) for details on managing secrets across deployments. ## Commands for agents and automation [#commands-for-agents-and-automation] These commands accept stable, machine-readable output. Use `--json` when another program will read the result; it also disables interactive prompts where supported. ### Inspect the current context [#inspect-the-current-context] ```bash alien whoami alien projects get --json alien projects capabilities status --json alien status --json ``` `alien status /` returns one deployment. Without a deployment, it lists the linked project. ### Wait for a deployment [#wait-for-a-deployment] ```bash alien deployments wait acme/production \ --for ready \ --timeout 10m \ --json ``` `--for` accepts `ready`, `terminal`, or `deleted`. Use this instead of writing a polling loop around `deployments get`. ### Inspect resources and machines [#inspect-resources-and-machines] ```bash alien deployments resources acme/production --json alien deployments machines acme/production --json ``` `resources` returns a safe summary without resource configuration or secrets. `machines` returns the connected machine inventory and network-health observations. ### Configure project capabilities [#configure-project-capabilities] ```bash alien projects capabilities enable ai \ --model byo/claude-opus-5 alien projects capabilities enable encryption alien projects capabilities status --json ``` For AI, repeat `--model`, `--required-model`, or `--provider` to configure more than one value. ### Create scoped API keys [#create-scoped-api-keys] ```bash alien api-keys create --for ai-gateway alien api-keys create --for encryption-gateway --json alien api-keys create --for remote-bindings alien api-keys list --json alien api-keys revoke --yes ``` The create command prints the secret once. `--for` selects a least-privileged project role; it accepts `ai-gateway`, `encryption-gateway`, `deployments`, `remote-bindings`, or `read-only`. ### Find customer environments [#find-customer-environments] ```bash alien deployment-groups list --json alien deployment-groups get org_123 --json alien deployment-groups list --search acme ``` `get` accepts a deployment-group ID, name, or external ID. `customers` and `customer` are aliases for `deployment-groups`. ### Print executable gateway requests [#print-executable-gateway-requests] ```bash alien examples ai-gateway \ --protocol openai-chat \ --model byo/claude-opus-5 alien examples ai-gateway --protocol anthropic-messages alien examples encryption-gateway --operation encrypt alien examples encryption-gateway --operation decrypt --json ``` Generated examples use the active Alien environment. Secrets remain environment-variable references. Add `--json` to receive the service, endpoint, command, and required environment variables as structured data. ### Search gateway diagnostics [#search-gateway-diagnostics] ```bash alien logs --source ai-gateway \ --status provider-error \ --provider anthropic \ --since 24h \ --json alien logs --source encryption-gateway \ --operation decrypt \ --status failed ``` AI diagnostics can be filtered by `--model` and `--provider`; Encryption diagnostics by `--operation`. Both accept `--deployment-group` and the standard log time filters. ### Inspect gateway usage [#inspect-gateway-usage] ```bash alien usage ai --range 24h alien usage encryption --range 30d --json ``` Ranges are `24h`, `7d`, and `30d`. The command reports that usage is unavailable when the project has no metrics source instead of inventing zeroes. ### Invoke an operation directly [#invoke-an-operation-directly] ```bash alien operations list alien operations invoke \ --deployment acme/production \ --operation kubernetes/get-pods \ --params '{"namespace":"default","maxResults":10}' ``` Operations use the `/` form. The command waits for the result, or reports that approval is pending when the selected operation requires it. # Reference (/docs/reference) Reference documents exact interfaces. For architecture, infrastructure resources, Commands, Remote Bindings, gateways, deployment behavior, and operational guidance, use the [Guide](/docs). ## Platform clients [#platform-clients] The TypeScript SDK and REST reference are generated from the same OpenAPI contract. Use the SDK for a typed server-side TypeScript client, or use the REST pages when implementing another client or inspecting the underlying request. # TypeScript API SDK (/docs/reference/typescript-sdk) `@alienplatform/platform-api` is generated from the same OpenAPI document as the REST API reference. ## Install [#install] ```bash pnpm add @alienplatform/platform-api ``` The package is ESM-only and supports Node.js 18 or newer. ## Create a client [#create-a-client] Create an Alien API key with the smallest scope and role required by the backend service. Keep it in a secret manager: ```typescript import { Alien } from "@alienplatform/platform-api" const alien = new Alien({ apiKey: process.env.ALIEN_API_KEY ?? "", }) ``` The SDK sends the key as an HTTP Bearer credential. Do not instantiate it in browser code. ## Call a resource group [#call-a-resource-group] Methods are grouped by the API resource. For example, list projects: ```typescript const response = await alien.projects.list() console.log(response) ``` Create a project: ```typescript await alien.projects.create({ name: "my-app", gitRepository: { type: "github", repo: "my-org/my-app", }, }) ``` Check the exact request type in the generated SDK or the [Projects API reference](/docs/reference/api/projects); the OpenAPI schema remains the source of truth. ## Standalone functions [#standalone-functions] Use generated standalone functions when bundle size or tree-shaking matters: ```typescript import { AlienCore } from "@alienplatform/platform-api/core.js" import { projectsList } from "@alienplatform/platform-api/funcs/projectsList.js" const alien = new AlienCore({ apiKey: process.env.ALIEN_API_KEY ?? "" }) const result = await projectsList(alien) if (!result.ok) throw result.error console.log(result.value) ``` ## Errors and retries [#errors-and-retries] Typed API errors include the API error code, message, retryability, request ID, and optional remediation hint. Retry only when the error or SDK retry policy marks the failure as retryable; validation, authorization, and conflict errors usually require a code or state change. The generated SDK source and per-operation examples are available in the [public repository](https://github.com/alienplatform/alien/tree/main/client-sdks/platform/typescript). # Access (/docs/remote-operator/access) Remote Operator does not create access from nothing. It uses what the generated installation and the customer environment give it. | Access | Where it comes from | What to review | | ---------------- | ------------------------------------------------ | ----------------------------------------------- | | Kubernetes | ServiceAccount and namespace-scoped RBAC | Resources, verbs, and namespace | | Cloud | Workload identity or configured cloud connection | IAM role and actions | | Private services | Network path and service connection | Destination, credential, and allowed operations | | Alien | Per-installation registration values | Storage, rotation, and reuse | | Logs | Generated collector configuration | Selected workloads and data leaving the cluster | The current operator initiates outbound connections. You do not need to expose an inbound Kubernetes service for Alien. Network access and identity are separate. A pod may be able to reach a database but still lack a valid credential, or have a credential but no network route. Review the rendered manifest for the exact project configuration you are installing. The docs cannot tell you the final permissions because they change with selected operations and connections. Render the chart before installation and inspect the generated objects: ```bash helm template my-product ./chart --namespace my-product > rendered.yaml # Find the operator's identity and every permission granted to it. grep -nE 'kind: (ServiceAccount|Role|RoleBinding)|resources:|verbs:' rendered.yaml ``` Then ask Kubernetes about a permission you expect to be read-only: ```bash kubectl auth can-i get pods \ --namespace my-product \ --as system:serviceaccount:my-product: ``` Repeat that check for write actions. A result of `yes` should correspond to an operation you intentionally enabled. # Overview (/docs/remote-operator) Remote Operator is one lightweight container that lets your team and agents inspect, diagnose, and operate customer deployments without direct access to the customer environment. It runs inside each deployment and connects outbound to Alien over HTTPS. Keep your existing Terraform, Helm, and release process; the operator uses only the operations and local permissions you enable. For Kubernetes, add the generated template to your existing Helm chart: The operator runs alongside your application. Your chart remains the source of truth for the application and its releases. ## What it adds [#what-it-adds] * deployment inventory and health; * the diagnostics and remediation operations you select; * connections to private or cloud services you configure; and * Kubernetes log collection when enabled. Each capability has its own access. Review the generated identity, permissions, secrets, and optional cloud access before installing it. ## Current installation path [#current-installation-path] The dashboard generates a namespace-scoped Helm template for Kubernetes. # Installation (/docs/remote-operator/install) Remote Operator setup produces two separate things. ## Project template [#project-template] `byoc-operator.yaml` belongs in your chart’s `templates/` directory. It contains the project-specific image and Kubernetes resources for the capabilities selected in the dashboard. The current dashboard renders it with namespace scope. Review at least: * the image and digest; * ServiceAccount and Role/RoleBinding rules; * Secrets and environment variables; * outbound destinations; and * log collector resources when enabled. Commit the template to the chart only after that review. ## Installation values [#installation-values] Every installed environment gets its own values: ```yaml remoteOperator: registrationToken: "..." encryptionKey: "..." collectorToken: "..." ``` These values identify and protect one installation. Keep them in the secret-delivery system you already use for Helm values. Do not commit them to the chart, reuse them across customers, or place them in `values.yaml` defaults. ## Release it like the rest of your chart [#release-it-like-the-rest-of-your-chart] Remote Operator does not replace your Helm release process. Test the chart, publish it, and roll it out through the same path as the rest of your application. Enable it one installation at a time. A connected installation appears under **Deployments** in Alien. # Operations (/docs/remote-operator/operations) Configure operations before generating the Helm template. The selected set changes what the generated operator image can do and what access it needs. Start with read-only diagnostics. Add a mutating operation only when there is a concrete support or maintenance task that needs it. Good operations answer one question or perform one repair: * read Kubernetes events for one workload; * inspect pod status; * check a private service; * restart a specific workload; or * run a product-specific repair with typed inputs. Avoid general shell, arbitrary SQL, or an unrestricted Kubernetes proxy. Those interfaces are difficult to review and turn a narrow operator into broad remote access. ## Access is additive [#access-is-additive] Selecting an operation is not enough by itself. It can only succeed when the local installation also has the required Kubernetes RBAC, cloud identity, network route, and service credentials. Test each operation in a non-production installation. Check both success and denial. Keep returned results small and do not include customer records or secrets unless the operation explicitly requires them. ## What is actually selected [#what-is-actually-selected] Operations are named as `plugin/operation`. The current operator includes focused plugins for Kubernetes and services such as PostgreSQL, Redis, S3, RDS, CloudWatch, GCS, and Pub/Sub. Each plugin exposes a fixed set of operations with typed parameters. For example, the S3 plugin exposes exactly these operations: ```text s3/head-bucket s3/list-objects s3/head-object ``` That is deliberately different from giving the operator a general AWS shell. Select the smallest operation that answers the support question, then grant its local identity only the corresponding IAM action. ```text question selected operation local access still required Can we reach the bucket? s3/head-bucket network + AWS identity What objects are there? s3/list-objects s3:ListBucket Does this object exist? s3/head-object s3:GetObject metadata access ``` ## Run an operation without an agent [#run-an-operation-without-an-agent] List the catalog, then invoke an operation enabled for the project: ```bash alien operations list alien operations invoke \ --deployment acme/production \ --operation kubernetes/get-pods \ --params '{"namespace":"default","maxResults":10}' ``` The CLI waits up to 60 seconds by default. Add `--timeout ` for a different limit. If policy requires approval, the command reports `pending-approval` instead of pretending that the operation ran. # Quickstart (/docs/remote-operator/quickstart) Start with Kind, a development cluster, or a non-production namespace. ## 1. Open Remote Operator setup [#1-open-remote-operator-setup] In your Alien project, open **Deployments → Remote Operator** and choose manual setup. Select the operations your team needs. Start with one read-only Kubernetes diagnostic. Add remediation, cloud access, private-service connections, or logs only when you intend to test them. ## 2. Add the generated template [#2-add-the-generated-template] Wait for Alien to prepare the project-specific operator image, then download `byoc-operator.yaml`. ```text your-chart/ ├── Chart.yaml ├── values.yaml └── templates/ ├── deployment.yaml ├── service.yaml └── byoc-operator.yaml ← add this ``` Review the rendered resources before installing them. ## 3. Create test values [#3-create-test-values] Enter a name for the test installation. The dashboard generates values shown once: ```yaml remoteOperator: registrationToken: "..." encryptionKey: "..." collectorToken: "..." ``` Save them as `byoc-operator-values.yaml` outside version control. ## 4. Install the chart [#4-install-the-chart] ```bash helm upgrade --install test-installation \ --namespace test-installation --create-namespace \ --values byoc-operator-values.yaml ``` ## 5. Verify the whole path [#5-verify-the-whole-path] The dashboard waits for a fresh operator heartbeat. When it connects, verify: * the operator pod is ready on the expected image; * the installation appears in Alien; * enabled connections report healthy; * one read-only Kubernetes diagnostic succeeds; and * the log collector is healthy if you enabled it. Delete the local values file when the test is done. Generate different values for every installation. # Security (/docs/remote-operator/security) Review Remote Operator as software running inside the customer’s cluster—not as a dashboard feature. ## Local permissions [#local-permissions] The operator can only perform Kubernetes actions allowed by its ServiceAccount and RBAC. Cloud and private-service actions require separate identity and network access. Keep the installation namespace-scoped unless a reviewed operation truly needs more. Compare every write permission to a specific operation. ```bash # Inspect the installed namespaced permissions. kubectl get role,rolebinding,serviceaccount \ --namespace \ -o yaml ``` ## Installation secrets [#installation-secrets] Registration, encryption, and collector values are unique to an installation and shown once by the dashboard. Store them with the same care as other production Helm secrets. Never reuse one customer’s values for another. ## Outbound data [#outbound-data] Depending on what you enable, the operator can send deployment identity, workload inventory, health, operation inputs and results, and Kubernetes logs to Alien. Logs and custom operation results are the easiest places to leak application data. Test with representative workloads and inspect what is returned before production rollout. ## Removing access [#removing-access] Uninstalling Remote Operator or removing its local identity and network access stops that installation from connecting. Test the removal procedure in the same way you test installation. ```text remove the Helm release │ ├── operator pod stops ├── its ServiceAccount and namespaced RBAC are removed └── its outbound connection closes also revoke any cloud identity or external service credential that your chart created outside the release ``` Do not claim that an action has approvals, an audit record, or a particular revocation guarantee unless you have verified that behavior in the exact Alien deployment and operator build you ship. # Troubleshooting (/docs/remote-operator/troubleshooting) Work from the cluster outward. ## The pod does not start [#the-pod-does-not-start] 1. Render the chart with the same values used for installation. 2. Check the operator Deployment, ServiceAccount, Secrets, and image. 3. Read pod events before changing permissions. ## The pod runs but Alien shows no connection [#the-pod-runs-but-alien-shows-no-connection] Check outbound DNS, HTTPS, and the installation’s registration value. The dashboard considers a connection current only when it receives a recent heartbeat. Do not rotate every value immediately: first distinguish a missing Secret from a rejected token or blocked network path. ## An operation fails [#an-operation-fails] Trace its dependencies in order: ```text operation selected in Alien ↓ present in the generated operator image ↓ allowed by Kubernetes or cloud identity ↓ target reachable over the network ↓ target accepts the credential and request ``` Fix the first failing step. Do not grant broad cluster access as a shortcut. ## Upgrades [#upgrades] Generate the new template, compare its image and permissions with the installed version, and deploy it to a test installation first. Repeat the connection and read-only diagnostic checks before rolling it out through your normal Helm release process. # Configuration (/docs/self-hosting/configuration) The manager is configured via `alien-manager.toml`. Generate a template: ```bash alien serve --init ``` Place the file in the working directory, or specify a path: ```bash alien serve --config /etc/alien/manager.toml ``` Configuration priority (lowest to highest): TOML defaults → TOML file values → environment variables → CLI flags. ## Server [#server] | Field | Type | Default | Description | | -------------------------- | ------- | ------------------------- | ------------------------------------------------------------------------------------------- | | `port` | integer | `8080` | HTTP server port | | `host` | string | `0.0.0.0` | Bind address | | `base-url` | string | `http://localhost:{port}` | Public URL for this manager. Set this when running behind a reverse proxy or load balancer. | | `releases-url` | string | `releases.alien.dev` | Base URL for binary downloads (Operator, deploy CLI) | | `deployment-interval-secs` | integer | `10` | How often the deployment loop runs (seconds) | | `heartbeat-interval-secs` | integer | `60` | Expected heartbeat interval from agents (seconds) | ```toml [server] port = 8080 host = "0.0.0.0" base-url = "https://manager.example.com" releases-url = "https://releases.alien.dev" deployment-interval-secs = 10 heartbeat-interval-secs = 60 ``` Environment variable overrides: `PORT`, `HOST`, `BASE_URL`. ## Database [#database] | Field | Type | Default | Description | | ---------------- | ------ | ------------------ | ------------------------------------------------------------------------------------------ | | `path` | string | `alien-manager.db` | SQLite database file path | | `state-dir` | string | `.alien-manager` | Directory for state files and local artifacts | | `encryption-key` | string | *(none)* | AEGIS-256 encryption key for sensitive data at rest. Generate with `openssl rand -hex 32`. | ```toml [database] path = "/var/lib/alien/alien-manager.db" state-dir = "/var/lib/alien/state" encryption-key = "your-64-char-hex-key" ``` ## Artifact Registry [#artifact-registry] By default, the manager starts an embedded local container image registry. This serves container images to pull-mode deployments over HTTPS — no configuration needed. For push-mode deployments on AWS, GCP, and Azure, configure a cloud registry so the platform can pull worker and container images directly. You can set a `default` registry for all platforms, or override per-platform: | Field | Type | Default | Description | | --------- | ------- | --------------------------- | -------------------------------------------------------------------------- | | `default` | binding | *(embedded local registry)* | Default artifact registry for all platforms | | `aws` | binding | *(none)* | AWS-specific registry (ECR). Used for AWS worker and container images. | | `gcp` | binding | *(none)* | GCP-specific registry (GAR). Used for GCP worker and container images. | | `azure` | binding | *(none)* | Azure-specific registry (ACR). Used for Azure worker and container images. | ### ECR (AWS) [#ecr-aws] ```toml [artifact-registry.aws] service = "ecr" repositoryPrefix = "alien-artifacts" pullRoleArn = "arn:aws:iam::123456789:role/ecr-pull" pushRoleArn = "arn:aws:iam::123456789:role/ecr-push" ``` | Field | Type | Description | | ------------------ | ------- | -------------------------------------- | | `repositoryPrefix` | string | Prefix for ECR repository names | | `pullRoleArn` | string? | IAM role ARN for pull permissions | | `pushRoleArn` | string? | IAM role ARN for push+pull permissions | ### GAR (GCP) [#gar-gcp] ```toml [artifact-registry.gcp] service = "gar" repositoryName = "projects/my-project/locations/us-central1/repositories/alien" pullServiceAccountEmail = "pull@project.iam.gserviceaccount.com" pushServiceAccountEmail = "push@project.iam.gserviceaccount.com" ``` | Field | Type | Description | | ------------------------- | ------- | ----------------------------------------------- | | `repositoryName` | string | Full Artifact Registry repository name | | `pullServiceAccountEmail` | string? | Service account email for pull permissions | | `pushServiceAccountEmail` | string? | Service account email for push+pull permissions | ### ACR (Azure) [#acr-azure] ```toml [artifact-registry.azure] service = "acr" registryName = "myregistry" resourceGroupName = "rg-alien" ``` | Field | Type | Description | | ------------------- | ------ | -------------------------------------------------- | | `registryName` | string | Azure Container Registry name (e.g., `myregistry`) | | `resourceGroupName` | string | Resource group where the registry is located | ### Local (explicit) [#local-explicit] Usually you don't need to set this — the embedded registry starts automatically. But if you want to control the URL or data directory: ```toml [artifact-registry.default] service = "local" registryUrl = "localhost:5000" dataDir = "/var/lib/alien/registry" ``` ## Commands [#commands] Backend storage for the [commands protocol](/docs/commands). The KV store holds command state; the storage backend holds large request/response payloads. Default: local filesystem in `{state-dir}/commands_kv` and `{state-dir}/commands_storage`. For push-mode deployments (Lambda, Cloud Run), use cloud-backed storage so runtimes can access presigned URLs. | Field | Type | Default | Description | | --------- | ------- | -------------------- | --------------------------------------- | | `kv` | binding | *(local filesystem)* | KV store for command state | | `storage` | binding | *(local filesystem)* | Blob storage for large command payloads | ### DynamoDB + S3 (AWS) [#dynamodb--s3-aws] ```toml [commands] kv = { service = "dynamodb", tableName = "alien-commands", region = "us-east-1" } storage = { service = "s3", bucketName = "alien-command-storage" } ``` ### Firestore + GCS (GCP) [#firestore--gcs-gcp] ```toml [commands] kv = { service = "firestore", projectId = "my-project", databaseId = "(default)", collectionName = "alien-commands" } storage = { service = "gcs", bucketName = "alien-command-storage" } ``` ### Table Storage + Blob (Azure) [#table-storage--blob-azure] ```toml [commands] kv = { service = "tablestorage", resourceGroupName = "rg-alien", accountName = "alienstate", tableName = "aliencommands" } storage = { service = "blob", accountName = "alienstate", containerName = "alien-commands" } ``` ### Redis (Kubernetes) [#redis-kubernetes] ```toml [commands] kv = { service = "redis", connectionUrl = "redis://redis:6379" } ``` ## Impersonation [#impersonation] Cross-account credential impersonation for push-mode deployments. Each platform entry provides a service account identity that the manager assumes when deploying to remote environments. | Field | Type | Default | Description | | ------- | ------- | -------- | --------------------------------------------------------------- | | `aws` | binding | *(none)* | AWS impersonation identity (IAM role for STS AssumeRole) | | `gcp` | binding | *(none)* | GCP impersonation identity (service account for token exchange) | | `azure` | binding | *(none)* | Azure impersonation identity (managed identity) | ### AWS [#aws] ```toml [impersonation.aws] service = "awsiam" roleName = "alien-management" roleArn = "arn:aws:iam::123456789:role/alien-management" ``` ### GCP [#gcp] ```toml [impersonation.gcp] service = "gcpserviceaccount" email = "alien-management@project.iam.gserviceaccount.com" uniqueId = "123456789012345678" ``` ### Azure [#azure] ```toml [impersonation.azure] service = "azuremanagedidentity" clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" resourceId = "/subscriptions/.../providers/Microsoft.ManagedIdentity/..." principalId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ``` ## Telemetry [#telemetry] | Field | Type | Default | Description | | --------------- | ------ | ------------ | --------------------------------------------------------------------- | | `otlp-endpoint` | string | *(disabled)* | OTLP HTTP endpoint for forwarding logs, traces, and metrics | | `headers` | map | *(empty)* | Custom HTTP headers sent with every OTLP request (for authentication) | ```toml [telemetry] otlp-endpoint = "https://otel-collector.example.com:4318" [telemetry.headers] DD-API-KEY = "your-datadog-key" Authorization = "Basic base64encoded" ``` Environment variable override: `OTLP_ENDPOINT`. The manager collects OpenTelemetry data from deployed workers and forwards it to the configured endpoint. Use any OTLP-compatible backend — Grafana, Datadog, Honeycomb, Jaeger, etc. Alien preserves captured application bodies and can normalize severity from common structured formats. See [Observability → Structured log severity](/docs/observability/log-parsing). ## Example: Minimal Production Config [#example-minimal-production-config] ```toml [server] port = 8080 base-url = "https://manager.example.com" [database] path = "/var/lib/alien/alien-manager.db" state-dir = "/var/lib/alien/state" encryption-key = "your-64-char-hex-key" [telemetry] otlp-endpoint = "https://otel-collector.example.com:4318" ``` ## Example: AWS Push-Mode [#example-aws-push-mode] ```toml [server] base-url = "https://manager.example.com" [database] path = "/var/lib/alien/alien-manager.db" state-dir = "/var/lib/alien/state" encryption-key = "your-64-char-hex-key" [artifact-registry.aws] service = "ecr" repositoryPrefix = "alien-artifacts" pushRoleArn = "arn:aws:iam::123456789:role/ecr-push" [commands] kv = { service = "dynamodb", tableName = "alien-commands", region = "us-east-1" } storage = { service = "s3", bucketName = "alien-command-storage" } [impersonation.aws] service = "awsiam" roleName = "alien-management" roleArn = "arn:aws:iam::123456789:role/alien-management" [telemetry] otlp-endpoint = "https://otel-collector.example.com:4318" ``` ## Example: Local Development [#example-local-development] For local testing, the defaults are usually sufficient. Just run: ```bash alien serve ``` This starts the manager on port 8080 with an embedded registry and a local SQLite database. # Self-Hosting (/docs/self-hosting) The manager is your control plane — it stores releases, coordinates deployments, dispatches commands, and collects telemetry from every customer environment. ## Run the Manager [#run-the-manager] The manager is available as a Docker image: ```bash docker run -d \ -p 8080:8080 \ -v alien-data:/data \ -e BASE_URL=https://manager.example.com \ ghcr.io/alienplatform/alien-manager ``` Deploy it wherever you run containers — ECS, Cloud Run, Kubernetes, a VM, anything. The only requirement is **persistent disk** for the SQLite database. On ECS, use EFS. On Kubernetes, use a PVC. On first run, the manager generates an admin API key and prints it to stdout. Save it — you'll need it to configure the CLI. You can also run the manager binary directly: ```bash alien serve ``` This is useful for local development. It starts the manager on port 8080 with an embedded registry and a local SQLite database. ## Configure the CLI [#configure-the-cli] Point the CLI at your manager: ```bash export ALIEN_MANAGER_URL=https://manager.example.com export ALIEN_API_KEY=ax_admin_... ``` ## What the Manager Does [#what-the-manager-does] * **Stores releases** — immutable snapshots of your built code, pushed via `alien release` * **Manages deployments** — runs a deployment loop that pushes updates to customer environments * **Hosts an artifact registry** — embedded container image registry, or connects to ECR, GAR, or ACR * **Dispatches commands** — routes remote command invocations to the right deployment * **Collects telemetry** — receives OpenTelemetry logs, metrics, and traces from deployed workers and forwards to your observability backend * **Manages tokens** — API keys for authentication between the CLI, deployments, and the manager ## Configuration [#configuration] The manager is configured via `alien-manager.toml`. Generate a template: ```bash alien serve --init ``` Or mount a config file into the Docker container: ```bash docker run -d \ -p 8080:8080 \ -v alien-data:/data \ -v ./alien-manager.toml:/app/alien-manager.toml \ -e BASE_URL=https://manager.example.com \ ghcr.io/alienplatform/alien-manager ``` See the full [Configuration Reference](/docs/self-hosting/configuration) for all options. ## Cloud Artifact Registries [#cloud-artifact-registries] By default, the manager runs an embedded container image registry. This works for pull-mode deployments that fetch images over HTTPS. For push-mode deployments on AWS, GCP, and Azure, configure a cloud registry so the target platform can pull worker and container images directly. ```toml title="alien-manager.toml" [artifact-registry.aws] service = "ecr" repositoryPrefix = "alien-artifacts" pushRoleArn = "arn:aws:iam::123456789:role/ecr-push" ``` Configure per-platform: `[artifact-registry.aws]`, `[artifact-registry.gcp]`, `[artifact-registry.azure]`. See [Configuration Reference](/docs/self-hosting/configuration#artifact-registry) for details. ## Cross-Account Impersonation [#cross-account-impersonation] For push-mode deployments, the manager needs to call cloud APIs in the customer's environment. Configure a service identity that the manager can assume: ```toml title="alien-manager.toml" [impersonation.aws] service = "awsiam" roleName = "alien-management" roleArn = "arn:aws:iam::123456789:role/alien-management" ``` See [Impersonation](/docs/impersonation) for how this works on each cloud, and [Configuration Reference](/docs/self-hosting/configuration#impersonation) for the config format. ## Commands Backend [#commands-backend] The [commands protocol](/docs/commands) needs a KV store and blob storage for command state and large payloads. By default, this uses the local filesystem. For production, use a cloud backend: ```toml title="alien-manager.toml" [commands] kv = { service = "dynamodb", tableName = "alien-commands", region = "us-east-1" } storage = { service = "s3", bucketName = "alien-command-storage" } ``` See [Configuration Reference](/docs/self-hosting/configuration#commands) for all backend options. ## Telemetry [#telemetry] Forward OpenTelemetry data from deployed workers to your observability backend: ```toml title="alien-manager.toml" [telemetry] otlp-endpoint = "https://otel-collector.example.com:4318" [telemetry.headers] DD-API-KEY = "your-datadog-key" ``` Works with any OTLP-compatible backend — Grafana, Datadog, Honeycomb, Jaeger, etc. ## Provisioning Cloud Resources [#provisioning-cloud-resources] For push-mode deployments, the manager needs cloud resources — an artifact registry, a commands backend, and an impersonation identity. We provide Terraform modules that create these resources for each cloud provider: ```hcl module "alien_infra" { source = "github.com/alienplatform/alien//infra/aws" name = "my-project" principal_arn = aws_iam_role.manager.arn enable_artifact_registry = true enable_commands_store = true enable_impersonation = true } ``` The modules output structured `config_values` that map directly to `alien-manager.toml` sections. Available for [AWS](https://github.com/alienplatform/alien/tree/main/infra/aws), [GCP](https://github.com/alienplatform/alien/tree/main/infra/gcp), and [Azure](https://github.com/alienplatform/alien/tree/main/infra/azure). These modules provision only the supporting resources — they do not deploy the manager itself. Run the manager wherever you like and point it at these resources via `alien-manager.toml`. ## Production Checklist [#production-checklist] * [ ] Persistent disk for the SQLite database (`/data` or configured path) * [ ] Set `base-url` to your public URL (required for Operator sync and command-enabled pull receivers) * [ ] Configure a cloud artifact registry for each platform you deploy to * [ ] Configure impersonation for push-mode deployments * [ ] Set up a cloud commands backend (DynamoDB + S3, Firestore + GCS, etc.) * [ ] Configure telemetry to forward logs and traces * [ ] Run behind a reverse proxy with TLS (the manager serves HTTP) * [ ] Back up the SQLite database regularly # API Reference (/docs/infrastructure/ai/api) Get a handle with `ai(name)` from `@alienplatform/sdk`. The AI surface is TypeScript only — there is no Rust binding. Workloads in other languages call the HTTP endpoints directly. ## The binding environment variable [#the-binding-environment-variable] A linked AI resource injects `ALIEN__BINDING` (uppercased, hyphens to underscores — a resource named `llm` becomes `ALIEN_LLM_BINDING`) containing a JSON object tagged by `service`: | `service` | Platform | Fields | | ------------- | ------------------------------------- | --------------------- | | `bedrock` | AWS | `region` | | `vertex` | GCP | `project`, `location` | | `foundry` | Azure | `endpoint`, `account` | | `external-ai` | Local, Kubernetes, bring-your-own-key | `provider`, `apiKey` | The three cloud variants carry no key: the workload's own identity authorizes each call. `external-ai` carries a provider API key, and the SDK calls that provider directly. *** ## chat.completions.create [#chatcompletionscreate] Sends an OpenAI Chat Completions request. Use this for every model except Claude and the GPT-5 family. ```typescript const completion = await ai("llm").chat.completions.create({ model: "gpt-oss-120b", messages: [{ role: "user", content: "Summarize this order." }], }) const stream = await ai("llm").chat.completions.create({ model: "gpt-oss-120b", messages: [{ role: "user", content: "Summarize this order." }], stream: true, }) for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? "") } ``` | Parameter | Type | Required | Description | | ---------- | ---------------------------------------------------- | -------- | ----------------------------------------------------------------------- | | `model` | `string` | Yes | A model id from `getAvailableModels()`. | | `messages` | `Array<{ role: string, content: string \| object }>` | Yes | The conversation. `content` takes a string or a provider content block. | | `stream` | `boolean` | No | `true` returns an async iterable of chunks. | Any other field is passed to the provider untouched. **Returns:** `ChatCompletion`, or `AsyncIterable` when `stream` is `true`. *** ## responses.create [#responsescreate] Sends an OpenAI Responses request. AWS only. The GPT-5 family serves this API and no other; `gpt-oss-20b` and `gpt-oss-120b` serve it as well as chat completions. ```typescript const response = await ai("llm").responses.create({ model: "gpt-5.5", input: "Summarize this order.", }) ``` | Parameter | Type | Required | Description | | --------- | ------------------------------------ | -------- | ----------------------------------------------------- | | `model` | `string` | Yes | A GPT-5 family id, or `gpt-oss-20b` / `gpt-oss-120b`. | | `input` | `string \| Array<{ role, content }>` | Yes | The prompt. | | `stream` | `boolean` | No | `true` returns an async iterable of events. | **Returns:** `Response`, or `AsyncIterable` when `stream` is `true`. **Errors:** `AI_RESPONSES_API_UNSUPPORTED` on a bring-your-own-key Anthropic binding, which serves no Responses endpoint. *** ## getAvailableModels [#getavailablemodels] Lists the models this deployment can invoke right now. ```typescript const models = await ai("llm").getAvailableModels() // [{ id: "gpt-oss-20b", provider: "openai", displayName: "GPT-OSS 20B" }, …] ``` **Returns:** `AiModel[]`, checked against the customer's cloud on the first call and reused after that, so a model their account has not enabled is normally absent — see [Behavior](/docs/infrastructure/ai/behavior#model-availability) for when one can still appear. On a bring-your-own-key binding it is a fixed list for that provider. *** ## getAiConnection [#getaiconnection] Resolves a binding to an endpoint you can hand to another client library. ```typescript import { createOpenAICompatible } from "@ai-sdk/openai-compatible" import { getAiConnection } from "@alienplatform/sdk" const connection = await getAiConnection("llm") const model = createOpenAICompatible({ name: "alien", ...connection, apiKey: connection.apiKey ?? "", })("gpt-oss-120b") ``` | Parameter | Type | Required | Description | | --------- | -------- | -------- | ------------------- | | `name` | `string` | Yes | The AI resource id. | **Returns:** `AiConnection`, ready to use — the endpoint is live by the time it resolves. Pass `apiKey: ""` on a cloud binding rather than leaving it unset. Most clients fall back to a provider key in the environment when the field is missing, and would send your personal `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` instead of using the workload's identity. *** ## Calling the endpoints directly [#calling-the-endpoints-directly] The endpoint runs beside your workload. `getAiConnection()` returns its address, and the `baseURL` already includes the resource segment and `/v1`. ```typescript const { baseURL } = await getAiConnection("llm") await fetch(`${baseURL}/chat/completions`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: "gpt-oss-120b", messages: [{ role: "user", content: "hi" }], }), }) ``` | Path | Method | For | | ------------------- | ------ | -------------------------------------------------------- | | `/chat/completions` | POST | OpenAI-protocol models | | `/messages` | POST | Claude models | | `/responses` | POST | The GPT-5 family and the two `gpt-oss` models (AWS only) | | `/models` | GET | The available-model list | On a cloud binding no credentials go on these calls: the endpoint is reachable only from inside your workload, and every request to the provider is signed with the workload's own cloud identity. A bring-your-own-key binding is different — `baseURL` is the provider's own URL and you must send `apiKey` as a bearer token yourself. Never hardcode the port — it is not fixed. A workload in another language reads `ALIEN_AI_GATEWAY_URL` and appends `/{name}/v1`, where `{name}` is the resource id lowercased with underscores replaced by hyphens. *** ## Types [#types] ```typescript interface AiConnection { baseURL: string // ends in /v1, ready for an OpenAI- or Anthropic-compatible client apiKey?: string // bring-your-own-key bindings only; `undefined` on the clouds } interface AiModel { id: string // pass as `model` provider: string // "openai" | "anthropic" | "google" | "mistral" | … displayName: string // human label, for a model picker } ``` `ChatCompletion`, `ChatCompletionChunk`, `Response`, and `ResponseStreamEvent` come from the `openai` package, an optional peer dependency. Install it for the types; it is not needed at runtime. *** ## Errors [#errors] | Code | Meaning | Retryable | | ------------------------------ | ------------------------------------------------------------------------------------ | --------------------- | | `AI_UPSTREAM_ERROR` | The provider returned a non-2xx status, which the error carries. | On 429, 502, 503, 504 | | `AI_TRANSPORT_ERROR` | The request never completed, or the response body was unreadable. | Yes | | `AI_RESPONSES_API_UNSUPPORTED` | `responses.create` against a provider that serves no Responses endpoint. | No | | `AI_UNSUPPORTED_PROVIDER` | A bring-your-own-key binding names a provider with no known endpoint. | No | | `BINDING_NOT_FOUND` | `ALIEN__BINDING` is unset — the resource is not linked to this workload. | No | | `INVALID_BINDING_CONFIG` | The binding JSON is malformed, has no `service` tag, or carries an unexpected field. | No | Match on `error.code`. `AiUpstreamError` and `AiTransportError` are importable classes; the rest are codes on an `AlienError` rather than exported types. A failed model call surfaces as `AI_UPSTREAM_ERROR` carrying the status the provider returned, so retry on the retryable statuses above and treat the rest as permanent. # Behavior & Limits (/docs/infrastructure/ai/behavior) ## Guarantees [#guarantees] **No Keys in Your Application.** On AWS, GCP, and Azure your workload's injected credentials — IAM role, Workload Identity, Managed Identity — authorize every call. Nothing in the binding is a credential, so there is no key to leak, rotate, or scope, and inference bills to the customer's cloud account. **Private Endpoint.** The AI endpoint is reachable only from inside the workload it is linked to. It is never exposed on the network, so there is no endpoint to secure and none to misconfigure. **Checked Model List.** `getAvailableModels()` reports what this account and region can invoke: Alien's curated list for that cloud, narrowed by a live check. A model the customer has not enabled normally does not appear; the exception is one the cloud gave no clear answer for, which is kept rather than dropped. **Forwarded, Not Translated.** Alien speaks each model's own protocol rather than converting between them, so a request reaches the provider in the shape you wrote it and the reply comes back as the provider wrote it. Streaming works everywhere, and a provider error arrives as that provider's own error rather than something Alien invented. Claude on AWS is the one exception, and it is described under Protocols. ## Limits [#limits] | Limit | Value | Notes | | -------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Max request body | 32 MiB | Returns 413. Providers cap requests well below this, so their limit is the one you meet. | | Model-list check | up to 10 s per model, run concurrently | Later calls are instant once every model got a definite answer; otherwise the next call checks again. Never affects inference. | | Model id | Must be in Alien's list for that cloud | An id outside it returns 404 instead of reaching the provider. | | Azure throughput per model | 1 GlobalStandard unit | A deliberately conservative default. Raise it per model in the Azure portal; a re-provision resets it. | | `id` | letters, digits, hyphens and underscores (`[A-Za-z0-9-_]`), up to 64 characters | Immutable after create. | **Beyond the body cap above, Alien adds no limits of its own to inference.** There is no rate limit, no request timeout, no concurrency cap, and no token ceiling — the cloud provider's quotas and deadlines apply directly, and a 429 from the provider reaches you unchanged. A public workload needs its own limits in front of it. **Changing a live resource.** The `id` is immutable. Nothing else is configurable, so there is no resize, no version pin, and no update that can disrupt a running workload. Model choice lives in your code, not the stack. ## Protocols [#protocols] Which wire format a model speaks is fixed by the model, and the client has to match it. Alien picks the upstream from the `model` in the body, not from the path, so the body format is what has to be right: | Models | Protocol | Endpoint | Client | | ---------------------- | ----------------------- | ---------------------- | --------------------------------------- | | Claude, on every cloud | Anthropic Messages | `/v1/messages` | Anthropic SDK, or any Messages client | | Everything else | OpenAI Chat Completions | `/v1/chat/completions` | OpenAI SDK, `createOpenAICompatible`, … | | GPT-5 family (AWS) | OpenAI Responses | `/v1/responses` | OpenAI SDK Responses API | Sending a Responses-only model to `/v1/chat/completions` returns a 400 naming the right endpoint, and no request reaches the cloud, so nothing is billed. Claude on AWS is the exception. Bedrock serves Claude through an older Anthropic schema, so Alien adapts the request there: Anthropic's server-executed tools such as web search and code execution are dropped, along with a few recent fields, and mid-conversation system messages are folded into the turn before them. Those edits are AWS-only. One thing is not: Alien forwards only a known set of `anthropic-beta` families on every cloud, so a beta outside that set is dropped on GCP and Azure too. ## Model Availability [#model-availability] The list is checked against the cloud on first use and then reused for the life of the workload. Model access changes only when someone enables a model in the cloud console, and a redeploy picks that up — there is no expiry to wait out. A model the cloud gives no clear answer on stays in the list and is rechecked on the next call, so a transient cloud problem never shrinks your model menu and the check never fails a deploy. Such a model can still fail when you call it, so handle an error from a model you have not used before. Claude is the one family gated behind a one-time step, on all three clouds — see [Models & Prerequisites](/docs/infrastructure/ai/models). Everything else works the moment the resource deploys, and the deploy succeeds either way. ## Platform Notes [#platform-notes] ### AWS — Amazon Bedrock [#aws--amazon-bedrock] Nothing is provisioned — Bedrock is an account-level API, so deploying the resource grants access and nothing more. The widest model selection of the three clouds, including the GPT-5 family and the full Claude range. ### GCP — Vertex AI [#gcp--vertex-ai] Alien enables the Vertex AI API and grants a custom role limited to prediction, rather than the broad `roles/aiplatform.user`. Gemini and Claude are available. ### Azure — Azure AI Foundry [#azure--azure-ai-foundry] The only cloud where Alien creates something lasting: an AI Foundry account plus deployments for three OpenAI models, which you'll see in the customer's portal and on their bill. Deleting the resource deletes the account. Claude is the exception — a first Claude deployment requires accepting Marketplace terms in the portal, which no API can do on your behalf, so it stays a one-time manual step and Claude appears once that deployment exists. ### Kubernetes / On-Prem [#kubernetes--on-prem] Alien does not provision AI here. Supply an external AI binding at deploy time with your own provider key, and the SDK calls that provider directly. Add the binding at the same time as the resource: without one the deployment fails, and it fails at deploy rather than at validation. ### Local [#local] A bring-your-own-key binding using OpenAI, with the key from `OPENAI_API_KEY`; `alien dev` fails with an actionable error if it is unset. Cloud model ids do not resolve, and `getAvailableModels()` returns a short built-in list rather than querying the provider. Set `ALIEN_AI_LOCAL_BASE_URL` to point at any OpenAI-compatible server instead, including one running on your machine. ## Design Decisions [#design-decisions] **No configuration on the resource.** Model, temperature, and every other knob belong to the request, not the stack, so changing model does not require a redeploy and one stack file works across three clouds whose model menus differ. **Forward, don't translate.** A translation layer over one OpenAI-shaped API drops whatever it has not been taught — tool-calling variants, thinking blocks, new fields. Forwarding gives you the model's real API, at the price of picking a client that matches the model. Bedrock's older Claude schema is the one place Alien has to compromise, which is why it is called out under Protocols. **The model list is checked, not declared.** No cloud answers "which models can this account invoke". A published list would be wrong for every customer who hasn't enabled a model, and wrong in the direction that fails at runtime, so Alien checks instead and reports what actually answered. **No Alien-level rate limit.** Any limit Alien imposed would be a second, weaker ceiling below the provider's real quota, and a number you could not plan against. **Delete removes the Azure account.** On Azure, deleting the resource deletes the AI Foundry account and its model deployments, consistent with every other Alien resource. AWS and GCP have nothing to delete. # Overview (/docs/infrastructure/ai) `alien.AI` is managed model inference. Declare it in your `alien.ts` and Alien wires your workload to the AI service already in the customer's cloud — Bedrock, Vertex AI, or Azure AI Foundry — so calls are billed to that cloud account and authorized by the workload's own identity. There is no API key in your application. ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | ---------------------------- | ------------------------------------------- | | AWS | Amazon Bedrock | Nothing to provision | | GCP | Vertex AI | Alien (enables the API) | | Azure | Azure AI Foundry | Alien (creates the account and deployments) | | Kubernetes / On-Prem | External (your provider key) | You, via an external binding | | Local | External (your provider key) | You, via `OPENAI_API_KEY` | On AWS nothing is created at all — Bedrock is an account-level API, so deploying the resource just grants your workload access. On GCP, Alien enables the Vertex AI API and grants the role. On Azure it goes further and creates an AI Foundry account with model deployments (see [Behavior](/docs/infrastructure/ai/behavior)). ## When to Use [#when-to-use] Use AI when your product calls language models and you deploy into your customers' clouds. Each customer's inference runs on their account, under their quotas and data-handling terms, and you ship no keys. The tradeoff is that the model menu is the customer's, not yours: what you can call depends on which cloud they run and what they've enabled there. If your product needs one specific model everywhere, or a provider none of the three clouds host, bring your own key with an external binding instead. ## Stack Definition [#stack-definition] ```typescript const assistant = new alien.AI("assistant").build() ``` | Parameter | Type | Default | Description | | --------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | — | Resource identifier: letters, digits, hyphens and underscores (`[A-Za-z0-9-_]`), up to 64 characters. Immutable after create. | There is nothing else to configure. The model is chosen per request in your code, not in the stack, so a stack file does not pin you to a model or a provider. Link it to a workload and grant `ai/invoke`: ```typescript const api = new alien.Worker("api") .code({ type: "source", src: "./api", toolchain: { type: "typescript" } }) .link(assistant) .permissions("execution") .build() export default new alien.Stack("assistant-app") .platforms(["aws", "gcp", "azure"]) .add(assistant, "live") .add(api, "live") .permissions({ profiles: { execution: { "*": ["ai/invoke"] }, }, }) .build() ``` `ai/invoke` grants inference and nothing else — no model management, no deployment writes. See [Permissions](/docs/permissions). ## Calling a Model [#calling-a-model] ```typescript import { ai } from "@alienplatform/sdk" const llm = ai("assistant") // Model ids differ per cloud, so take one the deployment actually has. const [model] = await llm.getAvailableModels() const completion = await llm.chat.completions.create({ model: model.id, messages: [{ role: "user", content: "Summarize this support thread." }], }) ``` Claude is the exception. It speaks Anthropic's API rather than OpenAI's, so it needs an Anthropic client — `getAiConnection()` gives you the endpoint to point it at, and there is still no key: ```typescript import { createAnthropic } from "@ai-sdk/anthropic" import { getAiConnection } from "@alienplatform/sdk" const connection = await getAiConnection("assistant") const anthropic = createAnthropic({ baseURL: connection.baseURL, apiKey: "" }) const model = anthropic("claude-sonnet-4.6") ``` Alien forwards your request to each model in that model's own format instead of translating between them, which is why the client has to match the model. See [Behavior](/docs/infrastructure/ai/behavior#protocols). `getAiConnection()` works with any OpenAI- or Anthropic-compatible client, so the Vercel AI SDK, the OpenAI SDK, and the Anthropic SDK all attach the same way. ## Discovering Models [#discovering-models] Model availability differs per cloud and per account, so ask at runtime rather than hardcoding an id: ```typescript const models = await ai("assistant").getAvailableModels() // [{ id: "gpt-oss-20b", provider: "openai", displayName: "GPT-OSS 20B" }, …] ``` This returns only what the deployment can actually invoke right now — a model the account has not enabled does not appear. It is what you want behind a model picker. ## Local Development [#local-development] There is no cloud identity on your machine, so locally the resource becomes a bring-your-own-key binding and the SDK calls the provider directly: ```bash OPENAI_API_KEY=sk-... alien dev ``` `alien dev` fails with an actionable error if the key is missing. The cloud model ids do not resolve locally, and `getAvailableModels()` returns a short built-in list for the provider rather than querying it, so pass whatever model id your key can reach. See the [API Reference](/docs/infrastructure/ai/api) for the full SDK surface, and [Behavior](/docs/infrastructure/ai/behavior) for limits and per-platform detail. # Models & Prerequisites (/docs/infrastructure/ai/models) Every cloud serves a different set of models, and each has setup a customer must do before some of them answer. `getAvailableModels()` reports what a deployment can invoke right now — use it at runtime rather than hardcoding an id, since the list narrows to what that account has enabled. Which endpoint each model uses is decided by its protocol; see [Behavior](/docs/infrastructure/ai/behavior#protocols). ## AWS — Amazon Bedrock [#aws--amazon-bedrock] The widest selection of the three clouds: 45 models. ### Prerequisites [#prerequisites] | Step | Who does it | When | | ---------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------- | | Model access | Nobody, for every model except Claude — Bedrock enables them automatically in commercial regions | — | | Claude agreement and use-case form | The customer, once per account | Before any Claude model answers | | Quota | AWS provisions per-model TPM and RPM | See below | Since October 2025, Bedrock enables serverless foundation models automatically in commercial regions, and every non-Claude model in Alien's catalog is invocable with no setup at all. Claude is the exception, and it takes two steps rather than one: a model agreement per Claude model, and a use-case form submitted once for the account. Submitting the form from an AWS Organizations management account covers every member account at once. Neither step needs the console: ```bash # once per account aws bedrock put-use-case-for-model-access --form-data "$(base64 < form.json)" # once per Claude model OFFER=$(aws bedrock list-foundation-model-agreement-offers --model-id "$MODEL" \ --query 'offers[0].offerToken' --output text) aws bedrock create-foundation-model-agreement --offer-token "$OFFER" --model-id "$MODEL" aws bedrock get-foundation-model-availability --model-id "$MODEL" ``` Agreements are usage-priced with no fixed fee, so accepting them costs nothing until a call is made. `get-foundation-model-availability` reports agreement, authorization, entitlement, and region status separately, so it tells you which of the four is missing. The GPT-5 family runs on a different Bedrock endpoint with its own quotas, which count input and output tokens separately rather than together. Raising your Claude quota does nothing for GPT-5, and vice versa. **Check quota on a new AWS account.** Quotas are per model and per region, and AWS states that *"new AWS accounts might receive reduced quotas."* A fresh account can therefore be far below the published defaults, and inference fails in a way that looks like a broken integration rather than a quota. Check Service Quotas for Amazon Bedrock before assuming the deployment is at fault. ### Models [#models] | Family | Model ids | Protocol | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | Claude | `claude-opus-5`, `claude-sonnet-5`, `claude-opus-4.8`, `claude-opus-4.7`, `claude-opus-4.6`, `claude-opus-4.5`, `claude-sonnet-4.6`, `claude-sonnet-4.5`, `claude-haiku-4.5`, `claude-fable-5` | Anthropic | | GPT-5 | `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5`, `gpt-5.4` | Responses | | GPT-OSS | `gpt-oss-20b`, `gpt-oss-120b`, `gpt-oss-safeguard-20b`, `gpt-oss-safeguard-120b` | OpenAI | | Qwen | `qwen3-32b`, `qwen3-coder-30b`, `qwen3-next-80b`, `qwen3-vl-235b` | OpenAI | | Mistral | `mistral-large-3`, `devstral-2`, `magistral-small`, `ministral-3-14b`, `ministral-3-8b`, `ministral-3-3b` | OpenAI | | Nemotron | `nemotron-nano-9b`, `nemotron-nano-12b`, `nemotron-nano-3-30b`, `nemotron-super-3-120b` | OpenAI | | MiniMax | `minimax-m2`, `minimax-m2.1`, `minimax-m2.5` | OpenAI | | Gemma | `gemma-3-4b`, `gemma-3-12b`, `gemma-3-27b` | OpenAI | | GLM | `glm-4.7`, `glm-4.7-flash`, `glm-5` | OpenAI | | DeepSeek | `deepseek-v3.2` | OpenAI | | Kimi | `kimi-k2.5` | OpenAI | | Palmyra | `palmyra-vision-7b` | OpenAI | The same ten Claude models are served on all three clouds. The GPT-5 family answers on the Responses endpoint and nowhere else — a chat-completions call returns a 400 naming the right endpoint. `gpt-oss-20b` and `gpt-oss-120b` answer on both. Model availability also varies by region. A model enabled in `us-east-1` may not exist in another region, so check Bedrock's regional availability table for the customer's region rather than assuming parity. ## GCP — Vertex AI [#gcp--vertex-ai] 15 models: Gemini and Claude. ### Prerequisites [#prerequisites-1] | Step | Who does it | When | | ----------------------- | ----------------------------------------------- | ------------------------------- | | Vertex AI API | Alien, at deploy | Automatic | | Claude terms of service | The customer, once per project, in Model Garden | Before any Claude model answers | | Quota | Per region, requested in the Cloud console | See below | Claude is enabled from Vertex AI Model Garden: find the model, click **Enable**, and accept Anthropic's terms. It is a per-project step, so a customer who has done it for one project still has to do it for another. **Quota is per region, and the global endpoint has its own pool.** A project with room in `us-central1` can still be at zero on a global or multi-region endpoint, because those draw from separate allocations. Check the quota page for the region the deployment actually runs in. ### Models [#models-1] | Family | Model ids | Protocol | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | Claude | `claude-opus-5`, `claude-sonnet-5`, `claude-opus-4.8`, `claude-opus-4.7`, `claude-opus-4.6`, `claude-opus-4.5`, `claude-sonnet-4.6`, `claude-sonnet-4.5`, `claude-haiku-4.5`, `claude-fable-5` | Anthropic | | Gemini | `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`, `gemini-3.5-flash`, `gemini-3.1-flash-lite` | OpenAI | Gemini is GCP-only, though Google's Gemma models are on AWS. Calls go to whatever location the deployment runs in — Alien does not reroute per model — and Google serves the 2.5 family in-region but the 3.x models only on its `global` location. So on a region-pinned deployment the 3.x models will not appear in the model list. ## Azure — Azure AI Foundry [#azure--azure-ai-foundry] 13 models: three OpenAI models Alien deploys, and Claude. ### Prerequisites [#prerequisites-2] | Step | Who does it | When | | --------------------------------------- | ----------------------------------- | -------------------------------- | | Foundry account and deployments | Alien, at deploy | Automatic, for the OpenAI models | | Claude Marketplace terms and deployment | The customer, in the Foundry portal | Before any Claude model answers | | Quota | Per model, per subscription, in TPM | See below | Alien creates the account and deploys `gpt-4.1`, `gpt-4o-mini`, and `model-router`. Claude cannot be automated: the first deployment requires accepting Marketplace terms, which is a portal action no API performs. Once the customer creates the deployment there, Claude appears in `getAvailableModels()` after the workload next restarts, since the model list is checked once per process. **Quota moved to the subscription in May 2026.** It is now tracked per model per subscription rather than per resource, and Global Standard deployments of the same model share one pool across regions. A new subscription can show **0 TPM in every region**, which blocks deployment rather than just throttling it. Viewing quota needs the Cognitive Services Usages Reader role; raising it needs Owner or Contributor. ### Models [#models-2] | Family | Model ids | Protocol | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | Claude | `claude-opus-5`, `claude-sonnet-5`, `claude-opus-4.8`, `claude-opus-4.7`, `claude-opus-4.6`, `claude-opus-4.5`, `claude-sonnet-4.6`, `claude-sonnet-4.5`, `claude-haiku-4.5`, `claude-fable-5` | Anthropic | | OpenAI | `gpt-4.1`, `gpt-4o-mini`, `model-router` | OpenAI | `model-router` is a single deployment that picks an underlying model per request, defaulting to the cheapest model within a narrow quality band of the best one. Its context window is that of the smallest model it can route to. **Check the region offers all three models before deploying.** Alien creates the three deployments together at the `GlobalStandard` tier and treats any one failing as a failed resource, so a region missing any of them fails the whole thing — mid-provision, after the account already exists, because nothing checks up front. Most Azure regions are fine. North Europe is a live counter-example: it offers `gpt-4.1` and `gpt-4o-mini` only as provisioned SKUs, not `GlobalStandard`, and does not offer `model-router` at all. To check a region before you commit to it: ```bash az rest --method get \ --url "https://management.azure.com/subscriptions//providers/Microsoft.CognitiveServices/locations//models?api-version=2024-10-01" \ --query "value[?model.name=='model-router'] | [0].model.skus[?name=='GlobalStandard']" ``` An empty result means that region will fail. ## Local and Kubernetes [#local-and-kubernetes] Neither uses a cloud catalog. Both take a bring-your-own-key binding, so you can call whatever your key can reach, but `getAvailableModels()` returns a short built-in list per provider rather than querying it. The only prerequisite is the key: `OPENAI_API_KEY` for `alien dev`, or the key inside the external binding on Kubernetes. ## When a Model Is Missing [#when-a-model-is-missing] A model absent from `getAvailableModels()` is almost always one of these, in order of likelihood: 1. **A one-time step is outstanding** — the agreement and use-case form on AWS, Model Garden terms on GCP, Marketplace terms on Azure. This is the usual answer for Claude, and only for Claude. 2. **Quota is zero** — common on new accounts and subscriptions, on all three clouds. On AWS a model with no access reads 0 applied quota, so a quota increase does nothing until the access exists. 3. **The model isn't offered in that region** — availability differs per region on every cloud. On AWS and GCP none of these fail the deploy: the model list narrows, every other model keeps working, and the model reappears once the customer completes the step. Azure is the exception — a model it cannot deploy, whether from zero quota or from the region not offering it, fails the resource rather than just narrowing the list. ## Sources [#sources] Cloud requirements change; these are the pages to re-check. * [Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) · [automatic enablement](https://aws.amazon.com/about-aws/whats-new/2025/10/amazon-bedrock-automatic-enablement-serverless-foundation-models) * [Bedrock runtime quotas](https://docs.aws.amazon.com/bedrock/latest/userguide/quotas-runtime.html) · [regional availability by model](https://docs.aws.amazon.com/bedrock/latest/userguide/models-region-compatibility.html) * [Claude on Vertex AI](https://cloud.google.com/blog/products/ai-machine-learning/global-endpoint-for-claude-models-generally-available-on-vertex-ai) * [Foundry quotas and limits](https://learn.microsoft.com/en-us/azure/foundry/openai/quotas-limits) · [model router](https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-router) # Pricing (/docs/infrastructure/ai/pricing) Alien routes inference to the AI service in your customer's cloud. The customer pays the cloud provider for the tokens, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Global-endpoint rates. Last verified: August 2026.* ## Token Pricing [#token-pricing] Inference is billed per token, priced per model. Output costs several times more than input on every provider. | Model | Provider | Input | Output | | ----------------- | --------------- | ----------------- | ------------------ | | Claude Sonnet 4.5 | AWS, GCP, Azure | $3.00 per million | $15.00 per million | | GPT-OSS 120B | AWS | $0.15 per million | $0.60 per million | Claude costs the same wherever it is served — Anthropic sets the rate and the clouds resell it. The figures above are the global-endpoint rate; pinning a model to one region adds roughly 10% on every cloud. Model choice still moves the bill far more than cloud or region does: the two rows above are the same workload more than 20× apart. ## Infrastructure Pricing [#infrastructure-pricing] | Platform | Standing charge | | -------- | ------------------------------------------------------------------------------ | | AWS | None. Bedrock is an account-level API with nothing provisioned. | | GCP | None. Enabling the Vertex AI API is free. | | Azure | None. The AI Foundry account and its deployments bill per token, not per hour. | An AI resource that nothing calls costs nothing, on all three clouds. ## Example: 5M Input + 1M Output Tokens/month [#example-5m-input--1m-output-tokensmonth] A support assistant handling about 5,000 conversations a month, at roughly 1,000 input and 200 output tokens each. | Model | Input | Output | Monthly | | ----------------- | ------ | ------ | ---------- | | Claude Sonnet 4.5 | $15.00 | $15.00 | **$30.00** | | GPT-OSS 120B | $0.75 | $0.60 | **$1.35** | Same traffic, same code, one line different in the request. ## Local and Kubernetes [#local-and-kubernetes] No cloud charges. Both use a bring-your-own-key binding, so you pay your provider directly at their published rates. ## Free Tiers [#free-tiers] * **AWS**: no free tier for Bedrock on-demand inference. * **GCP**: new-account credits apply to Vertex AI; there is no always-free inference allowance. * **Azure**: new-account credits apply to Foundry; there is no always-free inference allowance. ## Sources [#sources] * [Amazon Bedrock Pricing](https://aws.amazon.com/bedrock/pricing/) * [Vertex AI Pricing — Claude models](https://cloud.google.com/vertex-ai/generative-ai/pricing#claude-models) * [Microsoft Foundry Models pricing](https://azure.microsoft.com/en-us/pricing/details/phi-3/) * [Anthropic model pricing](https://platform.claude.com/docs/en/about-claude/pricing) # Behavior & Limits (/docs/infrastructure/artifact-registry/behavior) ## What Gets Provisioned vs What Happens at Runtime [#what-gets-provisioned-vs-what-happens-at-runtime] An `ArtifactRegistry` has two phases: **Provisioning** (when your stack deploys) creates the registry itself — the container that holds repositories: | Platform | What provisioning creates | | -------- | ---------------------------------------------------------------------------------------- | | AWS | IAM roles for pull/push access. ECR itself is implicit — no discrete resource to create. | | GCP | A GAR repository (the container), plus service accounts for pull/push access. | | Azure | An ACR resource (the container), which holds repositories created at runtime. | | Local | Connects to the running local container image registry. | **Runtime** (as images are built and deployed) creates repositories *within* the registry: | Platform | What creating a repository `my-app` does | | -------- | ------------------------------------------------------------------------------------------------- | | AWS | Creates an ECR repository named `{prefix}-my-app` via the `CreateRepository` API. | | GCP | No-op — GAR creates image paths implicitly on first push. Returns the routable name. | | Azure | Creates a scope map for access control. The image repository is created implicitly on first push. | | Local | Pushes a marker manifest to create the repository in the local container image registry. | ## Guarantees [#guarantees] **Temporary Credentials.** Generated credentials are scoped to a single repository with the specified permission level (pull or push-pull). Credentials expire after the requested TTL — they cannot be used after expiry. **Cross-Account Isolation.** Cross-account access grants are scoped to specific accounts, service accounts, and service types. Granting pull access to one account does not affect other accounts. ## Repository Naming [#repository-naming] When a repository named `my-app` is created, the name is transformed per platform to fit cloud naming constraints. Any reasonable string (e.g., a project ID like `prj_abc123`) is adapted by each provider. | Platform | How the repository name is constructed | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | AWS (ECR) | Prefixed: `{registry_prefix}-{name}` (e.g., `alien-artifacts-my-app`). Underscores allowed. | | GCP (GAR) | Nested under the GAR repository: `{project}/{gar_repo}/{name}`. Image paths allow lowercase letters, numbers, dots, underscores, and hyphens. | | Azure (ACR) | Used directly: `{name}`. Hash-based fallback if scope map name exceeds Azure constraints. | | Local | Namespaced: `{binding_name}/{name}` (e.g., `artifacts/my-app`). | Each repository is addressed by its **routable name** — the full, platform-specific path used for credential generation and cross-account grants. ## Limits [#limits] | Limit | Value | Notes | | --------------------- | ------------------ | ---------------------------------------------------------- | | Images per repository | 100,000 (ECR) | GCP and Azure have higher or no documented limits. | | Credential TTL | Platform-dependent | ECR: 12 hours max. ACR: varies by token type. | | Cross-account access | AWS and GCP only | Azure does not support cross-account access through Alien. | ## Platform Notes [#platform-notes] ### AWS (ECR) [#aws-ecr] * **Credential generation** uses `GetAuthorizationToken` — returns a base64-encoded username:password pair valid for 12 hours. * **Cross-account access** is implemented via ECR repository policies (IAM-style JSON policies). Grants are scoped to specific AWS account IDs and IAM role ARNs. * **Replication**: Images can be automatically replicated to additional regions via the `replicationRegions` stack option. This ensures Lambda workers in any region can pull images from a nearby ECR endpoint. * **Image pull** for Lambda workers in other accounts requires explicit repository policy + Lambda execution role permissions. * **Rate limits**: Pull rates are generous and significantly higher than Docker Hub. No per-repository throttling for authenticated requests. ### GCP (Artifact Registry) [#gcp-artifact-registry] * **Repository creation** is a no-op — GAR creates image paths implicitly on first push. The GAR repository itself (the container) is created at provisioning time by alien-infra. * **Repository naming**: image paths within a GAR repository allow lowercase letters, numbers, dots, underscores, and hyphens. No sanitization needed — underscores are valid. * **Credential generation** uses service account impersonation — generates a short-lived access token. **Max TTL is 1 hour (3,600 seconds)** — higher values are silently capped. * **Cross-account access** is implemented via IAM bindings on the repository. Grants are scoped to GCP project numbers and service account emails. * **No pre-signed URLs** — image layers are fetched through the registry API with bearer token authentication. * **Multi-format support**: Artifact Registry supports Docker images, Maven, npm, Python packages, and more — though Alien only uses Docker image support. * **Max artifact size**: 5 TB. ### Azure (ACR) [#azure-acr] * **Credential generation** uses a stateless AAD OAuth2 token exchange (AAD token → refresh token → scoped access token). Credentials are short-lived (\~5 minutes, controlled by Azure). No persistent resources are created. * **No cross-account access** through Alien. Azure ACR supports cross-subscription access via Azure RBAC, but this is not exposed through Alien. * **Tier-based limits**: * Basic: 10 GB storage, 10 write ops/min, 1,000 read ops/min * Standard: 100 GB storage, 100 write ops/min, 3,000 read ops/min * Premium: 500 GB storage, 2,000 write ops/min, 10,000 read ops/min * **Admin credentials** are used during initial setup. ### Local [#local] * Runs an in-process container image registry. * Basic auth support for credential generation. * No cross-account access (not applicable). * Suitable for development and testing. ## Design Decisions [#design-decisions] **No unified cross-account model.** AWS uses IAM policies with account IDs and role ARNs. GCP uses IAM bindings with project numbers and service account emails. These are fundamentally different models that can't be cleanly unified, so Alien keeps cross-account grants platform-specific (`aws` vs `gcp`) rather than forcing a lowest-common-denominator abstraction. **Replication is AWS-only.** ECR supports native cross-region replication. GCP Artifact Registry supports multi-region repositories but through different mechanisms. Rather than building a lowest-common-denominator abstraction, replication is exposed as an AWS-specific option. # Overview (/docs/infrastructure/artifact-registry) An Artifact Registry is a **collection of container image repositories** with shared identity and access control. You provision one per stack; Alien and the build pipeline manage the repositories inside it. ``` ArtifactRegistry("images") ← provisioned once (IAM roles, cloud registry, etc.) ├── images/app-a ← repositories created as your images are built and pushed ├── images/app-b └── images/app-c ``` Each platform maps this to its native container registry service: ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | ----------------------------------- | ---------------- | | AWS | Amazon ECR | Alien | | GCP | Google Artifact Registry | Alien | | Azure | Azure Container Registry | Alien | | Kubernetes / On-Prem | External container image registry | Cluster operator | | Local | In-process container image registry | Alien | Artifact Registry is **not an app-facing binding.** Unlike `storage`, `kv`, `queue`, and `vault`, your application code does not resolve it at runtime — there is no `artifactRegistry()` accessor in the SDK. It is a provisioned resource: Alien and the build pipeline create repositories and push [Container](/docs/infrastructure/container) and [Daemon](/docs/infrastructure/daemon) images into it for you. ## When to Use [#when-to-use] Provision an Artifact Registry when your stack builds and runs container images — for example [Containers](/docs/infrastructure/container) and [Daemons](/docs/infrastructure/daemon) — and you want Alien to own the registry, its IAM, and cross-account image distribution. ## Provisioning [#provisioning] Declare the registry in your stack. The builder is cloud-agnostic; `replicationRegions` is the only configurable option and applies to AWS ECR only. ```typescript // alien.ts const images = new alien.ArtifactRegistry("images") .replicationRegions(["us-west-2", "eu-west-1"]) // AWS only .build() ``` | Method | Applies to | Description | | --------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- | | `replicationRegions(regions: string[])` | AWS only | Replicate ECR images to additional regions, so images pushed in the home region are available where compute runs. | | `build()` | All | Validate the configuration and return the resource. | ## Cross-Account Access [#cross-account-access] On AWS and GCP, Alien can grant other cloud accounts permission to pull images from the registry — useful when compute in a different account needs to run your images. Cross-account pull access is not supported on Azure through Alien. See [Behavior & Limits](/docs/infrastructure/artifact-registry/behavior) for how access grants are scoped and isolated. # Pricing (/docs/infrastructure/artifact-registry/pricing) Alien provisions Artifact Registry in your customer's cloud. The customer pays the cloud provider for the underlying registry. Alien currently lists Artifact Registry as a free auto-managed resource on [Pricing](/pricing). *Prices shown for US East regions. Last verified: April 2026.* ## Storage Pricing [#storage-pricing] | Provider | Service | Price per GB/month | Notes | | -------- | ----------------- | ------------------- | ------------------------------------------- | | AWS | ECR | $0.10 | Per GB of stored images. | | GCP | Artifact Registry | $0.10 | First 0.5 GB free. | | Azure | ACR Basic | \~$5.00/month flat | 10 GB included. $0.10/GB overage. | | Azure | ACR Standard | \~$20.00/month flat | 100 GB included. $0.10/GB overage. | | Azure | ACR Premium | \~$50.00/month flat | 500 GB included. Geo-replication available. | ## Data Transfer [#data-transfer] Image pull (egress) costs vary. Pulls within the same region are typically free or very cheap. Cross-region and internet egress follows standard data transfer pricing. ## Example: 10 GB Image Storage/month [#example-10-gb-image-storagemonth] | Provider | Cost | Notes | | --------------------- | ------ | ------------------------------ | | AWS ECR | $1.00 | 10 GB × $0.10 | | GCP Artifact Registry | $0.95 | 9.5 GB billed (0.5 GB free) | | Azure ACR Basic | $5.00 | Flat monthly (10 GB included) | | Azure ACR Standard | $20.00 | Flat monthly (100 GB included) | ## Platform Notes [#platform-notes] * **ECR** charges purely per-GB with no base fee. Best value for small image stores. * **ACR** is tier-based. Basic tier's 10 write ops/min limit can bottleneck CI/CD pipelines. Standard or Premium recommended for production. * **Artifact Registry** pricing is simple and per-GB, similar to ECR. ## Sources [#sources] * [AWS ECR Pricing](https://aws.amazon.com/ecr/pricing/) * [GCP Artifact Registry Pricing](https://cloud.google.com/artifact-registry/pricing) * [Azure ACR Pricing](https://azure.microsoft.com/en-us/pricing/details/container-registry/) # API Reference (/docs/infrastructure/container/api) ## Constructor [#constructor] ```typescript new alien.Container(id: string) ``` | Parameter | Type | Required | Description | | --------- | -------- | -------: | ------------------------------------------------------------------------------------------- | | `id` | `string` | Yes | Container resource ID. Use lowercase DNS-compatible names such as `api` or `vector-reader`. | ## code [#code] Sets what the container runs. ```typescript .code({ type: "image", image: "ghcr.io/acme/api:v1" }) .code({ type: "source", src: "./api", toolchain: { type: "docker" } }) ``` | Field | Type | Required | Description | | ----------- | --------------------- | -----------: | ------------------------------------------- | | `type` | `"image" \| "source"` | Yes | Use an existing image or build from source. | | `image` | `string` | For `image` | Container image reference. | | `src` | `string` | For `source` | Source directory. | | `toolchain` | `ToolchainConfig` | For `source` | Build toolchain. | If you use `image`, Alien deploys that image directly. If you use `source`, Alien runs the selected toolchain during build/release and turns the result into a container image. In both cases the configured image command starts your app directly; there is no Worker runtime wrapper in front of a Container. Supported source toolchains are the same toolchains used by Workers: | Toolchain | Use For | | ------------ | ---------------------------------------------------- | | `typescript` | TypeScript or JavaScript projects compiled with Bun. | | `rust` | Rust projects compiled with Cargo. | | `docker` | Projects with a Dockerfile. | ## cpu [#cpu] Sets CPU requirements. ```typescript .cpu(1) .cpu({ min: "0.5", desired: "2" }) ``` | Parameter | Type | Required | Description | | --------- | -------------------------------------------- | -------: | ----------------------------------------------------- | | `value` | `number \| { min: string; desired: string }` | Yes | CPU in vCPUs. A number sets both `min` and `desired`. | ## memory [#memory] Sets memory requirements. ```typescript .memory("1Gi") ``` | Parameter | Type | Required | Description | | --------- | -------- | -------: | ------------------------------------------------------------ | | `size` | `string` | Yes | Memory request, for example `"512Mi"`, `"1Gi"`, or `"16Gi"`. | ## ports [#ports] Adds internal ports. ```typescript .port(8080) .ports([8080, 9090]) ``` | Method | Description | | --------------- | ---------------------------- | | `.port(port)` | Adds one internal port. | | `.ports(ports)` | Adds several internal ports. | Every container must define at least one port. ## publicEndpoint [#publicendpoint] Publicly exposes a named endpoint through provider load-balancing infrastructure. ```typescript .port(8080) .publicEndpoint("web", 8080, "http") .publicEndpoint("admin", 9090, "tcp") .publicEndpoint("tenants", 8080, { protocol: "http", hostLabel: "tenants", wildcardSubdomains: true, }) ``` | Method | Description | | -------------------------------------- | -------------------------------------------- | | `.publicEndpoint(name, port, options)` | Exposes a specific port as a named endpoint. | Options can be a protocol string (`"http"` or `"tcp"`) or an object: | Field | Type | Default | Description | | -------------------- | ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------- | | `protocol` | `"http" \| "tcp"` | — | Endpoint protocol. HTTP endpoints terminate TLS at the load balancer; TCP endpoints pass traffic through. | | `hostLabel` | `string` | endpoint name | Host label on the deployment domain. `"@"` serves the endpoint at the domain apex. | | `wildcardSubdomains` | `boolean` | `false` | Also routes `*..` to the endpoint. Cannot be combined with the apex host label. | Additional ports remain internal unless they have their own endpoint. See [External URLs](/docs/external-urls) for how hostnames, DNS, and TLS work. ## replicas and autoscaling [#replicas-and-autoscaling] ```typescript .replicas(3) .minReplicas(2) .maxReplicas(20) .autoScale({ min: 2, desired: 4, max: 20, targetCpuPercent: 70, targetMemoryPercent: 80, targetHttpInFlightPerReplica: 100, }) ``` | Method | Description | | --------------------- | ---------------------------------------------- | | `.replicas(count)` | Fixed replica count. | | `.minReplicas(count)` | Sets autoscaling minimum and desired replicas. | | `.maxReplicas(count)` | Sets autoscaling maximum replicas. | | `.autoScale(config)` | Sets the full autoscaling object. | Use either fixed replicas or autoscaling. ## stateful and storage [#stateful-and-storage] ```typescript .stateful(true) .persistentStorage("500Gi") .persistentStorage("500Gi", { mountPath: "/var/lib/postgresql/data" }) .ephemeralStorage("100Gi") ``` | Method | Description | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | `.stateful(enabled)` | Enables stable replica identity. Replicas get stable ordinals (`api-0`, `api-1`, …). | | `.persistentStorage(size, options?)` | Adds persistent storage and marks the container stateful. `options.mountPath` sets the mount path (default `/data`). | | `.ephemeralStorage(size)` | Requests scratch storage that may be lost when a replica restarts or moves. | Each ordinal keeps its volume: the volume survives replica restarts and replacement, and the replica is placed where its volume lives. ## gpu [#gpu] ```typescript .gpu({ type: "nvidia-t4", count: 1 }) ``` | Field | Type | Required | Description | | ------- | -------- | -------: | -------------------- | | `type` | `string` | Yes | GPU type identifier. | | `count` | `number` | Yes | Number of GPUs. | GPU placement depends on the machine pools available for the deployment. ## healthCheck [#healthcheck] ```typescript .healthCheck({ path: "/health", port: 8080, method: "GET", timeoutSeconds: 1, failureThreshold: 3, }) ``` | Field | Type | Default | Description | | ------------------ | -------- | -------------- | -------------------------------------- | | `path` | `string` | `"/health"` | HTTP path to check. | | `port` | `number` | Container port | Port to check. | | `method` | `string` | `"GET"` | HTTP method. | | `timeoutSeconds` | `number` | `1` | Probe timeout. | | `failureThreshold` | `number` | `3` | Consecutive failures before unhealthy. | ## environment, links, and permissions [#environment-links-and-permissions] ```typescript .environment({ LOG_LEVEL: "info" }) .link(storage) .permissions("execution") ``` | Method | Required | Description | | ----------------------- | -------: | ------------------------------------------------------- | | `.environment(vars)` | No | Merges environment variables into the container config. | | `.link(resource)` | No | Injects binding access to another resource. | | `.permissions(profile)` | Yes | Permission profile name. | ## placement [#placement] ```typescript .cluster("compute") .pool("gpu") ``` | Method | Description | | -------------- | ---------------------------------------------------------------------------------------------------------- | | `.cluster(id)` | Advanced: run on a specific container cluster. If omitted, Alien can assign or create the default cluster. | | `.pool(name)` | Advanced: run on a specific machine pool within the cluster. | ## command [#command] ```typescript .command(["./server", "--port", "8080"]) ``` Overrides the image default command. The container runs your entrypoint directly — there is no runtime wrapper. Bindings run in-process via `@alienplatform/bindings`. ## stopGracePeriod [#stopgraceperiod] ```typescript .stopGracePeriod(300) ``` Grace period in seconds used when stopping replicas during updates, drains, and deletes. Valid values are 1 through 86400. ## commandsEnabled [#commandsenabled] Enables or disables the Commands protocol. ```typescript .commandsEnabled(true) ``` | Parameter | Type | Default | Description | | --------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enabled` | `boolean` | `false` | When enabled, Alien injects the command-receiver environment so the container's own pull receiver (`createCommandReceiver`) can lease and dispatch commands. See [Remote Commands](/docs/commands#on-a-container-or-daemon). | ## Outputs [#outputs] Container outputs are available in stack state after provisioning. | Field | Type | Description | | ----------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `name` | `string` | Runtime container name. | | `status` | `"pending" \| "running" \| "stopped" \| "failing"` | Current status. | | `currentReplicas` | `number` | Replicas currently running. | | `desiredReplicas` | `number` | Target replica count. | | `internalDns` | `string` | Internal service DNS name. | | `publicEndpoints` | `Record` | Public endpoints keyed by endpoint name. | | `replicas` | `ReplicaStatus[]` | Per-replica status details when reported by the platform. Stateful replicas include their `ordinal`. | # Behavior & Limits (/docs/infrastructure/container/behavior) ## Guarantees [#guarantees] **Keeps running.** A container keeps the configured number of replicas running until the resource is updated, deleted, or marked failed. **Direct execution.** The container runs your image's entrypoint (or `.command(...)`) directly — there is no runtime wrapper process. Bindings run in-process via `@alienplatform/bindings`, and commands are received by an app-owned receiver from `@alienplatform/commands`. **Internal service name.** Every container has an internal DNS name in its deployment environment. Linked services can use the container binding instead of hard-coding cloud-specific service names. **Single public backend port.** A container can declare several named public endpoints, but they must all route to the same backend port. This keeps load-balancer ownership deterministic across AWS, GCP, Azure, Kubernetes, and Local. **Stable stateful identity.** Stateful replicas get stable ordinals (`api-0`, `api-1`, …). Each ordinal keeps its persistent volume: the volume survives replica restarts and replacement, and the replica is placed where its volume lives. **Immutable placement shape.** Resource ID, cluster, stateful mode, ports, and capacity pool are immutable for an existing container. Changing those fields requires replacing the resource. **Cloud scheduling.** On AWS, GCP, and Azure, Alien tracks replica placement and health for containers. The cloud controllers manage surrounding provider resources such as load balancers and persistent disks. ## Limits [#limits] | Limit | Value | Notes | | -------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------ | | Minimum ports | 1 | A container with no ports is invalid. | | Public backend ports | 1 | All public endpoints on a container must route to the same backend port. Additional ports are internal-only. | | Apex endpoints | 1 | Only one endpoint per resource can use `hostLabel: "@"`. | | Health check timeout default | 1 second | Configurable with `timeoutSeconds`. | | Health check failure threshold default | 3 failures | Configurable with `failureThreshold`. | | Persistent storage mount from `.persistentStorage(size)` | `/data` | Override with the `mountPath` option. | ## Platform Notes [#platform-notes] ### AWS [#aws] * Cloud containers run on EC2-backed machines in the customer's AWS account. * Public containers use load-balancing resources for the exposed port. * Stateful containers can create EBS volumes for persistent storage. * Alien tracks container scheduling and replica state. ### GCP [#gcp] * Cloud containers run on Compute Engine-backed machines in the customer's GCP project. * Public containers use Google Cloud load-balancing resources. * Stateful containers can create Persistent Disks. * Alien tracks container scheduling and replica state. ### Azure [#azure] * Cloud containers run on Azure Virtual Machines in the customer's subscription. * Public containers use Azure load-balancing resources. * Stateful containers can create Managed Disks. * Alien tracks container scheduling and replica state. ### Kubernetes / On-Prem [#kubernetes--on-prem] * Stateless containers map to Kubernetes Deployments. * Stateful containers map to StatefulSets. * Persistent storage maps to PersistentVolumeClaim templates. * The build pipeline converts source-based Containers to runnable images before the Kubernetes controller sees them. ### Local [#local] * Containers run through the local container runtime. * Linked filesystem-backed resources are bind-mounted into the container when applicable. * The build pipeline converts source-based Containers to runnable images before the local controller sees them. ## Design Decisions [#design-decisions] **One public backend port per container.** The current controllers create one load-balancer path per container; named endpoints share it. Keeping this limit universal prevents cloud-specific behavior from leaking into the resource model. **Cluster and ports are immutable.** Changing cluster placement or public ports changes networking and load-balancer shape. Alien treats those as replacement-level changes instead of in-place edits. **Alien places cloud replicas.** Provider controllers create the cloud resources around the container. Alien decides where replicas run on the customer's machines. # Overview (/docs/infrastructure/container) A Container runs a service or background process in each customer's cloud. Point it at an existing image, or point it at source code and Alien builds an image during release. Use Containers for HTTP services, databases, vector stores, stream processors, GPU-backed services, and anything that needs stable internal DNS or persistent local storage. Alien creates the required cloud infrastructure, places the container replicas on the customer's machines, and monitors them during rollout. The image's configured command starts your app directly. For source code, `alien build` first produces that runnable image; no Worker runtime wraps the process. ## Deployment Modes [#deployment-modes] Containers can run in three modes inside the customer's cloud. The same `alien.Container` definition works across all of them — the choice is made at install time, not in code. | Mode | What it is | When to choose | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **Managed VMs** | Alien provisions VMs (EC2 / Compute Engine / Azure VMs) in the customer's cloud and runs containers on them through a shared control plane. | Default. Lowest operational overhead — no cluster to maintain. | | **Managed Kubernetes** | Alien creates a dedicated Kubernetes cluster (EKS / GKE / AKS) in the customer's cloud and runs containers on it. | The customer standardizes on Kubernetes but doesn't already have a cluster, or wants Alien to own the cluster lifecycle. | | **BYO Kubernetes** | Install into the customer's platform team's existing cluster via Alien's namespace-scoped enterprise Helm chart. | The customer's platform team already operates a cluster and wants the deployment confined to a single namespace under their governance. | ## Platform Mapping [#platform-mapping] | Platform | Runtime | Managed by | | -------------------- | ----------------------------------------------- | ---------- | | AWS | EC2, EBS, load balancers | Alien | | GCP | Compute Engine, Persistent Disk, load balancers | Alien | | Azure | Virtual Machines, Managed Disks, load balancers | Alien | | Kubernetes / On-Prem | Deployment or StatefulSet | Kubernetes | \| Local | Local container runtime | Alien local runtime | ## When to Use [#when-to-use] Use Container when the service needs to keep running between requests, serve internal traffic over stable DNS, attach persistent or high-throughput local storage, use GPUs, or run an existing image. Use [Worker](/docs/infrastructure/worker) for request-response code. Use [Daemon](/docs/infrastructure/daemon) for a machine-oriented process that should run once on every eligible machine or node. ## Quick Start [#quick-start] ```typescript title="alien.ts" import * as alien from "@alienplatform/core" const api = new alien.Container("api") .code({ type: "image", image: "ghcr.io/acme/api:2026-05-17" }) .cpu(1) .memory("1Gi") .port(8080) .publicEndpoint("web", 8080, "http") .minReplicas(2) .maxReplicas(10) .permissions("execution") .build() export default new alien.Stack("app") .add(api, "live") .build() ``` ## Stateful Services [#stateful-services] Stateful containers get stable replica identity and can attach persistent storage. ```typescript title="alien.ts" const postgres = new alien.Container("postgres") .code({ type: "image", image: "postgres:16" }) .cpu(2) .memory("8Gi") .port(5432) .stateful(true) .persistentStorage("500Gi", { mountPath: "/var/lib/postgresql/data" }) .replicas(1) .permissions("database-runtime") .build() ``` Replicas get stable ordinals (`postgres-0`, `postgres-1`, …), and each ordinal keeps its volume: the volume survives replica restarts and replacement, and the replica is placed where its volume lives. ## Public and Internal Networking [#public-and-internal-networking] Every port is internal unless you attach a named public endpoint to it. Other linked services can reach internal ports through service discovery. Public exposure creates cloud load-balancing for the named endpoint. ```typescript const service = new alien.Container("service") .code({ type: "image", image: "ghcr.io/acme/service:v4" }) .cpu(1) .memory("512Mi") .port(8080) .port(9090) .publicEndpoint("web", 8080, "http") .permissions("execution") .build() ``` Additional ports remain internal unless you attach another endpoint. TCP endpoints pass traffic through a network load balancer without TLS termination — use them for databases and other non-HTTP protocols: ```typescript .publicEndpoint("pg", 5432, "tcp") ``` ## Commands [#commands] Containers can participate in the [Commands protocol](/docs/commands). Setting `.commandsEnabled(true)` injects the `ALIEN_COMMANDS_*` configuration, but it does not start a runtime or polling sidecar. Your app starts `createCommandReceiver()` from `@alienplatform/commands` (or the Rust receiver), registers its handlers, and runs the pull loop itself. ## Configuration [#configuration] | Method | Required | Description | | --------------------------------------------- | -------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.code(code)` | Yes | Container image or source build configuration. | | `.cpu(value)` | Yes | CPU request. Use a number for the same min/desired value or a `{ min, desired }` object. | | `.memory(size)` | Yes | Memory request such as `"512Mi"` or `"2Gi"`. | | `.port(number)` / `.ports(numbers)` | Yes | Internal container ports. | | `.publicEndpoint(name, port, protocol)` | No | Publicly expose a named endpoint on a container port as `"http"` or `"tcp"`. | | `.replicas(count)` | No | Fixed replica count. Cannot be combined with autoscaling. | | `.minReplicas(count)` / `.maxReplicas(count)` | No | Convenience autoscaling controls. | | `.autoScale(config)` | No | Full autoscaling configuration. | | `.stateful(boolean)` | No | Enables stable identity. Required for persistent local storage. | | `.persistentStorage(size, options?)` | No | Persistent volume mounted at `/data` (or `options.mountPath`). Sets `stateful(true)`. | | `.ephemeralStorage(size)` | No | Extra scratch storage that may be lost when a replica moves. | | `.gpu(config)` | No | Request GPU capacity. | | `.healthCheck(config)` | No | HTTP health check used during rollout and refresh. | | `.environment(vars)` | No | Environment variables injected into the container. | | `.link(resource)` | No | Gives the container binding access to another resource. | | `.permissions(profile)` | Yes | Permission profile used for cloud access. | | `.pool(name)` | No | Capacity group to run on. | | `.command(args)` | No | Overrides the image command. | | `.commandsEnabled(boolean)` | No | Injects configuration so an app-owned [command receiver](/docs/commands) can lease this container's commands. Does not start a runtime or sidecar. Default: `false`. | | `.stopGracePeriod(seconds)` | No | Grace period for stopping replicas during updates, drains, and deletes. | See [API Reference](/docs/infrastructure/container/api) for every builder method and [Behavior & Limits](/docs/infrastructure/container/behavior) for platform behavior and limits. # Pricing (/docs/infrastructure/container/pricing) Alien provisions Containers in the customer's cloud. The customer pays the cloud provider for the underlying infrastructure, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. Cloud cost for Containers is driven by machines, persistent storage, load balancers, data transfer, and any container registry storage or transfer. ## Cost Components [#cost-components] | Component | Applies When | Notes | | --------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | Machines | Every cloud container deployment | Container replicas run on machines in the customer account. Autoscaling changes this cost over time. | | Persistent disks | `.persistentStorage(...)` or stateful cloud containers | Billed by size, disk type, provisioned IOPS, and throughput where supported. | | Ephemeral disks / local SSD | `.ephemeralStorage(...)` or capacity groups with local disks | Pricing depends on selected instance families and local-disk options. | | Load balancers | A port is exposed publicly | Billed by provider-specific hourly, rule, capacity-unit, or data-processing dimensions. | | NAT / egress | Private services need outbound internet access | Created networks use managed NAT; BYO networks use customer-managed egress. | | Data transfer | Public ingress/egress or cross-zone traffic | Charged by cloud provider and region. | | Registry storage and pulls | Image storage and deployment pulls | Uses the configured artifact registry. | ## Provider Mapping [#provider-mapping] | Platform | Machines | Persistent Storage | Public Exposure | | -------------------- | ---------------- | ---------------------------------- | ---------------------------------------- | | AWS | EC2 | EBS | Load balancer and target groups | | GCP | Compute Engine | Persistent Disk | Google Cloud load balancing | | Azure | Virtual Machines | Managed Disks | Azure Load Balancer | | Kubernetes / On-Prem | Cluster nodes | PersistentVolumeClaims | Cluster ingress or service configuration | | Local | Local machine | Local Docker volumes / bind mounts | Local port forwarding | ## Example Cost Shape [#example-cost-shape] For a public two-replica API with a 100 GiB persistent disk: | Item | Quantity | | ------------------ | ----------------------------------------------------: | | Machines | At least two scheduled replicas plus cluster headroom | | Persistent storage | 100 GiB per stateful replica that owns a volume | | Load balancing | One public port | | Network | NAT and egress if replicas run on private subnets | | Registry | Image storage plus image pull transfer | ## Notes [#notes] * Autoscaling reduces idle replica count but does not remove the cost of minimum replicas. * Stateful replicas generally keep their disks after restarts and reschedules. * BYO networking shifts NAT, routing, and firewall costs to the customer-managed network. * GPU containers are priced by the selected GPU instance family and any attached disks. ## Sources [#sources] * [AWS EC2 Pricing](https://aws.amazon.com/ec2/pricing/) * [AWS EBS Pricing](https://aws.amazon.com/ebs/pricing/) * [AWS Elastic Load Balancing Pricing](https://aws.amazon.com/elasticloadbalancing/pricing/) * [GCP Compute Engine Pricing](https://cloud.google.com/compute/vm-instance-pricing) * [GCP Persistent Disk Pricing](https://cloud.google.com/compute/disks-image-pricing) * [GCP Cloud Load Balancing Pricing](https://cloud.google.com/vpc/network-pricing) * [Azure Virtual Machines Pricing](https://azure.microsoft.com/en-us/pricing/details/virtual-machines/) * [Azure Managed Disks Pricing](https://azure.microsoft.com/en-us/pricing/details/managed-disks/) * [Azure Load Balancer Pricing](https://azure.microsoft.com/en-us/pricing/details/load-balancer/) # API Reference (/docs/infrastructure/daemon/api) ## Constructor [#constructor] ```typescript new alien.Daemon(id: string) ``` | Parameter | Type | Required | Description | | --------- | -------- | -------: | ------------------------------------------------------------------------------------------------------ | | `id` | `string` | Yes | Daemon resource ID. Must contain only letters, numbers, hyphens, and underscores, up to 64 characters. | ## code [#code] Sets what the daemon runs. ```typescript .code({ type: "image", image: "ghcr.io/acme/connector:v1" }) ``` | Field | Type | Required | Description | | ----------- | --------------------- | -----------: | ------------------------------------------- | | `type` | `"image" \| "source"` | Yes | Use an existing image or build from source. | | `image` | `string` | For `image` | Container image reference. | | `src` | `string` | For `source` | Source directory. | | `toolchain` | `ToolchainConfig` | For `source` | Build toolchain. | For source code, `alien build` runs the selected toolchain and rewrites the resource to the resulting image before a controller sees it. The image command starts the app directly; there is no Worker runtime wrapper in front of a Daemon. ## environment [#environment] Sets environment variables. ```typescript .environment({ LOG_LEVEL: "info", ENDPOINT: "https://example.com", }) ``` | Parameter | Type | Required | Description | | --------- | ------------------------ | -------: | ------------------------- | | `vars` | `Record` | Yes | Environment variable map. | Calling `.environment()` replaces the current daemon environment map. ## link [#link] Links another resource to the daemon. ```typescript const vault = new alien.Vault("credentials").build() const daemon = new alien.Daemon("connector") .code({ type: "image", image: "ghcr.io/acme/connector:v1" }) .link(vault) .permissions("execution") .build() ``` Linked resources are passed to the daemon through the same environment variables used by Workers and Containers. ## permissions [#permissions] Assigns the daemon permission profile. ```typescript .permissions("execution") ``` | Parameter | Type | Required | Description | | --------- | -------- | -------: | ----------------------------------------------------------------- | | `profile` | `string` | Yes | Permission profile name from the stack permissions configuration. | ## publicEndpoint [#publicendpoint] Adds a named HTTP public endpoint. Daemons are private unless they declare one. ```typescript .publicEndpoint("api", 8080, "http") .publicEndpoint("webhooks", 8080, { protocol: "http", hostLabel: "webhooks", wildcardSubdomains: true, }) ``` | Parameter | Type | Required | Description | | --------- | ------------------ | -------: | ------------------------------------------------------------------------------------- | | `name` | `string` | Yes | Endpoint name. Lowercase DNS label. | | `port` | `number` | Yes | Port the daemon listens on. | | `options` | `string \| object` | No | Protocol string, or an object with `protocol`, `hostLabel`, and `wildcardSubdomains`. | `hostLabel` overrides the host label on the deployment domain (`"@"` serves the endpoint at the apex). `wildcardSubdomains` also routes `*..` to the endpoint; it cannot be combined with the apex host label. See [External URLs](/docs/external-urls). ## healthCheck [#healthcheck] Configures HTTP health checks for public daemon endpoints. ```typescript .healthCheck({ path: "/health", method: "GET", timeoutSeconds: 1, failureThreshold: 3, }) ``` | Field | Type | Required | Description | | ------------------ | -------- | -------: | ---------------------------------------------------------- | | `path` | `string` | No | HTTP endpoint path to check (e.g., `"/health"`). | | `method` | `string` | No | HTTP method to use. | | `port` | `number` | No | Port to check. Defaults to the endpoint port. | | `timeoutSeconds` | `number` | No | Request timeout in seconds (1–5). | | `failureThreshold` | `number` | No | Consecutive failures before marking the replica unhealthy. | ## commandsEnabled [#commandsenabled] Enables or disables the Commands protocol. ```typescript .commandsEnabled(true) ``` | Parameter | Type | Default | Description | | --------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enabled` | `boolean` | `false` | When enabled, Alien injects the command-receiver environment so the daemon's own pull receiver (`createCommandReceiver` from `@alienplatform/commands`) can lease and dispatch commands over outbound HTTPS. It does not start a runtime or sidecar. See [Remote Commands](/docs/commands#on-a-container-or-daemon). | ## cluster [#cluster] Selects the ComputeCluster that runs the daemon on AWS, GCP, and Azure. Local and Kubernetes ignore this field. ```typescript .cluster("runtime") ``` ## cpu and memory [#cpu-and-memory] Sets the resources requested by each daemon instance. Numeric CPU values set both the minimum and desired allocation; pass a `ResourceSpec` when they differ. Memory uses Kubernetes-style size strings. ```typescript .cpu(0.5) .memory("512Mi") // Or set different minimum and desired CPU allocations. .cpu({ min: "0.25", desired: "1" }) ``` ## pool [#pool] Selects the capacity group or machine pool used for placement. ```typescript .pool("general") ``` ## command [#command] Overrides the image's default command. ```typescript .command(["/app/agent", "--foreground"]) ``` ## stopGracePeriod [#stopgraceperiod] Sets the time allowed for a daemon to stop during an update, drain, or delete. Valid values are 1 through 86,400 seconds. When omitted, the runtime backend chooses its default. ```typescript .stopGracePeriod(30) ``` ## runtime [#runtime] Configures host-level options for trusted infrastructure daemons. Supported fields are `privileged`, `networkMode` (`"host"` or `"appnet"`), `pidNamespace` (`"host"` or `"private"`), `user`, and host `mounts`. ```typescript .runtime({ privileged: true, networkMode: "host", pidNamespace: "host", user: "0", mounts: [{ source: "/", target: "/host" }], }) ``` Use host access only for workloads such as node agents or bootstrap loaders that intentionally need it. ## readinessProbe [#readinessprobe] Shorthand for the public-endpoint health check that uses the default timeout and failure threshold. ```typescript .readinessProbe({ method: "GET", path: "/health" }) ``` ## build [#build] ```typescript const daemon = new alien.Daemon("connector") .code({ type: "image", image: "ghcr.io/acme/connector:v1" }) .permissions("execution") .build() ``` `build()` validates the daemon configuration and returns a `Resource` with type `"daemon"`. ## Outputs [#outputs] Daemon outputs are available in stack state after provisioning. | Field | Type | Description | | ----------------- | --------------------------------------------------- | ---------------------------------------------------------- | | `daemonName` | `string` | Runtime daemon name. | | `publicEndpoints` | `Record \| undefined` | Public endpoints keyed by endpoint name, when configured. | | `running` | `boolean` | Whether the daemon is running according to the controller. | ## Unsupported Surface [#unsupported-surface] Daemons do not expose builder methods for: * triggers * direct invocation * request timeout * replica count or autoscaling * persistent storage # Behavior & Limits (/docs/infrastructure/daemon/behavior) ## Guarantees [#guarantees] **Resident workload.** A daemon is started and kept running by the platform controller. On Local it runs as a supervised local runtime process. On Kubernetes it runs as a DaemonSet. On AWS, GCP, and Azure it runs one instance on every machine in the selected Alien Machines cluster. **No request-response API.** Daemon has no invocation API, timeout, or trigger model. Public endpoints are optional HTTP routing surfaces, not invocation semantics. **Command-capable when enabled.** If `commandsEnabled` is `true`, Alien injects the command-receiver environment. The daemon application must start its own pull receiver (`createCommandReceiver` or `alien_commands::Receiver`) to lease commands addressed to that Daemon over outbound HTTPS and execute registered handlers. No runtime, sidecar, or Operator starts the receiver for it. **Cloud controllers.** AWS, GCP, and Azure daemon resources run on the managed compute plane, can request host runtime settings, and can attach managed HTTP endpoints. ## Limits [#limits] | Limit | Value | Notes | | ------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Supported platforms | AWS, GCP, Azure, Local, Kubernetes | Controller behavior is platform-specific. | | Scheduling | One per eligible machine or node | Kubernetes uses a DaemonSet. On AWS, GCP, and Azure, Alien Machines runs one instance on each machine in the cluster. | | Public endpoints | HTTP only | Add named endpoints explicitly. | | Triggers | Not supported | Use Worker for queue, storage, schedule, or HTTP-triggered work. | | Direct invocation | Not supported | Use Worker for request-response calls. | | Commands | Disabled by default | Enable explicitly with `.commandsEnabled(true)`. | ## Platform Notes [#platform-notes] ### Local [#local] * The controller extracts the daemon container image, starts it through the local runtime manager, and checks health every 5 seconds while ready. * Linked resource bindings and standard Alien environment variables are injected into the process environment. * Delete stops and removes the local daemon process state. ### Kubernetes / On-Prem [#kubernetes--on-prem] * The controller creates a Kubernetes DaemonSet. * The pod restart policy is `Always`. * Linked resource bindings are injected as environment variables. Secret values are moved into Kubernetes Secrets where the binding extraction code supports it. * The build pipeline converts source-based Daemons to runnable images before the controller sees them. ### AWS, GCP, Azure [#aws-gcp-azure] * Daemons run on the managed compute plane, with one instance on every machine in the Alien Machines cluster. * A cloud daemon must specify a `.cluster(...)` so Alien knows which ComputeCluster should run it. * Public endpoints use provider load balancing and managed DNS when configured. * Omit public endpoints for private background processes. * Host runtime settings are passed through to the managed runtime for trusted loaders and node agents. * Managed clustered daemons receive generic topology hints in `ALIEN_DAEMON_PRIVATE_ADDRESS`, `ALIEN_DAEMON_PEER_PRIVATE_ADDRESSES`, and `ALIEN_DAEMON_NODE_COUNT`. Treat these as application-level hints; Alien owns the underlying private network implementation. ## Design Decisions [#design-decisions] **Daemon is process supervision, not a scalable service abstraction.** It has no triggers, scaling, or storage model. Use Container when you need replica controls, persistent storage, or multiple service ports. **Commands are opt-in.** A daemon without command handlers should leave `.commandsEnabled(false)` and should not start a receiver. Enabling commands injects configuration; it does not start polling on the application's behalf. # Overview (/docs/infrastructure/daemon) Daemons run resident processes on machines or nodes. Use Daemon for endpoint agents, host bootstrap loaders, node supervisors, local connectors, telemetry collectors, command handlers, and cluster-side services that should come up with the environment and stay running. Daemons can run anywhere Alien has a daemon controller. They are private by default and can optionally expose named HTTP public endpoints. Their image command starts the app directly; source builds are converted to runnable images before deployment, without a Worker runtime wrapper. ## Platform Mapping [#platform-mapping] | Platform | Backing Runtime | Status | | -------------------- | ---------------------------------------------------------- | --------- | | Local | Local process from a container image | Supported | | Kubernetes / On-Prem | Kubernetes DaemonSet | Supported | | AWS | One instance on every machine in an Alien Machines cluster | Supported | | GCP | One instance on every machine in an Alien Machines cluster | Supported | | Azure | One instance on every machine in an Alien Machines cluster | Supported | ## When to Use [#when-to-use] Use Daemon when the work is machine-oriented: a connector that maintains a long-lived session, a host loader that prepares the machine, a background command executor, a local/on-prem control loop, or a helper service that should restart if it exits. Use [Worker](/docs/infrastructure/worker) for request-response handlers. Use [Container](/docs/infrastructure/container) for cloud services with ports, stateful storage, GPUs, or scaling. ## Quick Start [#quick-start] ```typescript title="alien.ts" import * as alien from "@alienplatform/core" const connector = new alien.Daemon("connector") .code({ type: "image", image: "ghcr.io/acme/connector:2026-05-17" }) .commandsEnabled(true) .environment({ LOG_LEVEL: "info", }) .permissions("execution") .build() export default new alien.Stack("edge") .add(connector, "live") .platforms(["local", "kubernetes"]) .build() ``` ## Public Endpoints [#public-endpoints] Daemons are private unless they declare a named HTTP endpoint: ```typescript const gateway = new alien.Daemon("gateway") .code({ type: "image", image: "ghcr.io/acme/gateway:v1" }) .publicEndpoint("api", 8080, "http") .healthCheck({ path: "/health", method: "GET", timeoutSeconds: 1, failureThreshold: 3, }) .permissions("execution") .build() ``` ## Host Runtime [#host-runtime] Trusted daemon infrastructure can request host-level runtime options. Use this for loaders or node agents that intentionally need to inspect, prepare, or supervise the host: ```typescript const loader = new alien.Daemon("host-loader") .code({ type: "image", image: "ghcr.io/acme/host-loader:v1" }) .cluster("runtime") .runtime({ privileged: true, pidNamespace: "host", networkMode: "host", mounts: [{ source: "/", target: "/host" }], user: "0", }) .permissions("execution") .build() ``` ## Commands [#commands] Daemons can participate in the [Commands protocol](/docs/commands). Setting `.commandsEnabled(true)` tells Alien to inject the command-receiver environment (`ALIEN_COMMANDS_*`) into the process. The daemon runs your process directly, so it runs an explicit pull receiver — `createCommandReceiver()` from `@alienplatform/commands` — that leases commands addressed to it and dispatches them to registered handlers over outbound HTTPS. There is no runtime wrapper; the receiver is a library your app runs. See [Remote Commands](/docs/commands#on-a-container-or-daemon). ```typescript title="src/index.ts" import { createCommandReceiver } from "@alienplatform/commands" const receiver = createCommandReceiver() receiver.command("status", async () => ({ ready: true })) async function main() { await receiver.run() } void main() ``` ```typescript const executor = new alien.Daemon("executor") .code({ type: "image", image: "ghcr.io/acme/executor:v1" }) .commandsEnabled(true) .permissions("execution") .build() ``` ## Configuration [#configuration] | Method | Required | Description | | ------------------------------------- | ------------: | ----------------------------------------------------------------------------------------------------------------------------------------- | | `.code(code)` | Yes | Container image or source build configuration. Source builds become runnable images before deployment. | | `.environment(vars)` | No | Environment variables injected into the daemon process. | | `.link(resource)` | No | Gives the daemon binding access to another resource. | | `.publicEndpoint(name, port, "http")` | No | Adds a named HTTP public endpoint. | | `.healthCheck(config)` | No | Configures HTTP health checks for public daemon endpoints. | | `.cluster(clusterId)` | AWS/GCP/Azure | Selects the ComputeCluster that should run the daemon. | | `.cpu(value)` | No | CPU requested for each daemon instance. | | `.memory(size)` | No | Memory requested for each daemon instance. | | `.runtime(config)` | No | Host runtime settings for trusted daemon infrastructure. | | `.permissions(profile)` | Yes | Permission profile used for linked resources and cloud access. | | `.commandsEnabled(boolean)` | No | Injects the command-receiver environment so the daemon's own [command receiver](/docs/commands) can lease its commands. Default: `false`. | Daemons do not have triggers, direct invocation, request timeouts, replica settings, or autoscaling. See [API Reference](/docs/infrastructure/daemon/api) for every builder method and [Behavior & Limits](/docs/infrastructure/daemon/behavior) for supported platforms and lifecycle behavior. # Pricing (/docs/infrastructure/daemon/pricing) Daemon has no separate Alien infrastructure charge. ## Local [#local] Local daemons run on the developer or operator machine. There is no cloud provider bill from Alien. ## Kubernetes / On-Prem [#kubernetes--on-prem] Kubernetes daemons consume the cluster resources requested by their image and runtime environment. The cost is whatever the cluster operator pays for nodes, storage, networking, and registry pulls. | Component | Applies When | Notes | | -------------------------- | ------------------------------------- | -------------------------------------------------------- | | Cluster nodes | Always | The daemon runs as a pod on existing cluster capacity. | | Registry storage and pulls | Image-based daemons | Charged by the configured registry. | | Logs and telemetry | If enabled by the environment | Cost depends on the cluster observability stack. | | Kubernetes Secrets | Linked resources with secret material | Usually included in cluster control-plane/storage costs. | ## AWS, GCP, Azure [#aws-gcp-azure] Cloud daemons run once on every eligible machine in the selected Alien Machines cluster. They do not create a separate VM per Daemon, but they consume CPU and memory from that cluster and can add registry, logging, networking, and load-balancer costs. | Component | Applies When | Notes | | -------------------------- | -------------------------- | ----------------------------------------------------------------------- | | Cluster machines | Always | Charged as part of the selected Machines cluster's VM capacity. | | Registry storage and pulls | Always | Charged by ECR, Artifact Registry, or ACR. | | Logs and telemetry | If enabled | Charged by the configured observability backend and cloud egress rules. | | Load balancer and DNS | Public endpoint configured | Provider load-balancing and DNS charges apply. | ## Cost Control [#cost-control] * Keep daemon images small: every eligible machine pulls each new version. * Use Container instead of Daemon when you need explicit CPU, memory, scaling, ports, or persistent storage. * Use Worker instead of Daemon when work can be handled as bounded request-response execution. # API Reference (/docs/infrastructure/email/api) The Email binding is **capability-only**: it identifies the provisioned SES infrastructure, and your application talks to SES directly with the AWS SDK. There is no high-level Alien send API — workloads use `@aws-sdk/client-sesv2` with the permissions granted through `email/*` permission sets. ## The Binding [#the-binding] The binding is injected as JSON in the `ALIEN__BINDING` environment variable (the resource id uppercased, hyphens replaced with underscores): ```typescript const binding = JSON.parse(process.env.ALIEN_MAILER_BINDING!) ``` | Field | Type | Description | | ------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `service` | `"ses"` | The backing service. | | `region` | `string` | The AWS region of the SES infrastructure. | | `configurationSet` | `string` | The provisioned configuration set name. Pass it as `ConfigurationSetName` on every send so events flow to the linked queue. | | `eventTopicArn` | `string` (optional) | The SNS topic ARN for sending events. Present only when `.events()` is configured. | The binding deliberately carries **no domain list**: identities are created and removed at runtime, so a list frozen at deploy time would be stale by design. Applications discover the current identities via `ses:ListEmailIdentities` (granted by `email/manage-identities`). ## Permission Sets [#permission-sets] Grant these through [permission profiles](/docs/permissions): | Set | Grants | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email/send` | `ses:SendEmail`, `ses:SendRawEmail` — sending through any identity and the stack-scoped configuration set. | | `email/manage-identities` | The runtime domain-management capability: `ses:CreateEmailIdentity`, `ses:GetEmailIdentity`, `ses:DeleteEmailIdentity`, `ses:ListEmailIdentities`, and the `ses:PutEmailIdentity*` attribute actions (MailFrom, DKIM, feedback, configuration set). | | `email/management` | Read-only: identity/DKIM verification status, configuration set settings, receipt rules. | | `email/provision` | Deploy-time provisioning of the SES infrastructure. Used during setup; not a runtime grant. | ```typescript .permissions({ profiles: { execution: { mailer: ["email/send", "email/manage-identities"], }, }, }) ``` Because SES identities are named after customer mail domains — which are unknowable at deploy time — the identity permissions apply to all identities in the account and region, not just stack-prefixed ones. The configuration-set permissions stay stack-scoped. ## Sending [#sending] Pass the binding's configuration set on every send: ```typescript import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2" const binding = JSON.parse(process.env.ALIEN_MAILER_BINDING!) const ses = new SESv2Client({ region: binding.region }) await ses.send(new SendEmailCommand({ ConfigurationSetName: binding.configurationSet, FromEmailAddress: "hello@mail.example.com", Destination: { ToAddresses: ["someone@example.org"] }, Content: { Simple: { Subject: { Data: "Hello" }, Body: { Text: { Data: "Sent through Alien Email." } }, }, }, })) ``` The workload's credentials come from its permission profile — no keys to configure. ## Managing Identities at Runtime [#managing-identities-at-runtime] With `email/manage-identities`, the application owns the domain lifecycle: create an identity when a customer adds a domain, hand them the DKIM records, poll verification, delete the identity when the domain is removed — all without an infrastructure change. ```typescript import { SESv2Client, CreateEmailIdentityCommand, GetEmailIdentityCommand, DeleteEmailIdentityCommand, } from "@aws-sdk/client-sesv2" const ses = new SESv2Client({ region: binding.region }) // Customer adds a domain const created = await ses.send(new CreateEmailIdentityCommand({ EmailIdentity: "mail.customer.com", ConfigurationSetName: binding.configurationSet, })) // created.DkimAttributes.Tokens — three DKIM tokens; the customer creates // `._domainkey.mail.customer.com CNAME .dkim.amazonses.com` // Poll verification const identity = await ses.send(new GetEmailIdentityCommand({ EmailIdentity: "mail.customer.com", })) // identity.VerifiedForSendingStatus // Customer removes the domain await ses.send(new DeleteEmailIdentityCommand({ EmailIdentity: "mail.customer.com", })) ``` Identities created this way are **application data**: they are not tracked by the deployment and survive stack deletion. Their lifecycle — including deletion — belongs to the application (see [Behavior](/docs/infrastructure/email/behavior)). ## Inbound Mail [#inbound-mail] When `.inbound(storage)` is configured, raw incoming mail (full MIME messages) is written as objects into the linked Storage bucket. The receipt rule is a catch-all — mail for any identity the account receives mail for lands in the bucket, including identities verified at runtime. Read the objects with the [Storage binding](/docs/infrastructure/storage) and parse them with a MIME library. Setup activates the provisioned receipt rule set automatically — SES allows one active rule set per account, so installing inbound delivery makes this stack's rule set the active one (see [Behavior](/docs/infrastructure/email/behavior)). ## Sending Events [#sending-events] When `.events(queue)` is configured, SES publishes send, delivery, bounce, complaint, delivery-delay, and reject events for mail sent through the configuration set. Events flow through an SNS topic to the linked queue with raw message delivery, so each queue message body is the SES event JSON. Consume them with the [Queue binding](/docs/infrastructure/queue): ```typescript import { onQueueMessage } from "@alienplatform/sdk" onQueueMessage("mail-events", async (message) => { const event = message.payload // event.eventType: "Send" | "Delivery" | "Bounce" | "Complaint" | ... }) ``` # Behavior & Limits (/docs/infrastructure/email/behavior) Email is currently available only on AWS, backed by SES. ## Guarantees [#guarantees] **Your domains, your reputation.** Mail is sent through SES in the customer's AWS account, from domains they own. Sending reputation, quotas, and deliverability belong to that account — there is no shared sending pool. **Easy DKIM on every domain.** Every identity — seed or runtime-created — is provisioned with Easy DKIM. The resource outputs carry three DKIM CNAME records per seed domain; SES verifies the domain asynchronously once the records exist in DNS. Verification status is not part of the outputs because it can't be known at provisioning time — read it from `ses:GetEmailIdentity`. **Runtime identities survive the stack.** Identities created through the `email/manage-identities` grant are application data: they are not tracked by the deployment and are **not removed when the stack is deleted**. Their lifecycle — including deletion — belongs to the application. Seed domains, by contrast, are CloudFormation-managed: removing one from `domains` deletes its identity and DKIM verification state. **Catch-all inbound.** The provisioned receipt rule has no recipient filter, so mail for any identity the account receives mail for — including identities verified at runtime — lands in the linked Storage bucket without an infrastructure change. Each message is written as a raw MIME object, with spam and virus scanning enabled. **Complete event coverage.** With `.events()`, SES publishes send, delivery, bounce, complaint, delivery-delay, and reject events for all mail sent through the configuration set, delivered to the linked queue via an SNS topic with raw message delivery. ## Limits [#limits] | Limit | Value | Notes | | ------------------------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `id` | `[A-Za-z0-9-_]`, max 64 chars | Immutable after create. | | Seed domains | unique | Listing the same domain twice is rejected. | | Events queue | one Email resource per queue | SQS supports a single effective policy per queue; two Email resources targeting the same queue would overwrite each other's policy. | | Inbound regions | SES receiving regions only | SES email receiving is available in a subset of AWS regions; deploying `.inbound()` elsewhere fails at the CloudFormation layer. | | Active receipt rule sets | one per AWS account | See below. | **Changing the Email resource.** Email is frozen, so these changes require customer setup authority. `domains` is append-friendly: adding a domain provisions a new identity, removing one deletes its identity (and its DKIM verification state), and the list may be empty. `.inbound()` and `.events()` can be added, removed, or repointed; removing them tears down the corresponding receipt rule set or event destination wiring. The `id` is immutable. ## Inbound Activation [#inbound-activation] SES allows only **one active receipt rule set per AWS account**, and CloudFormation has no resource that activates one. Setup therefore provisions a small activator function that calls `ses:SetActiveReceiptRuleSet` automatically, making the provisioned rule set the account's active one — installing an Email resource with inbound delivery replaces whatever rule set was active before. The rule set name is surfaced in the resource outputs (`ruleSetName`), and the resource heartbeat verifies the expected rule set is still the active one, flagging account-level SES routing changes. ## What Gets Provisioned [#what-gets-provisioned] * One shared `AWS::SES::ConfigurationSet`, named after the stack and resource id. * Per seed domain, an `AWS::SES::EmailIdentity` with Easy DKIM enabled and the configuration set associated. * With `.events()`: a configuration set event destination publishing to an SNS topic, subscribed to the linked SQS queue (raw delivery), plus the queue policy that lets the topic send. * With `.inbound()`: a receipt rule set and a catch-all receipt rule whose S3 action writes into the linked bucket, plus the bucket policy statement that allows SES to write (scoped to the account). * IAM policies attaching any granted `email/*` permission sets to the owning workload roles. ## Platform Notes [#platform-notes] ### AWS — SES [#aws--ses] The only supported platform. New AWS accounts start in the SES sandbox (verified recipients only, low quotas); production access is an account-level request to AWS, outside Alien's scope. Sending quotas and reputation are per-account. ### Other Platforms [#other-platforms] Email is not available on GCP, Azure, Kubernetes / on-prem, or Local. ## Design Decisions [#design-decisions] **Infrastructure, not domain lifecycle.** The resource owns the configuration set and the event/inbound topology. The domain lifecycle belongs to the application: email products typically create identities via API when a customer adds a domain, hand over the DNS records, poll verification, and delete the identity when the domain is removed. Seed `domains` exist for day-0 bootstrap and static-domain products. **No domain list in the binding.** Identities come and go at runtime, so a deploy-frozen list would be stale by design. Applications discover the current identities via `ses:ListEmailIdentities`. **No high-level send API.** Workloads send with the AWS SDK directly using the `email/send` grant. Every backend is SES, so wrapping the SDK would add a layer without adding portability. **Frozen-only.** The Email resource is always [frozen](/docs/frozen-and-live): setup owns the durable routing state end to end. # Overview (/docs/infrastructure/email) `alien.Email` is email infrastructure for sending and receiving mail on your own domains. Declare it in your `alien.ts` and Alien provisions an SES configuration set, DKIM-verified domain identities, and optional inbound and event wiring — all in the customer's AWS account, so mail is sent with their domains and their sending reputation. ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | --------------- | -------------- | | AWS | Amazon SES | Alien | | GCP | — | Not available | | Azure | — | Not available | | Kubernetes / On-Prem | — | Not available | | Local | — | Not available | Email is currently available only on AWS. ## When to Use [#when-to-use] Use Email when your product sends or receives mail on customer-owned domains — transactional mail, notification delivery, or email-first products where each customer brings their own domain. The resource owns the email *infrastructure* — the configuration set, event topology, and inbound topology — not the domain lifecycle. Products that create and verify domains dynamically (a customer adds a domain, your app hands them DNS records and polls verification) manage identities at runtime through the `email/manage-identities` grant instead of listing domains in the stack. ## Stack Definition [#stack-definition] ```typescript const mailbox = new alien.Storage("mailbox").build() const mailEvents = new alien.Queue("mail-events").build() // Domain-dynamic products: no seed domains; identities are // created at runtime through the email/manage-identities grant. const email = new alien.Email("mailer") .inbound(mailbox) .events(mailEvents) .build() // Static-domain products can seed identities at deploy time: const seeded = new alien.Email("mailer") .domains(["mail.example.com"]) .build() ``` | Method | Type | Default | Description | | ------------------- | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` (constructor) | `string` | required | Resource identifier. `[A-Za-z0-9-_]`, max 64 characters. | | `.domains(domains)` | `string[]` | `[]` | Seed mail domains provisioned at deploy time (one SES identity each, Easy DKIM enabled). Optional — omit for products that manage domains at runtime. | | `.domain(domain)` | `string` | — | Adds a single seed mail domain. | | `.inbound(storage)` | `Resource` | none | Raw incoming mail is written to the linked [Storage](/docs/infrastructure/storage) bucket. | | `.events(queue)` | `Resource` | none | Send / delivery / bounce / complaint / delivery-delay / reject events are delivered to the linked [Queue](/docs/infrastructure/queue). | `domains` may be empty — a configuration-set-only resource is valid. Adding a domain later provisions a new identity; removing one deletes its identity and DKIM verification state. ## Domain Verification [#domain-verification] Each domain gets an SES identity with Easy DKIM. The resource outputs carry three DKIM CNAME records per seed domain — create them in the domain's DNS, and SES verifies the domain asynchronously once they exist. For identities created at runtime, your application gets the DKIM tokens from the SES `CreateEmailIdentity` / `GetEmailIdentity` responses and hands them to the customer. ## Sending [#sending] There is no high-level Alien send API. Workloads with the `email/send` grant send directly with the AWS SDK, passing the configuration set from the binding: ```typescript import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2" const binding = JSON.parse(process.env.ALIEN_MAILER_BINDING!) // { service: "ses", region, configurationSet, eventTopicArn? } const ses = new SESv2Client({ region: binding.region }) await ses.send(new SendEmailCommand({ ConfigurationSetName: binding.configurationSet, FromEmailAddress: "hello@mail.example.com", Destination: { ToAddresses: ["someone@example.org"] }, Content: { Simple: { Subject: { Data: "Hello" }, Body: { Text: { Data: "Sent through Alien Email." } }, }, }, })) ``` See the [API Reference](/docs/infrastructure/email/api) for the binding fields, permission sets, and runtime identity management. # Pricing (/docs/infrastructure/email/pricing) Alien provisions Email in your customer's cloud. The customer pays AWS for SES usage, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Prices shown for US East regions. Last verified: July 2026.* ## SES Pricing [#ses-pricing] SES bills per message, not per instance — an idle Email resource costs nothing. | Component | Price | | -------------------- | ------------------------------------ | | Outbound email | $0.10 per 1,000 emails | | Outbound attachments | $0.12 per GB | | Inbound email | $0.10 per 1,000 emails | | Inbound mail chunks | $0.09 per 1,000 chunks (256 KB each) | Sending 100,000 emails a month costs about $10, before attachment data. ## Linked Resources [#linked-resources] The linked resources bill separately at their own rates: inbound mail objects land in [Storage](/docs/infrastructure/storage/pricing) (S3), and sending events flow through an SNS topic to a [Queue](/docs/infrastructure/queue/pricing) (SQS). SNS-to-SQS delivery is free; both are negligible at typical mail volumes. ## Free Tier [#free-tier] SES includes 3,000 free message charges per month for the first 12 months of a new AWS account (shared across outbound and inbound). ## Sources [#sources] * [AWS SES Pricing](https://aws.amazon.com/ses/pricing/) # API Reference (/docs/infrastructure/kv/api) Get a handle with `kv(name)` from `@alienplatform/sdk` (TypeScript) or `alien_bindings::Bindings::from_env()?.kv(name).await?` (Rust). Every read returns the value together with an opaque, per-key **version**. Pass that version back into a later `set` or `delete` to make the write conditional — see [Behavior & Limits](/docs/infrastructure/kv/behavior#versions--conditional-writes). ## get [#get] Retrieves an entry by key. Returns `null` / `None` if the key does not exist or has expired. ```typescript const entry = await kv.get(key) // KvEntry | null const text = await kv.getText(key) // KvEntry | null const data = await kv.getJson(key) // KvEntry | null // entry: { key: string, value: T, version: string } ``` ```rust let entry: Option = kv.get(key).await?; // KvEntry { key: String, value: Vec, version: String } ``` | Parameter | Type | Required | Description | | --------- | -------- | -------- | --------------------------------------------- | | `key` | `string` | Yes | Max 512 bytes. Charset: `a-z A-Z 0-9 - _ : .` | **Returns:** a `KvEntry` — the key, the value (`getText` decodes UTF-8, `getJson` parses JSON), and an opaque `version` for a later conditional write — or `null` / `None` if not found or expired. *** ## set / setJson [#set--setjson] Stores a value. Unconditional by default; pass `ifVersion` for an atomic conditional write. ```typescript const applied: boolean = await kv.set(key, value, options?) // string value const applied: boolean = await kv.setJson(key, value, options?) // any value, JSON-serialized // options: { ttl?: number, ifVersion?: string | null } await kv.set(key, value) // unconditional await kv.set(key, value, { ifVersion: null }) // create only if absent await kv.set(key, value, { ifVersion: entry.version }) // compare-and-set ``` ```rust let applied: bool = kv.put(key, value_bytes, Some(PutOptions { ttl: Some(Duration::from_secs(3600)), condition: PutCondition::Version(entry.version), // or Absent, or None })).await?; ``` | Parameter | Type | Required | Description | | ------------------- | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | `string` | Yes | Max 512 bytes. | | `value` | `string` (`set`) / any (`setJson`) | Yes | Max 24 KiB. `setJson` values are JSON-serialized. | | `options.ttl` | `number` | No | Time-to-live in seconds. | | `options.ifVersion` | `string \| null` | No | `null`: create only when the key is absent (or expired). A version string: replace only when the key still has that version. Omit for an unconditional write. | **Returns:** `true` when the write was applied. A conditional write resolves `false` when its precondition does not match — no exception is thrown. Passing a version that belongs to a different key throws an invalid-input error. *** ## delete [#delete] Deletes a key. Deleting a non-existent key is a no-op. Pass `ifVersion` for an atomic compare-and-delete. ```typescript const applied: boolean = await kv.delete(key) const applied: boolean = await kv.delete(key, { ifVersion: entry.version }) ``` ```rust let applied: bool = kv.delete(key, None).await?; let applied: bool = kv.delete(key, Some(&entry.version)).await?; ``` | Parameter | Type | Required | Description | | ------------------- | -------- | -------- | ------------------------------------------------ | | `key` | `string` | Yes | Max 512 bytes. | | `options.ifVersion` | `string` | No | Delete only when the key still has this version. | **Returns:** `true` when the delete was applied. An unconditional delete resolves `true` even when the key was already absent; a conditional delete resolves `false` when the key is absent, expired, or has changed. *** ## exists [#exists] Checks if a key exists and has not expired. ```typescript const found: boolean = await kv.exists(key) ``` ```rust let found: bool = kv.exists(key).await?; ``` *** ## scan [#scan] Scans keys by prefix, one page at a time. Results are unordered and may contain duplicates — see [Behavior & Limits](/docs/infrastructure/kv/behavior#scan-semantics). ```typescript const page = await kv.scan(prefix, limit?, cursor?) // page.items: Array<{ key: string, value: Buffer, version: string }> // page.nextCursor: string | undefined // Follow the cursor across pages let cursor: string | undefined do { const page = await kv.scan("user:", undefined, cursor) for (const { key, value } of page.items) { /* ... */ } cursor = page.nextCursor } while (cursor) ``` ```rust let result: ScanResult = kv.scan_prefix(prefix, Some(100), None).await?; // result.items: Vec // result.next_cursor: Option ``` Values come back alongside their keys and versions — a scan needs no follow-up `get`, and each entry's version is usable for a conditional write. Cursors are opaque and may expire; do not persist them across sessions. *** ## Types [#types] ```typescript interface KvEntry { key: string value: T version: string // Opaque, key-bound version for conditional writes } interface KvSetOptions { ttl?: number // Time-to-live in seconds ifVersion?: string | null // null = create if absent; string = compare-and-set } interface KvDeleteOptions { ifVersion?: string // Compare-and-delete } interface KvScanResult { items: Array> nextCursor?: string } ``` ```rust pub struct KvEntry { pub key: String, pub value: Vec, pub version: String, // Opaque, key-bound version for conditional writes } pub enum PutCondition { None, // Unconditional (default) Absent, // Create only when absent or expired Version(String), // Compare-and-set } pub struct PutOptions { pub ttl: Option, pub condition: PutCondition, } pub struct ScanResult { pub items: Vec, pub next_cursor: Option, } ``` # Behavior & Limits (/docs/infrastructure/kv/behavior) ## Guarantees [#guarantees] On cloud platforms (AWS, GCP, Azure), Alien provisions and manages the KV backing service. These guarantees apply: **Strong Single-Key Consistency.** A `get()` after a successful `set()` on the same key returns the new value immediately. Firestore is always strongly consistent, DynamoDB uses strongly consistent reads, and Table Storage is strongly consistent within a region. **Atomic Conditional Writes.** `set()` with `ifVersion` and `delete()` with `ifVersion` are atomic across all platforms — see [Versions & Conditional Writes](#versions--conditional-writes) below. If two concurrent callers race on the same key, exactly one wins and the other resolves `false`. Safe for distributed locks, idempotency checks, and read-modify-write loops. **Durability.** Data is replicated across multiple availability zones by the cloud provider. **TTL is a Soft Hint.** When you set a TTL, the key becomes invisible on read after expiry — `get()` returns `null` and `exists()` returns `false`. Physical deletion of the underlying data is eventual and varies by platform (see below). Do not rely on storage being reclaimed at a specific time. **Delete Idempotency.** Deleting a non-existent key succeeds silently (a *conditional* delete resolves `false` when the key is absent, expired, or changed). ## Versions & Conditional Writes [#versions--conditional-writes] Every read (`get`, `getText`, `getJson`, `scan`) returns an opaque, key-bound version alongside the value. It changes on every applied write. * **Create if absent.** `set(key, value, { ifVersion: null })` creates the key only when it is absent or logically expired. Exactly one of several concurrent creators succeeds. * **Compare-and-set.** `set(key, value, { ifVersion: entry.version })` replaces the value only when the key still has the version you read. If another writer got there first, the write resolves `false` — re-read and retry. * **Compare-and-delete.** `delete(key, { ifVersion: entry.version })` removes the key only when it is unchanged since your read. A failed precondition is not an error: the call resolves `false` and nothing is written. Errors are reserved for real problems — passing a version that belongs to a *different* key throws an invalid-input error, since versions are bound to the key they were read from. Versions are opaque strings. Do not parse, compare, or order them; the only valid use is passing one back as `ifVersion`. A version read from an entry that has since expired never matches — conditional writes treat expired keys as absent. Under the hood, Alien maps conditions onto each backend's native atomic primitive: DynamoDB condition expressions, Firestore update-time preconditions, Azure Table Storage entity tags (ETags), and a conditional update in the local database. There is no client-side locking. Rows written by SDK versions that predate versioned KV (before v3.3.10) lack version metadata and are rejected on read rather than migrated. ## Limits [#limits] These are enforced by Alien on all platforms: | Limit | Value | | -------------- | --------------------- | | Max key size | 512 bytes | | Key charset | `a-z A-Z 0-9 - _ : .` | | Max value size | 24 KiB (24,576 bytes) | ## Scan Semantics [#scan-semantics] Scan operations have intentionally weak guarantees to remain portable: * **Unordered.** Results may arrive in any order. * **May contain duplicates.** Your application must deduplicate. * **Cursors are ephemeral.** Do not persist or share cursors. * **Not a snapshot.** Concurrent writes may or may not appear in results. ## Platform Details [#platform-details] ### AWS (DynamoDB) [#aws-dynamodb] * On-demand billing (pay-per-request). No capacity planning needed. * 16 hash buckets for load distribution (transparent to your code). * TTL: physical deletion within \~48 hours of expiry. * Per-partition throughput: 3,000 RCU + 1,000 WCU/second. ### GCP (Firestore) [#gcp-firestore] * Always strongly consistent. Only the default database is used. * TTL: physical deletion within \~24 hours of expiry. * Ramp-up required: starts at \~500 writes/sec, increases 50% every 5 minutes. ### Azure (Table Storage) [#azure-table-storage] * 16 partition buckets for load distribution. * **No native TTL.** Alien filters expired items on read, but they are never physically deleted. * Per-partition throughput: 2,000 entities/second. ### Kubernetes / On-Prem [#kubernetes--on-prem] KV is **not provisioned by Alien** on Kubernetes. The cluster operator provides the backing service (Redis, DynamoDB, etc.) and configures it via Helm values. The guarantees above (consistency, durability, TTL behavior) depend entirely on the backing service. Alien enforces the limits (key size, value size, charset) regardless of platform. ### Local [#local] * Backed by an embedded SQLite database (`localkv.sqlite` in the data directory). Data persists across restarts and is safe across multiple processes (WAL mode). * TTL: exact lazy deletion — expired items removed when accessed. * Scan returns sorted results. This is a local implementation detail — do not rely on ordering in production code. * Local stores written before v3.3.10 use an older on-disk format and are rejected rather than migrated — delete the local data directory to start fresh. ## Design Decisions [#design-decisions] **512-byte key limit.** DynamoDB supports 2,048-byte keys, but Azure Table Storage is more constrained. Alien enforces 512 bytes for portability. **24 KiB value limit.** DynamoDB allows 400 KB, Firestore 1 MiB. Alien enforces 24 KiB to encourage small, fast lookups and [Storage](/docs/infrastructure/storage) for larger objects. **Restricted key charset.** Azure Table Storage disallows many special characters. Alien restricts the charset globally so keys are valid on every platform. # Overview (/docs/infrastructure/kv) KV provides a minimal, portable key-value store for fast lookups. Store and retrieve opaque byte values by key, with optional time-to-live (TTL) for automatic expiration and atomic conditional writes for safe concurrent updates. Designed for caching, session state, feature flags, and distributed locks. ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | -------------------------------- | ---------------- | | AWS | Amazon DynamoDB (on-demand) | Alien | | GCP | Google Cloud Firestore | Alien | | Azure | Azure Table Storage | Alien | | Kubernetes / On-Prem | External (Redis, DynamoDB, etc.) | Cluster operator | | Local | SQLite (embedded database) | Alien | On Kubernetes / on-prem, KV is not provisioned by Alien. The cluster operator provides the backing service and configures it via Helm values. ## When to Use [#when-to-use] Use KV for fast key-based lookups — caching, session state, feature flags, distributed locks, idempotency tracking. Values are opaque bytes up to 24 KiB, with optional TTL. Don't use KV for complex queries, relationships, or large documents. For full-text search, joins, or items larger than 24 KiB, use a dedicated database. ## Stack Definition [#stack-definition] Declare a KV resource in your `alien.ts`: ```typescript const cache = new alien.Kv("cache").build() ``` | Parameter | Type | Description | | --------- | -------- | -------------------------------------------------------- | | `id` | `string` | Resource identifier. `[A-Za-z0-9-_]`, max 64 characters. | KV has no additional configuration options. The backing service (DynamoDB, Firestore, Table Storage) is determined by the deployment platform. ## Quick Start [#quick-start] ```typescript import { kv } from "@alienplatform/sdk" const cache = kv("cache") await cache.setJson("user:123", { name: "Alice", plan: "pro" }) const entry = await cache.getJson("user:123") // entry.value → { name: "Alice", plan: "pro" }, entry.version → opaque version ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let cache = bindings.kv("cache").await?; cache.put("user:123", value.into(), None).await?; let entry = cache.get("user:123").await?; // Option — value + version ``` ## Core Operations [#core-operations] ### Set a Value [#set-a-value] ```typescript await cache.setJson("user:123", { name: "Alice" }) // JSON (auto-serialized) await cache.set("greeting", "hello") // String await cache.set("session:abc", data, { ttl: 3600 }) // Expires in 1 hour (TTL in seconds) // Atomic create — only if key doesn't exist const created = await cache.setJson("lock:res", { owner: "w1" }, { ifVersion: null }) // true if created, false if the key already existed ``` ### Get a Value [#get-a-value] ```typescript const raw = await cache.get("user:123") // KvEntry | null const text = await cache.getText("greeting") // KvEntry | null const user = await cache.getJson("user:123") // KvEntry | null ``` Returns `null` if the key does not exist or has expired. Each entry carries the value plus an opaque `version` for conditional writes. ### Conditional Writes [#conditional-writes] Every read returns a version. Pass it back to write only if nothing changed in between: ```typescript const entry = await cache.getJson("counter") if (entry) { const updated = await cache.setJson("counter", { n: entry.value.n + 1 }, { ifVersion: entry.version, // compare-and-set }) // updated === false → another writer won the race; re-read and retry } ``` See [Behavior & Limits](/docs/infrastructure/kv/behavior#versions--conditional-writes) for the full semantics. ### Delete, Exists, Scan [#delete-exists-scan] ```typescript await cache.delete("user:123") if (await cache.exists("user:123")) { /* ... */ } // scan() resolves to a page of items plus a cursor const page = await cache.scan("user:") for (const { key, value } of page.items) { console.log(key) // "user:123", "user:456", ... } ``` ## Patterns [#patterns] ### Distributed Lock [#distributed-lock] ```typescript const acquired = await cache.setJson("lock:report-gen", { owner: workerId }, { ifVersion: null, // create only if absent (or expired) ttl: 30, }) if (acquired) { try { await generateReport() } finally { // Release only our own lock — a compare-and-delete won't remove // a lock that expired and was taken over by another worker. const lock = await cache.getJson<{ owner: string }>("lock:report-gen") if (lock?.value.owner === workerId) { await cache.delete("lock:report-gen", { ifVersion: lock.version }) } } } ``` ### Cache with TTL [#cache-with-ttl] ```typescript async function getUser(userId: string) { const cached = await cache.getJson(`user:${userId}`) if (cached) return cached.value const user = await fetchFromDatabase(userId) await cache.setJson(`user:${userId}`, user, { ttl: 5 * 60 }) return user } ``` # Pricing (/docs/infrastructure/kv/pricing) Alien provisions KV in your customer's cloud. The customer pays the cloud provider for the underlying database, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Prices shown for US East regions. Last verified: April 2026.* ## Request Pricing [#request-pricing] | Provider | Service | Reads | Writes | | -------- | -------------------- | -------------------------------- | -------------------------------- | | AWS | DynamoDB (on-demand) | $0.25 per million RRU | $1.25 per million WRU | | GCP | Firestore | $0.06 per 100K reads ($0.60/M) | $0.18 per 100K writes ($1.80/M) | | Azure | Table Storage | $0.00036 per 10K txns ($0.036/M) | $0.00036 per 10K txns ($0.036/M) | Azure Table Storage is significantly cheaper per transaction but has the most limited query capabilities. ## Storage Pricing [#storage-pricing] | Provider | Price per GB/month | | ------------------- | ------------------ | | AWS DynamoDB | $0.25 | | GCP Firestore | $0.18 | | Azure Table Storage | $0.045 | ## Example: 10M Reads + 1M Writes + 1 GB Storage/month [#example-10m-reads--1m-writes--1-gb-storagemonth] | Provider | Reads | Writes | Storage | **Total** | | ------------------- | ----- | ------ | ------- | --------- | | AWS DynamoDB | $2.50 | $1.25 | $0.25 | **$4.00** | | GCP Firestore | $6.00 | $1.80 | $0.18 | **$7.98** | | Azure Table Storage | $0.36 | $0.036 | $0.045 | **$0.44** | ## Free Tiers [#free-tiers] * **AWS DynamoDB**: 25 GB storage, 25 WCU + 25 RCU provisioned (always free). * **GCP Firestore**: 1 GiB storage, 50K reads, 20K writes, 20K deletes/day (always free). * **Azure Table Storage**: 5 GB with shared throughput (12 months). ## Platform Notes [#platform-notes] * **DynamoDB on-demand** pricing is used because Alien provisions tables with `PAY_PER_REQUEST` billing. No capacity planning needed but per-request cost is higher than provisioned mode. * **Firestore** charges separately for reads, writes, and deletes. TTL-based deletions are free. * **Azure Table Storage** has flat per-transaction pricing regardless of operation type. ## Sources [#sources] * [AWS DynamoDB Pricing](https://aws.amazon.com/dynamodb/pricing/) * [GCP Firestore Pricing](https://cloud.google.com/firestore/pricing) * [Azure Table Storage Pricing](https://azure.microsoft.com/en-us/pricing/details/storage/tables/) # API Reference (/docs/infrastructure/network/api) Network is configured through `StackSettings.network`. The generated `Network` resource appears in stack state, but application code does not create it directly. ## StackSettings.network [#stacksettingsnetwork] ```typescript type StackSettings = { network?: NetworkSettings | null } ``` ## use-default [#use-default] Uses the provider default network when available. ```json { "network": { "type": "use-default" } } ``` | Field | Type | Required | Description | | ------ | --------------- | -------: | ---------------------- | | `type` | `"use-default"` | Yes | Use provider defaults. | ## create [#create] Creates an isolated network for the deployment. ```json { "network": { "type": "create", "cidr": "100.88.0.0/16", "availability_zones": 2 } } ``` | Field | Type | Required | Description | | -------------------- | ---------------- | -------: | ----------------------------------------------------------------------------- | | `type` | `"create"` | Yes | Create a new VPC/VNet. | | `cidr` | `string \| null` | No | VPC/VNet CIDR. If omitted, Alien generates a non-overlapping `/16` candidate. | | `availability_zones` | `number` | No | Number of zones. Default is `2`. | ## byo-vpc-aws [#byo-vpc-aws] References an existing AWS VPC. ```json { "network": { "type": "byo-vpc-aws", "vpc_id": "vpc-0123456789abcdef0", "public_subnet_ids": ["subnet-public-a", "subnet-public-b"], "private_subnet_ids": ["subnet-private-a", "subnet-private-b"], "security_group_ids": ["sg-0123456789abcdef0"] } } ``` | Field | Type | Required | Description | | -------------------- | --------------- | -------: | ------------------------------------------------------------------------ | | `type` | `"byo-vpc-aws"` | Yes | AWS BYO VPC mode. | | `vpc_id` | `string` | Yes | Existing VPC ID. | | `public_subnet_ids` | `string[]` | Yes | Public subnet IDs used for public ingress resources. | | `private_subnet_ids` | `string[]` | Yes | Private subnet IDs used for services that should not receive public IPs. | | `security_group_ids` | `string[]` | No | Security groups to use. | ## byo-vpc-gcp [#byo-vpc-gcp] References an existing GCP VPC and subnet. ```json { "network": { "type": "byo-vpc-gcp", "network_name": "customer-vpc", "subnet_name": "app-us-central1", "region": "us-central1" } } ``` | Field | Type | Required | Description | | -------------- | --------------- | -------: | ----------------------------- | | `type` | `"byo-vpc-gcp"` | Yes | GCP BYO VPC mode. | | `network_name` | `string` | Yes | Existing VPC network name. | | `subnet_name` | `string` | Yes | Existing subnet name. | | `region` | `string` | Yes | Region containing the subnet. | ## byo-vnet-azure [#byo-vnet-azure] References an existing Azure VNet. ```json { "network": { "type": "byo-vnet-azure", "vnet_resource_id": "/subscriptions/.../virtualNetworks/customer-vnet", "public_subnet_name": "public", "private_subnet_name": "private" } } ``` | Field | Type | Required | Description | | --------------------------------- | ------------------ | -------: | -------------------------------------------------------------------------------------------------- | | `type` | `"byo-vnet-azure"` | Yes | Azure BYO VNet mode. | | `vnet_resource_id` | `string` | Yes | Full resource ID of the existing VNet. | | `public_subnet_name` | `string` | Yes | Public subnet name. | | `private_subnet_name` | `string` | Yes | Private subnet name. | | `application_gateway_subnet_name` | `string` | No | Dedicated subnet for a classic Application Gateway. | | `private_endpoint_subnet_name` | `string` | No | Dedicated subnet for Private Endpoints. Required only when the stack contains a Postgres resource. | ## Outputs [#outputs] Network outputs are cloud-agnostic and intended for observability. | Field | Type | Description | | ------------------- | ---------------- | ---------------------------------------- | | `networkId` | `string` | Human-readable cloud network identifier. | | `availabilityZones` | `number` | Number of zones used. | | `hasPublicSubnets` | `boolean` | Whether public subnets exist. | | `hasNatGateway` | `boolean` | Whether a managed NAT gateway exists. | | `cidr` | `string \| null` | CIDR block when created by Alien. | # Behavior & Limits (/docs/infrastructure/network/behavior) ## Guarantees [#guarantees] **Generated infrastructure resource.** Network is generated from `StackSettings.network` when required. It is not declared directly in `alien.ts`. **Frozen lifecycle.** Network is infrastructure. It is owned by setup/admin configuration, not by live application code. **BYO means no ownership transfer.** In BYO modes, Alien references and validates existing network infrastructure. It does not create or delete the VPC/VNet. **Network type is immutable.** Changing from one mode to another, such as `create` to `byo-vpc-aws`, is rejected as an invalid update. ## Limits [#limits] | Limit | Value | Notes | | ------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------- | | Direct `alien.ts` builder | None | Configure through deployment stack settings. | | Default created CIDR | `/16` | Generated from stack/network ID when `cidr` is omitted. | | AWS created subnets | `/20` per subnet | Public and private ranges are carved from the VPC CIDR. | | Azure created subnets | CIDR split into public/private halves | The controller derives public and private subnet CIDRs from the selected VNet CIDR. | | Update mode changes | Not allowed | Replace the deployment network instead. | | BYO egress | Customer-managed | Alien does not create NAT/proxy/VPN resources in BYO modes. | ## Platform Notes [#platform-notes] ### AWS [#aws] * `create` creates a VPC, subnets, route tables, Internet Gateway, NAT Gateway, and security group. * If `cidr` is omitted, Alien searches for an available `/16`, preferring `100.64.0.0/10`, then `172.16.0.0/12`, then `10.0.0.0/8`. * `use-default` discovers the account default VPC and public subnets. * `byo-vpc-aws` stores the provided VPC, subnet, and security group references. ### GCP [#gcp] * `create` creates a custom VPC network, regional subnetwork, Cloud Router, Cloud NAT, and firewall rule. * If `cidr` is omitted, Alien generates a deterministic `/16` in the `100.64.0.0/10` range. * `use-default` uses the project's `default` network and regional subnet. * `byo-vpc-gcp` stores the provided network, subnet, and region references. ### Azure [#azure] * Azure has no provider default VNet. `use-default` creates VNet infrastructure rather than attaching to a cloud default. * `create` creates a VNet, public and private subnets, NAT Gateway, Public IP, and Network Security Group. * If `cidr` is omitted, Alien generates a deterministic `/16` in the `100.64.0.0/10` range. * `byo-vnet-azure` stores the provided VNet resource ID and subnet names. ### Kubernetes / On-Prem [#kubernetes--on-prem] Network is provided by the cluster operator. Alien does not create VPC/VNet infrastructure for Kubernetes deployments. ### Local [#local] Local deployments use local host/container networking. No cloud network resource is created. ## Design Decisions [#design-decisions] **Network is configured outside `alien.ts`.** The same application code can be deployed into different customer network topologies without changing the stack manifest. **`create` is the production default.** It gives Alien ownership of the network it needs to create, update, and delete. BYO modes are for customers with existing network controls. **BYO modes keep responsibility with the customer.** Alien does not silently add NAT, routing, firewall, or peering rules to a customer-managed network. # Overview (/docs/infrastructure/network) Network configures the VPC or VNet used by a deployment. It covers subnets, NAT, routing, and security groups for services running in a customer environment. Developers do not instantiate `new alien.Network(...)` in `alien.ts`. Network is generated from deployment stack settings when the deployment needs cloud networking. ## Platform Mapping [#platform-mapping] | Platform | Backing Infrastructure | | -------------------- | ---------------------------------------------------------------------------- | | AWS | VPC, subnets, route tables, Internet Gateway, NAT Gateway, security group | | GCP | VPC network, subnetwork, Cloud Router, Cloud NAT, firewall rule | | Azure | VNet, public/private subnets, NAT Gateway, Public IP, Network Security Group | | Kubernetes / On-Prem | External cluster networking | \| Local | Local host networking | ## When to Configure Network [#when-to-configure-network] Configure Network when the customer environment has specific networking requirements: * use an isolated VPC/VNet instead of a provider default network * bring an existing customer-managed VPC/VNet * choose a CIDR block * control how private services get outbound internet access * attach public ingress resources to known public subnets If no network settings are provided and the stack needs networking, Alien creates the default network shape required by the platform. ## Modes [#modes] | Mode | Use For | Ownership | | ---------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------- | | `create` | Production deployments that should have an Alien-managed isolated network | Alien creates and deletes the network infrastructure. | | `use-default` | Fast dev/test deployments | Alien uses provider defaults where available. | | `byo-vpc-aws` | Existing AWS VPC | Customer owns routing, egress, subnets, and security posture. | | `byo-vpc-gcp` | Existing GCP VPC | Customer owns routing, egress, subnet, and firewall posture. | | `byo-vnet-azure` | Existing Azure VNet | Customer owns routing, egress, subnets, and NSG posture. | ## Example Stack Settings [#example-stack-settings] Network settings are supplied with deployment configuration or generated setup files, not in the resource builder. ```json title="stack-settings.json" { "network": { "type": "create", "cidr": "100.88.0.0/16", "availability_zones": 2 } } ``` ```bash alien render --format terraform --target aws \ --stack ./alien.ts \ --stack-settings ./stack-settings.json ``` ## Bring Your Own Network [#bring-your-own-network] ```json title="aws-stack-settings.json" { "network": { "type": "byo-vpc-aws", "vpc_id": "vpc-0123456789abcdef0", "public_subnet_ids": ["subnet-public-a", "subnet-public-b"], "private_subnet_ids": ["subnet-private-a", "subnet-private-b"], "security_group_ids": ["sg-0123456789abcdef0"] } } ``` BYO modes validate and reference existing infrastructure. Alien does not create or delete the network itself. See [API Reference](/docs/infrastructure/network/api) for all settings and [Behavior & Limits](/docs/infrastructure/network/behavior) for platform-specific behavior. # Pricing (/docs/infrastructure/network/pricing) Alien provisions Network infrastructure in the customer's cloud when the deployment uses `create` or a provider-specific default mode that requires cloud resources. The customer pays the cloud provider for the underlying network resources. Alien currently lists Network as a free auto-managed resource on [Pricing](/pricing). ## Cost Components [#cost-components] | Component | Applies When | Notes | | ----------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------- | | NAT Gateway / Cloud NAT | Private services need outbound internet access | Usually the largest network fixed cost for small deployments. | | Public IP addresses | NAT gateways or public load balancers need public IPs | Pricing varies by provider and region. | | Load balancers | Containers expose public ports | Billed under the Container or ingress cost shape, but depends on Network. | | Data processing | NAT, load balancing, and cross-zone traffic | Charged by provider-specific data processing dimensions. | | Data transfer | Internet egress or cross-region traffic | Charged by provider and destination. | ## Mode Impact [#mode-impact] | Mode | Network Resources Billed Through Alien-created Infra? | Notes | | ---------------- | ----------------------------------------------------: | ------------------------------------------------------------------------------------------------------- | | `create` | Yes | Alien creates VPC/VNet, subnets, NAT, routing, and firewall/security primitives as needed. | | `use-default` | Sometimes | AWS/GCP use provider defaults; Azure creates VNet/NAT infrastructure because Azure has no default VNet. | | `byo-vpc-aws` | No | Customer-owned VPC costs remain with the existing network. | | `byo-vpc-gcp` | No | Customer-owned VPC costs remain with the existing network. | | `byo-vnet-azure` | No | Customer-owned VNet costs remain with the existing network. | ## Cost Control [#cost-control] * Use BYO modes when the customer already has central NAT, inspection, or egress controls. * Use `create` when the deployment should own its network lifecycle and cleanup. * Review NAT and egress costs before deploying many small environments; fixed NAT hourly charges can dominate low-traffic deployments. * Public containers add load-balancer and data-processing charges on top of network baseline costs. ## Sources [#sources] * [AWS VPC Pricing](https://aws.amazon.com/vpc/pricing/) * [AWS NAT Gateway Pricing](https://aws.amazon.com/vpc/pricing/#NAT_Gateway) * [GCP VPC Network Pricing](https://cloud.google.com/vpc/network-pricing) * [Azure Virtual Network Pricing](https://azure.microsoft.com/en-us/pricing/details/virtual-network/) * [Azure NAT Gateway Pricing](https://azure.microsoft.com/en-us/pricing/details/azure-nat-gateway/) # API Reference (/docs/infrastructure/postgres/api) The Postgres binding is **connection-only**: it resolves connection details and you connect with your own driver or ORM. Unlike other resources it wraps no operations — every backend speaks the same PostgreSQL wire protocol. Get a handle with `postgres(name)` from `@alienplatform/sdk` (TypeScript) or `alien_bindings::Bindings::from_env()?.postgres(name).await?` (Rust); on the managed clouds it reads the password from the cloud secret store with the workload's own identity, so your code never touches a secret locator. ## The binding environment variable [#the-binding-environment-variable] A linked Postgres resource injects `ALIEN__BINDING` (uppercased, hyphens to underscores — a resource named `db` becomes `ALIEN_DB_BINDING`) containing a JSON object tagged by `service`: | `service` | Platform | Fields | | ----------------- | ---------------- | ---------------------------------------------------------------------- | | `local-postgres` | Local | `host`, `port`, `database`, `username`, `password` | | `external` | Kubernetes / BYO | `host`, `port`, `database`, `username`, `password` | | `aurora` | AWS | `clusterEndpoint`, `port`, `database`, `username`, `passwordSecretArn` | | `cloud-sql` | GCP | `host`, `port`, `database`, `username`, `passwordSecretName` | | `flexible-server` | Azure | `host`, `port`, `database`, `username`, `passwordSecretUri` | Local and External carry the password inline. The cloud variants keep the password out of the environment: they carry a locator into the cloud secret store (Secrets Manager ARN, Secret Manager name, Key Vault URI), and your workload's injected credentials (IAM role, Workload Identity, Managed Identity) authorize reading it. ## connection [#connection] Resolves everything a driver needs: `connectionString`, `host`, `port`, `database`, `username`, `password`, and TLS options discriminated by `sslmode` (`disable` for the local backend, `verify-ca` and `verify-full` on the managed clouds, with the provider CA roots included). On a managed cloud the first call reads the password from the cloud secret store with the workload's own identity; the resolved value is reused, so call the factory again to pick up a rotated password. ```typescript import { postgres } from "@alienplatform/sdk" import { Client } from "pg" const conn = await postgres("db").connection() // name matches the stack definition const client = new Client({ host: conn.host, port: conn.port, database: conn.database, user: conn.username, password: conn.password, ssl: conn.ssl, // false, or CA + verification options — already resolved per backend }) await client.connect() const { rows } = await client.query("SELECT 1") ``` Prefer the individual fields plus `ssl` over `connectionString`: node-postgres parses URL TLS parameters differently than explicit options, and `ssl` carries the CA roots the URL cannot. ```rust let bindings = alien_bindings::Bindings::from_env()?; let db = bindings.postgres("db").await?; // `postgres://user:password@host:port/database?sslmode=`, derived on demand let url = db.connection_string(); // or the individual fields: host, port, database, username, password, tls let params = db.connection_params(); ``` Other languages parse the binding environment variable and resolve the provider-specific password locator with the cloud's native SDK — the workload credentials Alien injects (IAM role, Workload Identity, Managed Identity) authorize the read. ## Notes [#notes] * **TLS per platform:** Local can use plaintext. External/BYO and managed-cloud connections must follow the server's TLS policy and verify its certificate. Bundle the provider or database CA when the system trust store does not contain it. * Configure your client with **connect retries and a ≥ 30 s connect timeout** so the first connection after an AWS auto-pause succeeds (see [Behavior](/docs/infrastructure/postgres/behavior)). * The credentials connect as the admin user: `alien` on the managed clouds, `postgres` on Local. Read it from `username`, don't hard-code it. Define application-level SQL roles yourself if you need them. * The binding JSON never contains a plaintext password on the managed clouds — treat the inline Local/External password as sensitive anyway: keep it out of logs and telemetry. # Behavior & Limits (/docs/infrastructure/postgres/behavior) ## Guarantees [#guarantees] **Real PostgreSQL.** Every platform runs PostgreSQL on the standard wire protocol: full ACID transactions, the complete SQL surface, and your own driver or ORM. Alien provisions and connects it. It does not wrap or subset it. **Private networking.** On the clouds (AWS, GCP, Azure), Postgres has no public IP and is reachable only by same-stack workloads — there's no public-exposure option to misconfigure, and Alien auto-creates the Network those workers (Lambda, Cloud Run, Container Apps) attach to. Local listens only on localhost. On Kubernetes the operator provides the database and owns its networking. **pgvector and core extensions.** On the clouds and Local, `vector` (pgvector), `pg_trgm`, `uuid-ossp`, and `pgcrypto` are available — your migrations just run `CREATE EXTENSION IF NOT EXISTS …`. Vector indexes use pgvector's HNSW and IVFFlat types. Each managed cloud ships its provider's pgvector — AWS Aurora 0.8.x, GCP Cloud SQL 0.8.0, Azure Flexible Server 0.8.2 (the version tracks the engine's minor release and moves over time); Local ships 0.8.1, built by Alien. On Kubernetes, extensions depend on the operator-provided database. **Automated backups on every cloud.** The managed clouds keep automated backups with 7-day retention. Local is for development and is not backed up. ## Limits [#limits] | Limit | Value | Notes | | -------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Engine version | `15`, `16`, `17` | Fixed after create; changing it is rejected (recreate to move versions). | | Storage | default `20Gi`, grow-only | Shrinking is rejected; Azure rounds up to a 32 GiB minimum. | | Max `cpu` / `memory` | GCP 16 vCPU / 60 GiB · Azure 8 vCPU / 32 GiB | GCP/Azure reject a larger request; AWS sizes the ACU ceiling from `memory`, up to 256 ACU (≈ 512 GiB). | | Backup retention | 7 days | Managed clouds only; not on Local. | `cpu` and `memory` are sizing hints, applied differently per cloud: * **GCP** and **Azure** pick the smallest tier that satisfies both `cpu` and `memory`, rounding up (GCP to a `db-custom` tier, Azure to a Flexible Server SKU). * **AWS** (Aurora Serverless v2, ACU-based) sizes from `memory`; `cpu` is not used. **Changing a live database.** `cpu` and `memory` resize **in place** on the managed clouds — AWS adjusts the Aurora ACU ceiling, GCP patches the Cloud SQL machine tier, and Azure re-applies the Flexible Server SKU. Your data is kept, with a brief restart. The major `version` is fixed after create: an in-place major upgrade isn't supported yet, so changing it is rejected — move to a new version by creating a new resource and migrating the data. On Local, `cpu`/`memory` have no effect and the version is pinned at create. ## Platform Notes [#platform-notes] ### AWS — Aurora Serverless v2 [#aws--aurora-serverless-v2] Auto-pause: while idle, compute scales to zero and you pay for storage only. The first connection after idle waits roughly 15 s while the instance resumes (longer after long idle periods), so use connect retries and a ≥ 30 s connect timeout, and don't point health-check probes at the endpoint — they keep it awake. Private access is a DB subnet group plus a dedicated security group that admits 5432 from the stack only. ### GCP — Cloud SQL (Enterprise) [#gcp--cloud-sql-enterprise] Provisioned instances (no scale-to-zero); `cpu`/`memory` map to a `db-custom` Enterprise tier. `highAvailability()` maps to a regional configuration. Private access is via a Private Service Connect endpoint. ### Azure — Flexible Server [#azure--flexible-server] Provisioned instances. `highAvailability()` maps to zone-redundant HA. Minimum storage is 32 GiB (smaller requests round up). Private access is via a Private Endpoint in a dedicated subnet plus a private DNS zone. ### Kubernetes / On-Prem [#kubernetes--on-prem] Postgres is not provisioned by Alien. The cluster operator provides the database, and its guarantees — networking, extensions, backups — depend on that backing service. ### Local [#local] A native process from embedded binaries, no Docker required. The data directory persists across restarts; Alien restarts the process on crash and on CLI startup. Backups are out of scope. ## Design Decisions [#design-decisions] **Private only.** There is deliberately no public-exposure option: a public database endpoint simply can't be configured, so it can't be misconfigured. **Delete removes the data.** Deleting the resource deletes the database and its data, with no final snapshot — consistent with every other Alien resource. Export it first if you need to keep it. **Grow-only storage, fixed version.** Storage grows but never shrinks, and the engine version is fixed at create. An in-place major upgrade is significant and hard to reverse, so rather than silently not upgrading or destroying data by recreating, Alien rejects a version change and asks you to migrate to a new resource. `cpu` and `memory`, by contrast, resize in place. # Overview (/docs/infrastructure/postgres) `alien.Postgres` is a managed relational database. Declare it in your `alien.ts` and Alien provisions the right PostgreSQL for the target platform — with `pgvector` available out of the box and no public IP. ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | ----------------------------------------------- | ---------------- | | AWS | Aurora Serverless v2 (PostgreSQL) | Alien | | GCP | Cloud SQL for PostgreSQL (Enterprise) | Alien | | Azure | Azure Database for PostgreSQL — Flexible Server | Alien | | Kubernetes / On-Prem | External (operator-provided) | Cluster operator | | Local | Embedded native PostgreSQL process | Alien | On Kubernetes / on-prem, Postgres is not provisioned by Alien — the cluster operator provides the database (see [Behavior](/docs/infrastructure/postgres/behavior)). ## When to Use [#when-to-use] Use Postgres when your app needs a relational database: transactions, joins, SQL, and — via `pgvector` — embeddings and similarity search for RAG and semantic search. Reach for a different resource when Postgres isn't the fit: large files and blobs belong in [Storage](/docs/infrastructure/storage), and simple key lookups with TTL in [KV](/docs/infrastructure/kv). The database is reachable only by same-stack workloads (see [Behavior](/docs/infrastructure/postgres/behavior)). It is not an internet-facing database. ## Stack Definition [#stack-definition] ```typescript const db = new alien.Postgres("db").build() // Sized explicitly const analytics = new alien.Postgres("analytics") .version("17") .cpu("2") .memory("8Gi") .storage("100Gi") .highAvailability() .build() ``` | Parameter | Type | Default | Description | | ------------------ | --------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | — | Resource identifier. On Local it's also the database name; on the managed clouds the database is named `alien` (read it from the binding's `database` field). | | `version` | `string` | `"17"` | Major engine version: `"15"`, `"16"`, or `"17"`. | | `cpu` | `string` | smallest tier | Requested vCPUs, e.g. `"0.5"`, `"2"`. Helps size the GCP/Azure tier; not used on AWS. | | `memory` | `string` | smallest tier | Requested memory, e.g. `"1Gi"`, `"8Gi"`. Sizes AWS, and helps size the GCP/Azure tier. | | `storage` | `string` | `"20Gi"` | Allocated storage. Grow-only. | | `highAvailability` | `boolean` | `false` | Multi-AZ / regional / zone-redundant HA. | `cpu` and `memory` are sizing hints: GCP and Azure pick the smallest tier that satisfies both; AWS sizes from `memory` (Aurora Serverless v2 is ACU-based). See [Behavior](/docs/infrastructure/postgres/behavior) for the per-cloud detail. ## pgvector Out of the Box [#pgvector-out-of-the-box] `pgvector` is available on every platform Alien provisions. Run the one standard line in your migrations: ```sql CREATE EXTENSION IF NOT EXISTS vector; ``` `pg_trgm`, `uuid-ossp`, and `pgcrypto` are available too. See [Behavior](/docs/infrastructure/postgres/behavior) for the shipped version and index types. ## Connecting [#connecting] The binding exposes connection details; your app connects with its own driver or ORM. The binding shape differs by platform — see the [API Reference](/docs/infrastructure/postgres/api) for the full shape per platform. It is the source of truth; the quick start below only covers Local and External, where the password arrives inline. For Local and External Postgres, parse the `ALIEN_DB_BINDING` environment variable (`ALIEN__BINDING`, uppercased, hyphens to underscores) and hand the fields to your driver: ```typescript import { Client } from "pg" const binding = JSON.parse(process.env.ALIEN_DB_BINDING!) // local-postgres / external — { service, host, port, database, username, password } const client = new Client({ host: binding.host, port: binding.port, database: binding.database, user: binding.username, password: binding.password, }) ``` On AWS, GCP, and Azure the binding carries a secret locator instead of an inline password — see [API Reference](/docs/infrastructure/postgres/api#the-binding-environment-variable) for the per-cloud fields and how to resolve them. ```rust let binding: serde_json::Value = serde_json::from_str(&std::env::var("ALIEN_DB_BINDING")?)?; ``` Local and External variants include connection fields directly. Managed-cloud variants contain a provider secret locator; resolve it with the provider's native SDK and connect with verified TLS. See the [API Reference](/docs/infrastructure/postgres/api) for the full binding shape per platform, TLS settings, and driver-specific notes. # Pricing (/docs/infrastructure/postgres/pricing) Alien provisions Postgres in your customer's cloud. The customer pays the cloud provider for the database, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Prices shown for US East regions and are indicative. Last verified: June 2026.* ## Cost Components [#cost-components] A Postgres bill has the same components on every cloud: | Component | What it is | | ------------- | ---------------------------------------------------------------------------------------- | | Compute | The instance (or, on AWS, ACU-seconds). | | Storage | Allocated GB per month. | | Backups | Retained backup storage (7-day retention). | | HA multiplier | `highAvailability()` roughly doubles compute + storage (a standby that serves no reads). | ## Per Cloud [#per-cloud] **AWS — Aurora Serverless v2.** Billed per ACU-hour with **scale-to-zero**: while idle, compute bills nothing and you pay storage only (\~$0.10/GB-month). A 0.5-ACU floor is \~$44/month if it never idles; with scale-to-zero the effective cost tracks how much your stack actually idles. The trade-off is a \~15 s resume after idle (see [Behavior](/docs/infrastructure/postgres/behavior)). **GCP — Cloud SQL (Enterprise).** Alien defaults to a 1 vCPU / 3.75 GB Enterprise tier (`db-custom-1-3840`); `cpu`/`memory` size it up to a larger `db-custom` tier. There is no shared-core or scale-to-zero option, so it bills continuously (around $50/month for compute at the default tier, before storage). `highAvailability()` maps to a regional configuration. **Azure — Flexible Server.** Burstable `B1ms` starts around $13/month; minimum storage is 32 GiB (smaller requests round up). `highAvailability()` maps to zone-redundant HA at roughly 2× cost. ## Local [#local] No cloud charges — Postgres runs as a local process on your machine. ## Free Tiers [#free-tiers] * **AWS**: Aurora Serverless v2 is not covered by the RDS free tier. * **GCP**: Cloud SQL has no always-free tier. * **Azure**: Flexible Server offers a 12-month free tier (a B1ms instance, 750 hours/month, plus 32 GiB storage) for new accounts. ## Sources [#sources] * [AWS Aurora Pricing](https://aws.amazon.com/rds/aurora/pricing/) * [GCP Cloud SQL Pricing](https://cloud.google.com/sql/pricing) * [Azure Database for PostgreSQL Pricing](https://azure.microsoft.com/en-us/pricing/details/postgresql/flexible-server/) # API Reference (/docs/infrastructure/queue/api) Get a handle with `queue(name)` from `@alienplatform/sdk` (TypeScript) or `alien_bindings::Bindings::from_env()?.queue(name).await?` (Rust). Queue handles are bound to one queue by name — resolve the handle once, then call operations without repeating the queue name. ```typescript import { queue } from "@alienplatform/sdk" const tasks = queue("tasks") // name matches the stack definition ``` ```rust let tasks = bindings.queue("tasks").await?; // name matches the stack definition ``` ## send [#send] Sends a message to the queue. ```typescript await tasks.send({ type: "process-image", imageId: "123" }) // serialized as JSON await tasks.sendText("ping") // raw text ``` ```rust tasks.send(MessagePayload::Json(json!({ "type": "process-image", "imageId": "123" }))).await?; tasks.send(MessagePayload::Text("ping".into())).await?; ``` | Parameter | Type | Required | Description | | --------- | ------------------------------------------ | -------- | ------------------------------------------------------------------------ | | `message` | `unknown` (`send`) / `string` (`sendText`) | Yes | `send` serializes with `JSON.stringify`. Max 64 KiB after serialization. | *** ## receive [#receive] Receives messages. Messages become invisible for 30 seconds (lease). ```typescript const messages = await tasks.receive(max) // messages: QueueMessage[] — { payloadType, payload, receiptHandle, attempt } for (const msg of messages) { const payload = msg.payloadType === "json" ? JSON.parse(msg.payload) : msg.payload // ... } ``` ```rust let messages: Vec = tasks.receive(max_messages).await?; ``` | Parameter | Type | Required | Description | | --------- | -------- | -------- | ---------------------------------------------- | | `max` | `number` | Yes | Maximum messages to receive per call (max 10). | *** ## ack / nack [#ack--nack] `ack` acknowledges a message, permanently removing it. Idempotent. `nack` makes it immediately redeliverable. ```typescript await tasks.ack(msg.receiptHandle) await tasks.nack(msg.receiptHandle) ``` ```rust tasks.ack(&msg.receipt_handle).await?; tasks.nack(&msg.receipt_handle).await?; ``` *** ## purge [#purge] Deletes every message in the queue. ```typescript await tasks.purge() ``` ```rust tasks.purge().await?; ``` *** ## Types [#types] ```typescript interface QueueMessage { payloadType: "json" | "text" // Payload discriminant payload: string // Serialized JSON when payloadType === "json", raw text when "text" receiptHandle: string // Opaque handle for ack/nack attempt: number // Delivery attempt, 1-based (1 = first delivery); > 1 means redelivery } ``` ```rust pub enum MessagePayload { Json(serde_json::Value), Text(String), } pub struct QueueMessage { pub payload: MessagePayload, pub receipt_handle: String, pub attempt: u32, // 1-based delivery attempt (1 = first delivery); > 1 means redelivery } ``` # Behavior & Limits (/docs/infrastructure/queue/behavior) ## Guarantees [#guarantees] On cloud platforms (AWS, GCP, Azure), Alien provisions and manages the queue backing service. These guarantees apply: **At-Least-Once Delivery.** Every message is delivered at least once. Messages may be delivered more than once — your handler must be idempotent. **No Ordering.** Messages may arrive in any order. Implement sequence numbers at the application layer if ordering matters. **30-Second Lease.** Received messages are invisible to other consumers for 30 seconds. Unacknowledged messages become visible again after the lease expires. **Ack Idempotency.** Acknowledging the same message twice is a safe no-op. **Durability.** Messages are persisted by the cloud provider and survive infrastructure failures. ## Limits [#limits] These are enforced by Alien on all platforms: | Limit | Value | | ------------------------- | --------------------------- | | Max message size | 64 KiB | | Max batch size | 10 messages per `receive()` | | Lease duration | 30 seconds | | Payload types | JSON or UTF-8 text | | Queue triggers per worker | 1 | ## Platform Details [#platform-details] ### AWS (SQS) [#aws-sqs] * Standard queues (not FIFO). At-least-once, best-effort ordering. * Native limit: 256 KB (Alien enforces 64 KiB). * Worker trigger visibility timeout: `max(30s, min(12h, worker_timeout × 6))`. * Retention: up to 14 days (default: 4 days). * `receive()` uses 20-second long polling. ### GCP (Pub/Sub) [#gcp-pubsub] * Topic + Subscription model. Pull-based. * Native limit: 10 MB (Alien enforces 64 KiB). * Retention: up to 31 days (default: 7 days). ### Azure (Service Bus) [#azure-service-bus] * Queue-based. Standard or Premium tier. * Native limit: 256 KB Standard, 100 MB Premium. * Lock duration: up to 5 minutes. ### Kubernetes / On-Prem [#kubernetes--on-prem] Queue is **not provisioned by Alien** on Kubernetes. The cluster operator provides the backing service (SQS, Kafka, Redis Streams) and configures it via Helm values. The guarantees above (delivery, durability, retention) depend entirely on the backing service. Alien enforces the limits (message size, batch size) regardless of platform. **Queue triggers are not currently supported on Kubernetes.** Use `receive()` polling instead. ### Local [#local] * Backed by an embedded SQLite database. Messages persist across restarts. * **FIFO ordering** — messages delivered in exact send order. Cloud platforms do not guarantee this; do not rely on ordering in production code. * Expired leases reclaimed lazily on next `receive()`. * Triggers supported via LocalTriggerService. ## Triggers [#triggers] | Platform | Queue Triggers | | -------------------- | -------------------------------------- | | AWS | SQS event source mapping on Lambda | | GCP | Pub/Sub push subscription to Cloud Run | | Azure | Service Bus trigger via KEDA | | Kubernetes / On-Prem | **Not currently supported** | | Local | LocalTriggerService | ## Design Decisions [#design-decisions] **64 KiB message limit.** SQS supports 256 KB and Pub/Sub 10 MB. Alien enforces 64 KiB for portability. Store large payloads in [Storage](/docs/infrastructure/storage) and pass a reference. **Fixed 30-second lease.** Configurable timeouts would expose backend-specific behavior. A fixed window keeps behavior predictable. For worker triggers, Alien auto-calculates an appropriate visibility timeout. **No exactly-once.** Some backends support it in specific configurations, but the guarantees aren't portable. Alien documents at-least-once and encourages idempotent handlers. # Overview (/docs/infrastructure/queue) Queue provides at-least-once message delivery between producers and consumers. Send JSON or text messages, receive them in batches, and acknowledge when processing is complete. Unacknowledged messages are automatically re-delivered. ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | ------------------------------------ | ---------------- | | AWS | Amazon SQS (Standard) | Alien | | GCP | Google Cloud Pub/Sub | Alien | | Azure | Azure Service Bus | Alien | | Kubernetes / On-Prem | External (SQS, Kafka, Redis Streams) | Cluster operator | | Local | SQLite (embedded database) | Alien | On Kubernetes / on-prem, Queue is not provisioned by Alien. The cluster operator provides the messaging service and configures it via Helm values. ## When to Use [#when-to-use] Use Queue for decoupling producers from consumers — task queues, event pipelines, background job processing, webhook relay. Don't use Queue for request-response patterns (use [Worker](/docs/infrastructure/worker) invocation) or for ordered event streams (Queue does not guarantee ordering). ## Stack Definition [#stack-definition] Declare a Queue resource in your `alien.ts`: ```typescript const tasks = new alien.Queue("tasks").build() ``` | Parameter | Type | Description | | --------- | -------- | -------------------------------------------------------- | | `id` | `string` | Resource identifier. `[A-Za-z0-9-_]`, max 64 characters. | Queue has no additional configuration options. The backing service (SQS, Pub/Sub, Service Bus) is determined by the deployment platform. ## Quick Start [#quick-start] ```typescript import { queue } from "@alienplatform/sdk" const tasks = queue("tasks") // bind the queue once by name // The handle is bound to the queue; send() JSON-serializes the message await tasks.send({ type: "process-image", imageId: "123" }) const messages = await tasks.receive(10) for (const msg of messages) { await processTask(msg.payloadType === "json" ? JSON.parse(msg.payload) : msg.payload) await tasks.ack(msg.receiptHandle) } ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let q = bindings.queue("tasks").await?; // bind the queue once by name q.send(MessagePayload::Json(json!({"type": "process-image"}))).await?; let messages = q.receive(10).await?; for msg in messages { // process... q.ack(&msg.receipt_handle).await?; } ``` ## Triggers [#triggers] Queues can automatically trigger workers — one message per invocation: ```typescript // alien.ts const worker = new alien.Worker("worker") .trigger(tasks) .build() ``` ```typescript // worker code import { onQueueMessage } from "@alienplatform/sdk" onQueueMessage("tasks", async (message) => { await processTask(message.payload) // auto-acknowledged on success }) ``` See [Behavior & Limits](/docs/infrastructure/queue/behavior) for trigger support per platform. # Pricing (/docs/infrastructure/queue/pricing) Alien provisions Queue in your customer's cloud. The customer pays the cloud provider for the underlying queue service, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Prices shown for US East regions. Last verified: April 2026.* ## Request Pricing [#request-pricing] | Provider | Service | Price per million requests | Notes | | -------- | ----------------- | ------------------------------ | ------------------------------------------------ | | AWS | SQS Standard | $0.40 | First 1M requests/month free. | | GCP | Pub/Sub | \~$0.40 (per million messages) | Charged per message + data volume ($40/TiB). | | Azure | Service Bus Basic | $0.05 | Basic tier. Standard: $10/mo base + per-message. | ## Example: 10M Messages/month [#example-10m-messagesmonth] | Provider | Cost | Notes | | ----------------- | ----- | ---------------------------------- | | AWS SQS | $3.60 | 10M - 1M free = 9M × $0.40/M | | GCP Pub/Sub | $4.00 | 10M × $0.40/M (data volume varies) | | Azure Service Bus | $0.50 | Basic tier: 10M × $0.05/M | ## Free Tiers [#free-tiers] * **AWS SQS**: 1 million requests/month (always free). * **GCP Pub/Sub**: 10 GiB of messages/month (always free). * **Azure Service Bus**: No free tier. ## Platform Notes [#platform-notes] * **SQS** charges per request, where each request can contain up to 10 messages (batch). Alien's `receive()` uses long polling (20-second wait) which counts as one request. * **Pub/Sub** pricing is based on message volume (data transferred), not just message count. Small messages are cheaper. * **Service Bus** has tiered pricing. Basic is cheapest but limited. Standard includes topics and sessions at $10/month base. ## Sources [#sources] * [AWS SQS Pricing](https://aws.amazon.com/sqs/pricing/) * [GCP Pub/Sub Pricing](https://cloud.google.com/pubsub/pricing) * [Azure Service Bus Pricing](https://azure.microsoft.com/en-us/pricing/details/service-bus/) # API Reference (/docs/infrastructure/sandbox/api) Every method below is reached through the binding, not the resource declaration. The declaration provisions the parent; these create and drive sessions at runtime. Where a platform lacks a capability the call returns a typed `AlienError` naming **both the platform and the capability**. It never no-ops and never returns a null-ish success — you will know, and the error will tell you what to change. ## capabilities() [#capabilities] ```typescript const caps: string[] = await box.capabilities() if (caps.includes("files")) { /* ... */ } ``` ```rust let caps: SandboxCapabilities = sandbox.capabilities(); if caps.files { /* ... */ } ``` Returns what this platform's backend actually supports — a list of capability names in TypeScript, a struct of booleans in Rust. Branch on it rather than catching errors, when you have a sensible fallback. | Capability | AWS | Azure | GCP | Kubernetes | Local | | ------------------------ | --- | ------ | ------ | ---------- | ----- | | `files` | yes | **no** | yes | yes | yes | | `reconnect` | yes | yes | **no** | yes | yes | | `preview` | yes | no | no | no | yes | | `suspendResume` | yes | no | no | no | no | | `snapshot` | no | no | no | no | no | | `egressDeny` | yes | **no** | yes | yes | yes | | `domainEgressRules` | no | no | no | no | no | | `enforcedLimits` | yes | **no** | **no** | yes | yes | | `processLimit` | no | no | no | no | yes | | `sessionLifetime` | yes | no | no | yes | no | | `supervisorPidNamespace` | no | no | no | no | no | ## Sessions [#sessions] ### create / getOrCreate / get / list [#create--getorcreate--get--list] ```typescript const session = await box.create({ sessionId: "turn-1" }) ``` ```rust let session = sandbox.create(CreateSessionRequest { session_id: Some("turn-1".to_string()), tenant_key: None, env: BTreeMap::new(), }).await?; ``` `create` takes a session id and, optionally, a tenant key. It takes **no image, cpu, memory or port arguments** — those come from your `alien.Sandbox` declaration. An application cannot raise its own ceilings by asking, which is the point. `get` requires `reconnect`. On GCP it returns the typed error rather than `null`: a `null` reads as "the session expired", when the truth is that GCP cannot address a session from another instance at all. `getOrCreate` is the one call that degrades instead — where reconnect is unavailable it creates a fresh session. `list` enumerates this sandbox's sessions on Kubernetes and Local only. AWS, Azure and GCP raise rather than enumerate — none of their APIs can list sessions scoped to one sandbox. Reach a session whose id you hold with `get`. ### terminate [#terminate] ```typescript await box.terminate(session.sessionId) ``` ```rust sandbox.terminate(&session.session_id).await?; ``` Terminating a session that is already gone succeeds on Azure and Kubernetes, because terminate is the cleanup path and a cleanup that fails on an absent session turns teardown into a retry loop. AWS and Local raise instead — they check the session exists before acting — so treat a not-found error from `terminate` as success if you are writing a teardown that must converge. ## runCommand [#runcommand] ```typescript for await (const frame of sandbox.runCommand(sessionId, ["python3", "main.py"], { deadlineMs: 30_000, })) { if (frame.kind === "stdout") process.stdout.write(frame.data) if (frame.kind === "exit") console.log(frame.exitCode) } ``` ```rust let mut frames = sandbox.run_command(session_id, RunCommandRequest { command: vec!["python3".into(), "main.py".into()], working_directory: None, env: BTreeMap::new(), deadline: Duration::from_secs(30), }).await?; while let Some(frame) = frames.next().await { /* ... */ } ``` Streams output frames, then exactly one terminal frame. * **`deadline` is required.** A request without one is rejected rather than defaulted — a default deadline is a hang waiting for a slow day. * **`seq` is monotonic across stdout and stderr together** on AWS, Kubernetes and GCP, so you can interleave them in the order they were produced. Azure and Local return an already-buffered response and number it stdout-then-stderr, which does not reconstruct production order. * **Exactly one terminal `exit` frame**, always last. Frames are `stdout`, `stderr` and `exit` — a failure is a thrown error in TypeScript and a stream `Err` in Rust, not a frame. A stream that ends without an `exit` is a transport failure, not a clean exit 0. * Output carries backpressure on AWS, Kubernetes and GCP: a consumer that stops reading stops the sandbox's writer rather than filling memory. Azure and Local buffer the whole response first. ## Files [#files] ```typescript await box.writeFiles(sessionId, { "/work/main.py": bytes }) const out = await box.readFile(sessionId, "/work/out.txt") await box.mkdir(sessionId, "/work/build") ``` ```rust sandbox.write_files(session_id, BTreeMap::from([ ("/work/main.py".to_string(), bytes), ])).await?; let out = sandbox.read_file(session_id, "/work/out.txt").await?; sandbox.mkdir(session_id, "/work/build").await?; ``` Requires `files`, which Alien's Azure binding does not implement. Paths may not escape the session root, and the strength of that varies. On AWS and Kubernetes the agent opens through `openat2`, so the kernel resolves and opens in one call and refuses `..`, absolute paths and symlinks rather than following them — code inside the sandbox cannot race a check it never gets to see. Transfers there are bounded at 32 MiB. Local rewrites paths against the session root and rejects `..` without resolving symlinks; GCP checks for `..` and passes the path through unchanged, and neither applies a size bound. ## preview [#preview] **Rust only.** The TypeScript binding exposes no `preview` method, so `capabilities()` there never reports it. ```rust let capability = sandbox.preview(session_id, 8080).await?; ``` | Parameter | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------------------------- | | `sessionId` | `string` | Yes | The session to expose. | | `port` | `number` | Yes | Must be listed in `previewPorts` on the resource declaration. | **Returns a typed capability, not a URL string.** AWS needs auth headers and a port header. A bare URL cannot carry those, and handing you one would push the auth onto you to get wrong. Local returns an unauthenticated capability with no headers and no expiry. | Field | Type | Description | | ------------------ | ------------------------ | ----------------------------------------------- | | `endpoint` | `string` | The address to send requests to. | | `headers` | `Record` | Auth headers that must accompany every request. | | `allowedPorts` | `number[]` | Ports this capability covers. | | `expiresInSeconds` | `number` | Lifetime of the capability. | A port not declared in [`previewPorts`](/docs/infrastructure/sandbox#configuration) cannot be exposed at runtime, so an application cannot widen its own ingress. Requires `preview`, which today means AWS and Local. Kubernetes returns the typed error until the session-scoped ingress gateway exists, GCP has no mechanism, and Azure's is not implemented yet — the platform has a per-port URL closed to anonymous traffic, and the binding does not use it. ## suspend / resume / snapshot [#suspend--resume--snapshot] ```typescript await box.suspend(sessionId) await box.resume(sessionId) ``` ```rust sandbox.suspend(session_id).await?; sandbox.resume(session_id).await?; ``` `snapshot` is Rust-only and unavailable on every platform; TypeScript exposes no method for it. `suspend`/`resume` need `suspendResume`, which today is **AWS only**. `snapshot` is not available on any platform yet: AWS has no user-callable session snapshot, and Azure's full-VM capture is not wired into the binding. ## Unsupported Surface [#unsupported-surface] The surface is close enough to Vercel's that porting is small. Two differences will show up when you do: 1. **`snapshot()` is capability-gated, not universal**, and no platform advertises it today. AWS has build-time image capture and suspend/resume but no user-callable session snapshot; Azure captures full VM state and the binding does not use it yet. Both return the typed error. 2. **`preview()` returns a capability, not a URL** — see above. # Behavior & Limits (/docs/infrastructure/sandbox/behavior) Claims here were verified against a real account unless the text says otherwise at the point it matters. ## Guarantees [#guarantees] **No inherited identity.** A sandbox never holds your workload's identity. There is no mounted token, no metadata credential, and no ambient principal. Credentials it legitimately needs are brokered short-lived after start, never written to env or disk at create time. **No implicit access to your stack.** A sandbox reaches no other resource in your deployment unless you hand it something explicitly. **No inbound exposure.** A sandbox is never reachable from the internet except through an authenticated, port-scoped preview capability, on the platforms that offer one. **Contents stay out of the control plane.** Heartbeats carry session counts and lifecycle states only. On AWS the image's own provider logging is disabled outright. **Capabilities are published, not assumed.** Each platform declares what it supports, and calling an unsupported operation returns a typed error naming both the platform and the capability — never a silent no-op, never a null-ish success. See [API Reference](/docs/infrastructure/sandbox/api) for the full set. **A declaration is accepted or refused, never half-applied.** A ceiling a platform cannot enforce, an egress mode it cannot express, or a session deadline it has no primitive for is rejected when you deploy rather than ignored at runtime. **`deny` egress is constructed, not inherited.** Where a cloud's default is the open internet, Alien builds the deny rather than trusting the default. ## Limits [#limits] | Limit | Value | Notes | | ------------------------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Command deadline | Required, no default | A `runCommand` without `deadlineMs` is rejected. A defaulted deadline is a hang waiting for a slow day. | | Session deadline ceiling | 8 hours on AWS | `maximumDurationInSeconds`, capped by the service. Kubernetes uses `activeDeadlineSeconds` with no fixed ceiling. | | Smallest AWS ceiling | 2Gi memory, 1 cpu, 8Gi disk | A MicroVM scales to 4x baseline, so a declaration no size satisfies is refused rather than rounded. | | Egress addressing | IPv4 only | | | Session id | Unique per sandbox | Reused ids reach the existing session where `reconnect` is available. | Which operations exist at all is a separate question from how far they go — see the capability table in the [Overview](/docs/infrastructure/sandbox#capabilities-differ-by-platform). The short version: no platform has `snapshot` or a hostname allowlist, Azure's binding implements no file transfer, and GCP cannot reconnect. ## Platform Notes [#platform-notes] ### AWS [#aws] Lambda MicroVMs, isolated at the VM level by Firecracker. GA. * **`deny` requires a network.** The cloud default is the open internet — `CreateMicrovmImage` fills in an `INTERNET_EGRESS` connector when you name none — so Alien builds a VPC egress connector whose security group permits nothing outbound. A stack declaring `deny` without a network is refused, because the connector is what makes the deny real. * Measured against a live account: one MicroVM with that connector failed to reach a public address within the probe's 8-second budget; an otherwise identical one without it returned **200**. * `allow` emits no connector at all and needs no network. * Session deadline comes from `maximumDurationInSeconds`, which the service caps at 8 hours. * Suspend and resume are available; auto-resume is off, so a stray request cannot bring a session back after the caller has moved on. Outbound connections fail, but name resolution still works: AWS resolves for a MicroVM through a stub inside the guest backed by a link-local resolver, outside your VPC, where no route table, security group or network ACL can reach it. Treat DNS as a low-bandwidth side channel under `deny` — code in the sandbox can encode data into a hostname it looks up. Nothing else leaks: the sandbox holds no cloud credentials and the metadata endpoint is unreachable. If that channel matters to you, do not run code you cannot tolerate leaking small amounts of data. ### GCP [#gcp] Cloud Run sandboxes. **Public preview.** Choose GCP for cost, not capability. Google does not publish the isolation mechanism for the sandbox itself, so this page does not name one. What is documented: a sandbox requires a second-generation execution environment, and it isolates process execution from the rest of your container. * **No reconnection.** A sandbox id is scoped to one Cloud Run instance. Turn N+1 reaches turn N's sandbox only if the request lands on the same instance. Measured over 100 conversations of 5 turns under real scale-out: `sessionAffinity = false` kept 0 of 100; `sessionAffinity = true` kept 2 of 100. A control pinned to one instance kept 4 of 4, so the measurement is sound. **GCP sandboxes are single-turn.** * **No enforced ceilings.** Sandboxes share the hosting service's CPU and memory budget, so size that service for your app *and* its concurrent sandboxes. A Sandbox declaring limits on GCP is rejected at plan time. * **No private-range denies when egress is allowed.** Kubernetes blocks RFC1918 and link-local in both modes; Azure emits no egress configuration at all, and Local's `allow` is a plain bridge. A Cloud Run sandbox has no network identity to attach policy to. * Two things GCP gives you that no other cloud does, both verified from inside: the metadata server is unreachable, and the hosting service's environment variables are invisible. * **A Sandbox on GCP needs a Worker to host it.** A Container does not satisfy this — the check names Workers specifically — so a stack with a Sandbox and no Worker fails at plan time. ### Azure [#azure] Container Apps Sandboxes, isolated at the VM level by Hyper-V. **Public preview.** * **No file transfer.** Alien's Azure binding does not implement it, so `readFile`, `writeFiles` and `mkdir` are refused. Pass what the session needs on the command line. * **Your declared image is not used.** The binding starts every session from a stock `ubuntu` disk whatever `.code(...)` says. No capability covers this, so the table cannot warn you: if your sandbox needs your own tooling, Azure cannot carry it today. This is the one Azure gap that fails silently rather than with a typed error. * **No enforced ceilings and no enforced `deny`.** Alien emits no egress configuration here, so a declared `deny` is refused rather than accepted and dropped. * Azure the cloud has a per-port URL closed to anonymous traffic, a 0.54s resume, a full-VM snapshot and a hostname egress proxy. None of them are wired through the binding today, so each capability reads `false`. * The preview carries an explicit warning that sandboxes created now might not be compatible with future releases and might need recreating. Weigh that before committing production traffic. ### Kubernetes / On-Prem [#kubernetes--on-prem] A pod under a sandboxed `runtimeClassName` — gVisor or Kata. * **A sandboxed runtime class is required, not preferred.** The controller inspects the cluster before creating anything and refuses if none is declared, or if the declared one uses an ordinary container runtime. That check has to happen up front: on Autopilot an unschedulable sandbox pod is not rejected — node auto-provisioning picks it up and the pod sits in `Pending` while nodes are created and billed. * **gVisor is a kernel boundary, not a network boundary.** The GCE metadata server answered from inside a gVisor pod on GKE, so Alien denies link-local in **both** egress modes here rather than assuming the runtime handles it. * **`deny` is only as strong as your CNI.** Kubernetes accepts a NetworkPolicy on any cluster and silently ignores it where no controller implements one. Measured both ways with the same chart: on a cluster with no NetworkPolicy controller the pod reached the public internet; on GKE Autopilot with Dataplane V2 it could not resolve a hostname. Supply a NetworkPolicy-enforcing CNI along with the cluster. * Where the CNI does enforce it, `deny` also closes DNS, which AWS's cannot. * Session deadline comes from `activeDeadlineSeconds`. ### Local [#local] Docker, on a **shared kernel**. Development only for untrusted code. * Hardening narrows the attack surface — the sandbox runs unprivileged, with all capabilities dropped, a read-only root filesystem and enforced pid, memory and cpu ceilings — but container escape stays in scope for code you do not trust. Use gVisor or Kata if you need more. * `deny` gives the session no network interface at all, which is a stronger and simpler guarantee than a private network with its gateway firewalled off. * `allow` puts sessions on one bridge per sandbox with inter-container communication disabled, which blocks session-to-session traffic on stock Linux Docker. **OrbStack accepts that setting and ignores it** — verified with the Docker CLI, no Alien code involved. On macOS under OrbStack, two egress-allowed sessions can reach each other. * The only platform with a process ceiling, via Docker's pid limit. ## Startup [#startup] | Cloud | Create | Lifetime control | | ---------- | -------------------- | ------------------------------------------ | | Azure | 0.9s measured | auto-suspend on idle | | AWS | \~5s MicroVM start | `maximumDurationInSeconds`, 8-hour ceiling | | Kubernetes | 2.7s warm / 79s cold | pod lifetime | | Local | \~1s | manager state | 79 seconds per agent turn is unusable, so on Kubernetes Alien keeps a warm pool of idle pods — two by default — and hands one out per session. A create that finds the pool drained fails rather than falling back to a cold start. ## Design Decisions [#design-decisions] **Sessions are the unit, not the resource.** Every other Alien resource provisions one durable object at deploy time. A Sandbox declaration provisions a parent; the sessions your application uses are created and destroyed at runtime, per agent turn if you want. **The floor is create, exec and terminate.** File transfer is not on it, because one backend does not implement it. Publishing that as a capability is what lets portable code branch instead of discovering the gap through an error. **Snapshot billing is not a snapshot feature.** AWS bills for MicroVM snapshot storage and I/O — see [Pricing](/docs/infrastructure/sandbox/pricing) — but those are the build-time image snapshot and suspend/resume state. `snapshot()` is unavailable on every platform. **A refused declaration beats a silently ignored one.** A ceiling GCP cannot enforce would give you a sandbox that looks bounded and is not, so the declaration is rejected instead. **No hostname allowlist, even where the cloud has one.** Azure runs a proxy that enforces one — measured, a permitted host returned **200** and another **403** — but Alien does not render that policy today. The capability reads `false` and `allowDomains` is refused, rather than working on one platform and silently doing nothing on the rest. # Overview (/docs/infrastructure/sandbox) A Sandbox runs code you do not trust — generated by a model, submitted by a user, pulled from a pull request — in an environment isolated from your application, your customer's cloud, and every other sandbox. Your app creates one per agent turn if it wants, runs commands in it, moves files in and out, and throws it away. Unlike most Alien resources, a Sandbox is not one thing that exists for the life of your stack. The declaration provisions a durable parent; **sessions are created and destroyed at runtime through the binding**. ## Platform Mapping [#platform-mapping] | Platform | Runtime | Isolation | Provisioned by | | -------------------- | ---------------------------------------- | ----------------------- | ---------------------- | | AWS | Lambda MicroVMs | Firecracker, VM-level | Alien | | GCP | Cloud Run sandboxes | Not published by Google | Alien | | Azure | Container Apps Sandboxes | Hyper-V, VM-level | Alien | | Kubernetes / On-Prem | Pod under a sandboxed `runtimeClassName` | gVisor or Kata | Alien, on your cluster | | Local | Docker container | Shared kernel | Alien local runtime | Azure and GCP are in public preview upstream. Kubernetes needs a sandboxed runtime class on the cluster, and refuses to provision without one. See [Behavior & Limits](/docs/infrastructure/sandbox/behavior). ## When to Use [#when-to-use] Use Sandbox to execute code your application did not write — an agent's generated script, a customer's build step, a submitted notebook cell — where a crash, a fork bomb, or an attempt to read the filesystem must not reach your service. Use [Container](/docs/infrastructure/container) or [Worker](/docs/infrastructure/worker) for code you control; both are simpler and cheaper. Don't reach for a Sandbox when you need a durable filesystem — sessions are disposable, so keep state outside. And Local shares a kernel with the host, so it is development-only for untrusted code unless gVisor or Kata is present. ## Capabilities differ by platform [#capabilities-differ-by-platform] Check this before you build. Create, exec and terminate work everywhere; everything else varies, and calling an unsupported capability returns a typed error naming the platform and the capability — never a silent no-op. | Capability | AWS | Azure | GCP | Kubernetes | Local | | ---------------------------------------- | --- | ------ | ------ | ---------- | ----- | | `files` — move files in and out | yes | **no** | yes | yes | yes | | `reconnect` — reach a session again | yes | yes | **no** | yes | yes | | `preview` — authenticated ingress | yes | no | no | no | yes | | `suspendResume` | yes | no | no | no | no | | `snapshot` | no | no | no | no | no | | `egressDeny` — `deny` is enforced | yes | **no** | yes | yes | yes | | `domainEgressRules` — hostname allowlist | no | no | no | no | no | | `enforcedLimits` — cpu / memory / disk | yes | **no** | **no** | yes | yes | | `processLimit` | no | no | no | no | yes | | `sessionLifetime` — platform deadline | yes | no | no | yes | no | | `supervisorPidNamespace` | no | no | no | no | no | `preview` and `snapshot` are Rust-only: the TypeScript binding exposes no method for either, so they never appear in the list `capabilities()` returns there. File transfer is the one to check first, because it is the only floor operation a platform lacks: Alien's Azure binding does not implement it. Pass what the session needs on the command line there, or pick another platform. A declaration a platform cannot honour is rejected when you deploy, not ignored at runtime. So `maxProcesses` is accepted on Local alone, and `maxLifetimeSeconds` on AWS and Kubernetes. Leave them out unless you are targeting a platform that applies them. ```typescript const caps = await box.capabilities() if (caps.includes("reconnect")) { // multi-turn: reuse the session across turns } else { // single-turn: finish inside one turn, or keep state outside the sandbox } ``` ## Quick Start [#quick-start] Declare the sandbox in your stack: ```typescript title="alien.ts" import * as alien from "@alienplatform/core" const agent = new alien.Sandbox("agent") .code({ type: "image", image: "ubuntu:24.04" }) .limits({ cpu: "1", memory: "2Gi", disk: "20Gi" }) .egress({ mode: "deny" }) .session({}) .build() export default new alien.Stack("app") .add(agent, "live") .build() ``` That declaration targets AWS, Kubernetes and Local: `.limits(...)` needs `enforcedLimits` and `deny` needs `egressDeny`, and Azure has neither while GCP lacks the first. Drop both to deploy everywhere, and see [Configuration](#configuration) for which field needs which capability. Then drive sessions from your application: ```typescript import { sandbox } from "@alienplatform/sdk" const box = sandbox("agent") const session = await box.create({ sessionId: "turn-1" }) for await (const frame of box.runCommand( session.sessionId, ["python3", "-c", "print(2 + 2)"], { deadlineMs: 30_000 }, )) { if (frame.kind === "stdout") process.stdout.write(frame.data) if (frame.kind === "exit") console.log("exit", frame.exitCode) } await box.terminate(session.sessionId) ``` Every method is in the [API Reference](/docs/infrastructure/sandbox/api). ## Configuration [#configuration] | Method | Type | Required | Description | | -------------------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.code(...)` | `{ type: "image", image }` | Yes | A prebuilt image reference. Building from source is not supported on any backend yet. **Not applied on Azure** — see [Behavior & Limits](/docs/infrastructure/sandbox/behavior). | | `.egress(...)` | `{ mode: "deny" \| "allow" }` | Yes | Outbound network policy. `allowDomains` is refused on every platform. | | `.session(...)` | `{ idleSuspendSeconds?, maxLifetimeSeconds? }` | Yes | Required, but both fields are optional — `.session({})` deploys anywhere. `idleSuspendSeconds` needs `suspendResume` (AWS only); `maxLifetimeSeconds` needs `sessionLifetime` (AWS and Kubernetes). | | `.limits(...)` | `{ cpu, memory, disk, maxProcesses? }` | No | Enforced ceilings. Needs `enforcedLimits`; `maxProcesses` needs `processLimit`. | | `.previewPorts(...)` | `number[]` | No | Ports eligible for a preview capability. A port not listed here can never be exposed, so an application cannot widen its own ingress. | Omitting `.limits(...)` takes the platform's defaults. Naming it on a platform that cannot enforce it is rejected at plan time rather than accepted and ignored. ## Two things that bite if you skim [#two-things-that-bite-if-you-skim] **GCP sandboxes are single-turn.** A session id is scoped to one Cloud Run instance, and session affinity was measured keeping **2 of 100** five-turn conversations. That is the absence of a reconnect guarantee, not a weak one. If your agent needs the same sandbox across turns, GCP is the wrong platform — see [Behavior & Limits](/docs/infrastructure/sandbox/behavior). **Limits are ceilings, not requests.** The platform enforces them and your stack is validated against them when it is planned, which is why a GCP Sandbox declaring limits fails at plan time. # Pricing (/docs/infrastructure/sandbox/pricing) Alien provisions Sandboxes in your customer's cloud. The customer pays the cloud provider for the underlying compute, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Prices shown for US East regions. Last verified: August 2026.* Sandbox cost varies by more than an order of magnitude across platforms, and the differences are structural rather than a matter of rates. This page names the charges that do not appear on the line you expect. ## AWS [#aws] Per-second compute while a MicroVM runs, on the image's **baseline** memory. A running MicroVM auto-scales to 4x baseline, and the memory and vCPU above baseline are **billed for the duration they are used**. Size on baseline and you can be charged up to four times it. Then the parts that are easy to miss. These snapshots are internal — the build-time image snapshot and suspend/resume state, not a user-callable feature; `snapshot()` is unavailable on every platform. * **Snapshot storage**, continuous. * **Snapshot read** on every start and resume. * **Snapshot write** on every suspend. * **A one-week minimum retention on image storage.** An image you create and delete an hour later still bills for a week. That matters if you roll images often — each roll starts its own week, and an image that fails to delete keeps billing whether or not you noticed. ## GCP [#gcp] **No additional charge.** Sandboxes run as subprocesses inside the hosting Cloud Run service's existing CPU and memory. The cost is the headroom you provision on that service. This is the cheapest option by a wide margin and the reason GCP is worth considering at all, given how much it cannot do — no reconnection, no preview, no enforced limits. Size the hosting service for your app plus its concurrent sandboxes; nothing else appears on the bill. ## Azure [#azure] Container Apps Sandboxes bill **per second, only while a sandbox runs**, on the Consumption plan's vCPU and memory rates across resource tiers from XS to XL. Stopped sandboxes accrue no CPU or memory charge, and the sandbox group itself is free — it carries no SKU and no capacity. Snapshots are free during the preview and move to Blob Storage rates afterwards. This is the most forgiving shape for short, frequent turns of any platform that charges at all. Two caveats: Sandboxes are in **public preview**, so the rates can move before GA; and the scale-to-zero claim is Microsoft's published word rather than something measured here. Azure also offers Container Apps *dynamic sessions* (`Microsoft.App/sessionPools`), which bill very differently — custom-container pools run on dedicated nodes sized by `nodeCount`, with a five-minute minimum idle timeout. Alien provisions `Microsoft.App/sandboxGroups`, not session pools, so that cost model does not apply here. ## Kubernetes [#kubernetes] The cluster and its gVisor node pool, standing. Alien adds a **warm pool of idle pods**, because cold start measured 79s against 2.7s warm and 79s per agent turn is unusable — those idle pods occupy nodes you are already paying for. ## Local [#local] Your machine. ## What a per-turn agent workload actually costs [#what-a-per-turn-agent-workload-actually-costs] Take a 20-second turn — the shape of an agent running generated code. | Turns/hour | AWS | Azure | GCP | Kubernetes | | ---------- | ----------------------------- | --------------------------------- | ---------------------------------- | -------------------- | | 10 | \~200s compute + snapshot I/O | \~200s of sandbox runtime | headroom only | pool, standing | | 100 | \~2,000s + snapshot I/O | \~2,000s of sandbox runtime | headroom only | pool, standing | | 1000 | \~20,000s + snapshot I/O | \~20,000s, concurrency permitting | headroom only, until CPU saturates | pool, plus scale-out | Read the table as **turns**, not sessions. One conversation reusing a session across many turns costs about one session's runtime, not one session per turn. The numbers above assume each turn's work runs back to back; concurrent conversations multiply them. Both per-second platforms track execution rather than readiness, so the shape of your workload matters less than it looks. AWS is predictable, but the snapshot I/O and the one-week image retention are real and neither shows up where you look first. On Azure the sandbox group is free and stopped sandboxes cost nothing, which suits bursty turns — at the price of the thinnest capability set of any platform: no file transfer, no enforced ceilings, no enforced `deny`. GCP is close to free and correspondingly limited. If your workload fits inside a single turn, that trade is very good. If it does not, GCP is not cheaper — it is unusable, and cost is the wrong axis to have chosen it on. ## Sources [#sources] * [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/) — MicroVM compute, snapshot I/O, image retention * [Azure Container Apps Pricing](https://azure.microsoft.com/en-us/pricing/details/container-apps/) — Sandboxes follow Consumption-plan rates * [Azure Container Apps Sandboxes overview](https://learn.microsoft.com/en-us/azure/container-apps/sandboxes-overview) * [Google Cloud Run Pricing](https://cloud.google.com/run/pricing) — the hosting service a GCP sandbox runs inside # API Reference (/docs/infrastructure/storage/api) Get a handle with `storage(name)` from `@alienplatform/sdk` (TypeScript) or `alien_bindings::Bindings::from_env()?.storage(name).await?` (Rust). Constructing the TypeScript handle does no I/O — the resource is resolved on the first operation. ## get [#get] Retrieves an object by path. ```typescript const { data, meta, attributes } = await storage.get(path) const json = JSON.parse(data.toString("utf8")) // data: Buffer console.log(meta.size, meta.eTag) // ObjectMeta console.log(attributes.contentType) // StorageObjectAttributes ``` ```rust let result = storage.get(&path.into()).await?; let meta = result.meta.clone(); let attributes = result.attributes.clone(); let bytes = result.bytes().await?; // Range read (Rust only) let range_bytes = storage.get_range(&path.into(), 0..1024).await?; ``` | Parameter | Type | Required | Description | | --------- | -------- | -------- | ----------------------------------- | | `path` | `string` | Yes | Object path (key). Max 1,024 bytes. | **Returns:** `StorageGetResult` — `{ data, meta, attributes }`. `data` is the object bytes (`Buffer`), `meta` is the object's [`ObjectMeta`](#objectmeta), and `attributes` are its stored [`StorageObjectAttributes`](#storageobjectattributes) (content type, cache control, custom metadata, …). **Errors:** * Object not found — throws with a not-found error. * `STORAGE_OPERATION_FAILED` — backend error (retryable). *** ## put [#put] Stores an object. Overwrites if the path already exists. ```typescript // data: Buffer | Uint8Array — encode strings/JSON yourself const { eTag, version } = await storage.put( "config.json", Buffer.from(JSON.stringify(config)), ) // Store object attributes alongside the bytes await storage.put("reports/q1.pdf", pdfBytes, { attributes: { contentType: "application/pdf", contentDisposition: 'attachment; filename="q1.pdf"', cacheControl: "public, max-age=3600", metadata: { quarter: "q1", schema: "report-v1" }, }, }) ``` ```rust storage.put(&path.into(), payload).await?; // Attributes and conditional writes via put_opts (object_store PutOptions) storage.put_opts(&path.into(), payload, PutOptions { mode: PutMode::Create, attributes: Attributes::from_iter([ (Attribute::ContentType, "application/pdf".into()), ]), ..Default::default() }).await?; ``` | Parameter | Type | Required | Description | | -------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `path` | `string` | Yes | Object path (key). Max 1,024 bytes. | | `data` | `Buffer \| Uint8Array` | Yes | Object bytes. | | `options.attributes` | `StoragePutAttributes` | No | Provider-neutral attributes to store with the object: `contentType`, `contentDisposition`, `contentEncoding`, `contentLanguage`, `cacheControl`, `metadata`. | **Returns:** `StoragePutResult` — `{ eTag?, version? }`, the provider identifiers for the written object when the backend reports them. Attribute-bearing writes are rejected before any payload is written on backends that can't persist them: the local filesystem backend rejects all attributes, and GCS rejects `contentEncoding: "gzip"` (its decompressive transcoding breaks byte-exact reads; other encodings like `br` work). See [Behavior & Limits](/docs/infrastructure/storage/behavior). *** ## delete [#delete] Deletes an object. Deleting a non-existent object is a no-op. ```typescript await storage.delete(path) ``` ```rust storage.delete(&path.into()).await?; ``` *** ## list [#list] Lists objects under a prefix. ```typescript // Resolves to an array of object metadata const objects = await storage.list("reports/") for (const obj of objects) { console.log(obj.location, obj.size, obj.lastModified) } // List everything const all = await storage.list() ``` ```rust // Streams object metadata let mut stream = storage.list(Some(&"reports/".into())); while let Some(meta) = stream.next().await { let meta = meta?; println!("{} {}", meta.location, meta.size); } ``` | Parameter | Type | Required | Description | | --------- | -------- | -------- | ------------------------------------------------------- | | `prefix` | `string` | No | Only return objects whose path starts with this prefix. | **Returns:** `ObjectMeta[]` in TypeScript — `{ location, size, lastModified, eTag?, version? }`. Rust streams `ObjectMeta` and also offers `list_with_delimiter` for directory-style browsing. `list()` is metadata-only — cloud list APIs don't return object attributes. Use [`head`](#head) to read a specific object's attributes. *** ## head [#head] Returns object metadata and attributes without downloading the object body. ```typescript const { meta, attributes } = await storage.head(path) console.log(meta.size, meta.eTag, attributes.contentType) ``` ```rust let meta = storage.head(&path.into()).await?; ``` **Returns:** `StorageHeadResult` — `{ meta, attributes }`, the same [`ObjectMeta`](#objectmeta) and [`StorageObjectAttributes`](#storageobjectattributes) as `get`, without the payload. **Errors:** throws if the object does not exist. *** ## copy [#copy] Copies an object from one path to another within the same storage resource. ```typescript await storage.copy(from, to) ``` ```rust storage.copy(&from.into(), &to.into()).await?; // Rust also exposes rename and conditional variants storage.rename(&from.into(), &to.into()).await?; storage.copy_if_not_exists(&from.into(), &to.into()).await?; ``` *** ## signedUrl [#signedurl] Generates a time-limited presigned request for direct client access. The result is a full request description — URL plus the method and headers to replay it with — so it works on every provider, including local stores. ```typescript const req = await storage.signedUrl({ method: "GET", // "GET" | "PUT" | "DELETE" path: "reports/q1.json", expiresIn: 3600, // seconds }) // req: { url, method, headers } ``` ```rust let req = storage.presigned_get(&path.into(), Duration::from_secs(3600)).await?; // Also: presigned_put, presigned_delete ``` | Field | Type | Required | Description | | ----------- | ---------------------------- | -------- | ----------------------------------------------- | | `method` | `"GET" \| "PUT" \| "DELETE"` | Yes | The operation the presigned request authorizes. | | `path` | `string` | Yes | Object path. | | `expiresIn` | `number` | Yes | Validity window, in seconds. | **Returns:** `PresignedRequest` — `{ url: string, method: string, headers: Record }` In Rust, use `presigned_get`, `presigned_put`, and `presigned_delete`: ```rust let request = storage.presigned_get(&path.into(), Duration::from_secs(3600)).await?; ``` *** ## Advanced operations (Rust) [#advanced-operations-rust] The Rust `Storage` handle also implements the `object_store` `ObjectStore` trait, which adds range reads, conditional writes, streaming lists, delimiter listing, and rename: ```rust // Range read let chunk = storage.get_range(&path.into(), 0..1024).await?; // Conditional write — only if the path doesn't already exist storage.put_opts(&path.into(), payload, PutOptions { mode: PutMode::Create, ..Default::default() }).await?; // Directory-style listing let result = storage.list_with_delimiter(Some(&"reports/".into())).await?; // Move an object (copy + delete) storage.rename(&from.into(), &to.into()).await?; ``` The Rust handle also exposes conditional `copy_if_not_exists` / `rename_if_not_exists` and the binding introspection helpers `get_base_dir()` / `get_url()`. The TypeScript handle is deliberately minimal; use these from Rust, or use a native cloud SDK for provider-specific features. *** ## Types [#types] ### ObjectMeta [#objectmeta] ```typescript interface ObjectMeta { location: string // Object path within the store size: number // Object size in bytes lastModified: string // Last-modified timestamp (RFC 3339) eTag?: string // Provider entity tag, when available version?: string // Provider object version, when available } ``` ### StorageObjectAttributes [#storageobjectattributes] Provider-neutral attributes returned with a stored object by `get` and `head`. ```typescript interface StorageObjectAttributes { contentType?: string // Stored MIME type contentDisposition?: string // Stored browser content-disposition behavior contentEncoding?: string // Stored content encoding contentLanguage?: string // Stored content language cacheControl?: string // Stored cache-control policy storageClass?: string // Provider storage class, when reported metadata: Record // User-defined object metadata } ``` ### StoragePutAttributes [#storageputattributes] Attributes accepted by `put` under `options.attributes` — the writable subset of `StorageObjectAttributes` (`storageClass` is read-only). ```typescript interface StoragePutAttributes { contentType?: string contentDisposition?: string contentEncoding?: string // GCS rejects "gzip" contentLanguage?: string cacheControl?: string metadata?: Record } ``` ### StorageGetResult / StorageHeadResult / StoragePutResult [#storagegetresult--storageheadresult--storageputresult] ```typescript interface StorageGetResult { data: Buffer meta: ObjectMeta attributes: StorageObjectAttributes } interface StorageHeadResult { meta: ObjectMeta attributes: StorageObjectAttributes } interface StoragePutResult { eTag?: string version?: string } ``` ### PresignedRequest [#presignedrequest] ```typescript interface PresignedRequest { url: string // The presigned URL method: string // HTTP method to replay it with headers: Record // Headers to include } ``` # Behavior & Limits (/docs/infrastructure/storage/behavior) Alien's Storage binding is built on the [Apache Arrow `object_store`](https://github.com/apache/arrow-rs-object-store) crate — the same library used by DataFusion, Delta Lake, and the wider Arrow ecosystem. ## Guarantees [#guarantees] On cloud platforms (AWS, GCP, Azure), Alien provisions and manages the storage backing service. These guarantees apply: **Atomic Writes.** A `put()` call either succeeds completely or fails completely. No partial writes are visible to other readers. **Strong Read-After-Write.** After a successful `put()`, any subsequent `get()` returns the new data immediately. All three cloud providers guarantee this. **Strong List Consistency.** After a `put()` or `delete()`, `list()` reflects the change immediately. **Durability.** AWS S3 and GCP Cloud Storage: 99.999999999% (11 nines). Azure Blob Storage: 12+ nines with zone-redundant storage. **Conditional Operations.** Conditional creates are atomic — if two concurrent callers create the same key, exactly one succeeds. Backed by `object_store`'s `PutMode::Create` (Rust `put_opts`). **Conditional Updates (ETag).** Compare-and-swap via ETags — update an object only if it hasn't changed since you last read it. ## Limits [#limits] | Limit | Value | | ---------------------- | ---------------------------------------------------------------------- | | Max object size | 5 TB (multipart required above 5 GB) | | Max key (path) length | 1,024 bytes (UTF-8) | | Path segments | Max 255 bytes each | | Path charset | UTF-8, no control chars, no `.`/`..` segments, no leading/trailing `/` | | Multipart minimum part | 5 MiB (except last part) | Alien does not impose rate limits — backend rate limits apply (see platform details). ## Operation Semantics [#operation-semantics] These come from the `object_store` crate and hold across all backends: **List ordering.** `list()` does **not** guarantee ordering. Sort on the client if needed. **List prefix matching.** Segment-based: prefix `"foo/bar"` matches `"foo/bar/x.json"` but **not** `"foo/bar_baz/x.json"`. **Copy and rename.** `copy()` is atomic when the backend supports it. The Rust-only `rename()` is copy + delete — **not atomic**. **Multipart uploads** (Rust only). Parts uploaded concurrently in any order. Finalized atomically on `complete()`. Abandoned uploads cleaned up by the backend. **Object attributes.** `put()` can store provider-neutral attributes (content type, content disposition, content encoding, content language, cache control, custom metadata); `get()` and `head()` return them, plus the provider-reported storage class. Writes with unsupported attributes are rejected *before* any payload is written — the operation fails cleanly rather than storing an object with attributes silently dropped. `list()` is metadata-only: cloud list APIs don't return attributes. **ETags and versions.** `ObjectMeta` carries the provider `eTag` and `version` when available, and `put()` returns them for the written object — the basis for compare-and-swap flows. ## Platform Details [#platform-details] ### AWS (S3) [#aws-s3] * Request rates auto-scale per prefix. No ramp-up required. * Baseline: 5,500 GET/HEAD + 3,500 PUT/DELETE per second per prefix. * Versioning and lifecycle rules fully supported via stack configuration. ### GCP (Cloud Storage) [#gcp-cloud-storage] * Baseline: \~5,000 reads/sec + \~1,000 writes/sec per bucket. * Must ramp up gradually (double every 20 minutes) for sustained high load. * IAM/ACL changes are eventually consistent (\~1 minute). * **`contentEncoding: "gzip"` is rejected** before upload: GCS decompressive transcoding omits the response length required for byte-exact reads. Other encodings (e.g. `br`) are preserved. ### Azure (Blob Storage) [#azure-blob-storage] * Per-blob limit: 500 requests/sec, 60 MB/s throughput. * **Versioning not supported** per container. The `versioning` stack option is ignored with a warning. * **Lifecycle rules not supported** per container. The `lifecycle` stack option is ignored. * Requires a Storage Account (provisioned automatically by Alien). ### Kubernetes / On-Prem [#kubernetes--on-prem] Storage is **not provisioned by Alien** on Kubernetes. The cluster operator provides the backing service (S3, MinIO, GCS, etc.) via Helm values. Guarantees depend entirely on the backing service. ### Local [#local] * Filesystem-backed directory. Durability depends on OS fsync behavior. * Deleting a non-existent key returns a not-found error (unlike cloud platforms where it's a no-op). * **Object attributes are not supported** — the filesystem cannot persist them, so attribute-bearing `put()` calls are rejected before any payload is written. ## Triggers [#triggers] | Platform | Storage Triggers | | -------------------- | --------------------------- | | AWS | S3 event notifications | | GCP | Cloud Storage notifications | | Azure | Dapr blob storage binding | | Kubernetes / On-Prem | Not currently supported | | Local | LocalTriggerService | ## Design Decisions [#design-decisions] **Built on `object_store`.** Battle-tested foundation rather than a custom abstraction. Same library used by DataFusion and Delta Lake. **No Alien-level rate limiting.** Cloud provider native rate limits apply. AWS auto-scales, GCP requires ramp-up — we document the difference rather than hiding it. **Permissive key charset.** Unlike KV, Storage allows any valid UTF-8 key. Matches developer expectations for file paths. # Overview (/docs/infrastructure/storage) Storage lets you store and retrieve files, blobs, and objects of any size — from small JSON documents to multi-gigabyte datasets. Objects are organized by key (path) and accessed via a simple put/get/list/delete API. ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | ------------------------------- | ---------------- | | AWS | Amazon S3 | Alien | | GCP | Google Cloud Storage | Alien | | Azure | Azure Blob Storage | Alien | | Kubernetes / On-Prem | External (S3, MinIO, GCS, etc.) | Cluster operator | | Local | Filesystem directory | Alien | On Kubernetes / on-prem, Storage is not provisioned by Alien. The cluster operator provides the external storage service and configures it via Helm values. ## When to Use [#when-to-use] Use Storage for files, blobs, and large objects — uploads, reports, generated artifacts, backups, static assets. Objects can be any size up to 5 TB. Don't use Storage as a database. For key-based lookups with TTL and atomic operations, use [KV](/docs/infrastructure/kv). ## Stack Definition [#stack-definition] Declare a Storage resource in your `alien.ts`: ```typescript const data = new alien.Storage("data") .publicRead(false) .versioning(false) .lifecycleRules([{ days: 90 }]) .build() ``` | Method | Type | Default | Description | | ------------------------ | ----------------- | -------- | ----------------------------------------------------------------------------------- | | `id` (constructor) | `string` | required | Resource identifier. Dot-separated labels, each ≤ 63 chars. | | `.publicRead(value)` | `boolean` | `false` | Allow public read access without authentication. | | `.versioning(value)` | `boolean` | `false` | Enable object versioning. Not supported on Azure. | | `.lifecycleRules(rules)` | `LifecycleRule[]` | `[]` | Auto-delete objects after `days`. Optional `prefix` filter. Not supported on Azure. | ## Quick Start [#quick-start] Use it in your application: ```typescript import { storage } from "@alienplatform/sdk" const data = storage("data") await data.put("reports/q1.json", Buffer.from(JSON.stringify(report))) const { data: bytes } = await data.get("reports/q1.json") ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let data = bindings.storage("data").await?; data.put(&"reports/q1.json".into(), bytes).await?; let obj = data.get(&"reports/q1.json".into()).await?; ``` ## Core Operations [#core-operations] ### Store an Object [#store-an-object] ```typescript // put() takes bytes — encode strings/JSON yourself await data.put("config.json", Buffer.from(JSON.stringify({ version: 2 }))) // Binary data, with object attributes and custom metadata await data.put("image.png", imageBytes, { attributes: { contentType: "image/png", cacheControl: "public, max-age=86400", metadata: { source: "avatar-upload" }, }, }) ``` ```rust // Bytes data.put(&"config.json".into(), bytes).await?; // Conditional write (Create mode) data.put_opts(&"lock.json".into(), payload, PutOptions { mode: PutMode::Create, ..Default::default() }).await?; ``` ### Retrieve an Object [#retrieve-an-object] ```typescript const object = await data.get("reports/q1.json") const json = JSON.parse(object.data.toString("utf8")) // data: Buffer console.log(object.meta.eTag, object.attributes.contentType) // Metadata and attributes without the body const { meta, attributes } = await data.head("reports/q1.json") ``` ```rust let result = data.get(&"reports/q1.json".into()).await?; let bytes = result.bytes().await?; // Range read let range_bytes = data.get_range(&"large-file.bin".into(), 0..1024).await?; ``` ### List Objects [#list-objects] ```typescript // list() resolves to an array of object metadata for (const obj of await data.list("reports/")) { console.log(obj.location, obj.size) } ``` ### Delete and Copy [#delete-and-copy] ```typescript await data.delete("reports/old.json") // no-op if not found await data.copy("reports/q1.json", "archive/q1.json") ``` ### Presigned Requests [#presigned-requests] Generate time-limited presigned requests for direct client access: ```typescript const req = await data.signedUrl({ method: "GET", path: "reports/q1.json", expiresIn: 3600, }) // req: { url, method, headers } — replay it from the client ``` ## Triggers [#triggers] Storage events can trigger workers when objects are created or deleted: ```typescript import { onStorageEvent } from "@alienplatform/sdk" onStorageEvent("data", async (event) => { console.log(event.eventType, event.objectKey) // "created", "reports/q1.json" }, { prefix: "reports/" }) ``` See [Behavior & Limits](/docs/infrastructure/storage/behavior) for trigger support per platform. # Pricing (/docs/infrastructure/storage/pricing) Alien provisions Storage in your customer's cloud. The customer pays the cloud provider for the underlying storage, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Prices shown for US East regions. Last verified: April 2026.* ## Monthly Cost Breakdown [#monthly-cost-breakdown] ### Storage [#storage] | Provider | Service | Price per GB/month | Notes | | -------- | ---------------------- | ------------------ | ------------------------------------ | | AWS | S3 Standard | $0.023 | First 50 TB. $0.022 for next 450 TB. | | GCP | Cloud Storage Standard | $0.020 | | | Azure | Blob Storage Hot (LRS) | $0.018 | Locally redundant. ZRS is $0.023. | ### Requests [#requests] | Provider | PUT/POST/LIST (per 1,000) | GET/HEAD (per 1,000) | | ------------------ | ------------------------- | -------------------- | | AWS S3 | $0.005 | $0.0004 | | GCP Cloud Storage | $0.005 (Class A) | $0.0004 (Class B) | | Azure Blob Storage | $0.005 | $0.0004 | ### Data Transfer [#data-transfer] Ingress (upload) is free on all providers. Egress pricing varies: | Provider | First 100 GB/month | Up to 10 TB/month | | -------- | ------------------ | ----------------- | | AWS | Free (to internet) | $0.09/GB | | GCP | Free (to internet) | $0.12/GB | | Azure | Free (to internet) | $0.087/GB | ## Example: 100 GB Storage + 1M Reads + 100K Writes/month [#example-100-gb-storage--1m-reads--100k-writesmonth] | Provider | Storage | Reads | Writes | **Total** | | ---------- | ------- | ----- | ------ | --------- | | AWS S3 | $2.30 | $0.40 | $0.50 | **$3.20** | | GCP GCS | $2.00 | $0.40 | $0.50 | **$2.90** | | Azure Blob | $1.80 | $0.40 | $0.50 | **$2.70** | ## Free Tiers [#free-tiers] * **AWS S3**: 5 GB storage, 20,000 GET, 2,000 PUT requests/month (12 months). * **GCP**: 5 GB storage, 50,000 Class A, 5,000 Class B ops/month (always free). * **Azure**: 5 GB LRS hot storage (12 months). ## Sources [#sources] * [AWS S3 Pricing](https://aws.amazon.com/s3/pricing/) * [GCP Cloud Storage Pricing](https://cloud.google.com/storage/pricing) * [Azure Blob Storage Pricing](https://azure.microsoft.com/en-us/pricing/details/storage/blobs/) # API Reference (/docs/infrastructure/vault/api) Get a handle with `vault(name)` from `@alienplatform/sdk` (TypeScript) or `alien_bindings::Bindings::from_env()?.vault(name).await?` (Rust). ## get [#get] Retrieves a secret by name. Throws if the secret does not exist. ```typescript const value: string = await vault.get(name) const config: T = await vault.getJson(name) ``` ```rust let value: String = vault.get_secret(name).await?; ``` | Parameter | Type | Required | Description | | --------- | -------- | -------- | ---------------- | | `name` | `string` | Yes | The secret name. | *** ## put / putJson [#put--putjson] Creates or updates a secret (upsert). ```typescript await vault.put(name, "sk_live_abc123") await vault.putJson(name, { retryCount: 3 }) // JSON-serialized ``` ```rust vault.set_secret(name, "sk_live_abc123").await?; ``` | Parameter | Type | Required | Description | | --------- | ---------------------------------- | -------- | ---------------- | | `name` | `string` | Yes | The secret name. | | `value` | `string` (`put`) / any (`putJson`) | Yes | Max 25 KB. | *** ## delete [#delete] Deletes a secret. ```typescript await vault.delete(name) ``` ```rust vault.delete_secret(name).await?; ``` *** ## list [#list] Lists the names of all secrets in the vault. ```typescript const names: string[] = await vault.list() ``` ```rust let names: Vec = vault.list_secrets().await?; ``` # Behavior & Limits (/docs/infrastructure/vault/behavior) ## Guarantees [#guarantees] On cloud platforms (AWS, GCP, Azure), Alien provisions and manages the vault backing service. These guarantees apply: **Encryption at Rest.** All cloud platforms encrypt secrets with managed keys. AWS uses SSM's KMS encryption, GCP uses Google-managed encryption, Azure uses Key Vault's built-in encryption. **Encryption in Transit.** All communication uses TLS. **Upsert Semantics.** `put()` / `putJson()` create the secret if it doesn't exist, or update it if it does. **Get Fails on Missing.** `get()` throws an error if the secret does not exist. Handle the error, or check `list()` first when probing. **Customer Vault Privacy.** User-declared vaults do not grant the Alien management identity data read or write access by default. The management identity can operate the internal `secrets` vault, and it can access a user vault only when the stack explicitly extends management permissions for that vault. ## Limits [#limits] | Limit | Value | | --------------------- | ---------------------------------------------------- | | Max secret value size | 25 KB (Azure Key Vault limit; AWS/GCP support 64 KB) | | Secret name charset | Alphanumeric, `-`, `_` | ## Platform Details [#platform-details] ### AWS (SSM Parameter Store) [#aws-ssm-parameter-store] * SecureString parameters. No infrastructure created — Parameter Store is always available. * Naming: `{stackPrefix}-{vaultName}-{secretName}`. * Resource-scoped IAM uses the full vault prefix, so access to one vault does not imply access to another vault with the same stack prefix. * Read: 10,000 `GetParameter`/second. Write: avoid sustained writes more than once per 10 minutes per parameter. * Max value: 64 KB. ### GCP (Secret Manager) [#gcp-secret-manager] * Versioned secrets — each `put()` / `putJson()` call creates a new version. * Requires API enablement (handled during provisioning). * Secret creation is project-scoped in GCP IAM, so Alien uses IAM Conditions on the secret resource name prefix for vault-scoped read and write access. * Read: 90,000 access requests/minute/project. * Max value: 64 KiB. ### Azure (Key Vault) [#azure-key-vault] * Actual Azure resource provisioned by Alien. * Data-plane access is granted through Azure RBAC on the specific Key Vault resource. * Read: 4,000 GET/10 seconds. Write: 300 CREATE/10 seconds. Returns HTTP 429 when exceeded. * Max value: **25 KB** — smallest of all platforms. ### Kubernetes / On-Prem [#kubernetes--on-prem] Depends on Helm configuration: **Kubernetes Secrets (default):** Stored as native K8s Secrets. Created on-demand. No versioning. Encryption at rest depends on the cluster's etcd encryption configuration — Alien does not control this. **External vault (HashiCorp Vault, cloud KMS):** Behavior depends entirely on the external service. ### Local [#local] * Secrets stored as **plaintext JSON** files on disk. No encryption. * File-based with read-modify-write pattern. ## Design Decisions [#design-decisions] **25 KB cross-platform limit.** Azure Key Vault sets the floor. Alien documents this as the portable limit. **Simple CRUD API.** Advanced features (versioning, rotation, audit) vary too much across providers. Use the native SDK via [Direct Access](/docs/resource-apis) for those. # Overview (/docs/infrastructure/vault) Vault provides encrypted secret storage — store API keys, database credentials, and sensitive configuration that your application reads at runtime. Secrets are encrypted at rest and transmitted over TLS on all cloud platforms. ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | -------------------------------------------------- | ----------------- | | AWS | AWS Systems Manager Parameter Store (SecureString) | Alien (implicit) | | GCP | Google Secret Manager | Alien (implicit) | | Azure | Azure Key Vault | Alien | | Kubernetes / On-Prem | Kubernetes Secrets | Alien (on-demand) | | Local | Plaintext JSON files | Alien | On AWS and GCP, Vault uses services that exist by default — no new infrastructure is created. On Azure, Alien provisions a Key Vault resource. On Kubernetes / on-prem, secrets are created in the namespace on-demand. ## Management Access [#management-access] Vaults that you declare in `alien.ts` are customer-managed by default. Alien can provision the vault resource and give your runtime identity the permissions you declare, but the management identity does not get secret read or write access to those vaults unless you explicitly grant it. Alien also creates an internal `secrets` vault for deployment environment secrets. That vault is Alien-managed, and the management identity can write and read it so per-deployment secret environment variables can be synced. If you want the management identity to write a user-declared vault, extend management permissions deliberately: ```typescript export default new alien.Stack("my-app") .add(customerSecrets, "frozen") .permissions({ management: { extend: { "customer-secrets": ["vault/data-write"], }, }, }) .build() ``` Use `vault/data-read` only when the management identity must read secret values back. `vault/data-write` allows creating, updating, and deleting secrets but does not include value-read permission. ## When to Use [#when-to-use] Use Vault for secrets your application needs at runtime — API keys, database credentials, encryption keys, third-party tokens. Don't use Vault for non-sensitive configuration (use environment variables) or for large data (vault values are limited to 25 KB). ## Stack Definition [#stack-definition] Declare a Vault resource in your `alien.ts`: ```typescript const secrets = new alien.Vault("app-secrets").build() ``` | Parameter | Type | Description | | --------- | -------- | -------------------------------------------------------- | | `id` | `string` | Resource identifier. `[A-Za-z0-9-_]`, max 64 characters. | Vault has no additional configuration options. The backing service (SSM, Secret Manager, Key Vault) is determined by the deployment platform. ## Quick Start [#quick-start] ```typescript import { vault } from "@alienplatform/sdk" const secrets = vault("app-secrets") const apiKey = await secrets.get("STRIPE_API_KEY") await secrets.put("API_KEY", "sk_live_abc123") ``` ```rust let bindings = alien_bindings::Bindings::from_env()?; let secrets = bindings.vault("app-secrets").await?; let api_key = secrets.get_secret("STRIPE_API_KEY").await?; secrets.set_secret("API_KEY", "sk_live_abc123").await?; ``` ## Stack Secrets vs. Vault [#stack-secrets-vs-vault] | Feature | Stack secrets (env vars) | Vault | | --------- | ------------------------ | ----------------------------- | | Set by | Stack definition | Application code at runtime | | Read by | Environment variable | SDK call | | Lifecycle | Tied to deployment | Independent | | Use case | Static config | Dynamic credentials, rotation | # Pricing (/docs/infrastructure/vault/pricing) Alien provisions Vault in your customer's cloud. The customer pays the cloud provider for the underlying secret store, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Prices shown for US East regions. Last verified: April 2026.* ## Pricing [#pricing] | Provider | Service | API calls | Per-secret cost | Storage | | -------- | ------------------- | --------------------------------- | ------------------------------ | ------- | | AWS | SSM Parameter Store | $0.05 per 10K (higher throughput) | Free (standard params) | Free | | GCP | Secret Manager | $0.03 per 10K access ops | $0.06 per active version/month | Free | | Azure | Key Vault | $0.03 per 10K operations | Free | Free | ## Example: 10 Secrets + 100K API Calls/month [#example-10-secrets--100k-api-callsmonth] | Provider | API calls | Secrets | **Total** | | ------------------ | --------- | ------- | --------- | | AWS SSM | $0.50 | $0.00 | **$0.50** | | GCP Secret Manager | $0.30 | $0.60 | **$0.90** | | Azure Key Vault | $0.30 | $0.00 | **$0.30** | ## Platform Notes [#platform-notes] * **AWS SSM Parameter Store**: Standard parameters with standard throughput are free (up to 10K params). Higher throughput costs $0.05 per 10K API calls. Advanced parameters cost $0.05/param/month. * **GCP Secret Manager**: Charges per active secret version. Each `put()` / `putJson()` call creates a new version. Avoid frequent updates to the same secret. * **Azure Key Vault**: Flat per-operation pricing. Rate limited to 4,000 GET/10sec and 300 CREATE/10sec — returns HTTP 429 when exceeded. ## Free Tiers [#free-tiers] * **AWS SSM**: Standard parameters with standard throughput are always free. * **GCP Secret Manager**: 6 active secret versions free, 10K access operations free/month. * **Azure Key Vault**: No free tier. ## Sources [#sources] * [AWS SSM Pricing](https://aws.amazon.com/systems-manager/pricing/) * [GCP Secret Manager Pricing](https://cloud.google.com/secret-manager/pricing) * [Azure Key Vault Pricing](https://azure.microsoft.com/en-us/pricing/details/key-vault/) # Behavior & Limits (/docs/infrastructure/worker/behavior) ## Guarantees [#guarantees] **Stateless Execution.** Each worker invocation runs in an isolated environment. Do not rely on in-memory state persisting between invocations — use [KV](/docs/infrastructure/kv) or [Storage](/docs/infrastructure/storage) for persistent state. **At-Least-Once Invocation (Triggers).** Queue and storage triggers deliver events at least once. Your handler must be idempotent — the same event may be delivered more than once. **Automatic Scaling.** Workers scale up with incoming requests and scale to zero when idle. Alien manages instance counts; you can optionally cap concurrent execution with `.concurrencyLimit()`. **Binding Access.** Workers can only access resources that are explicitly `link()`ed in the stack definition. This is enforced at the infrastructure level via IAM/RBAC. ## Limits [#limits] | Limit | Value | Notes | | ------------------------- | ------------------ | -------------------------------------------------------------------------------------- | | Max request body | Platform-dependent | Lambda: 6 MB sync, 256 KB async. Cloud Run: 32 MB. Container Apps: varies. | | Max response body | Platform-dependent | Lambda: 6 MB. Cloud Run: 32 MB. | | Max execution time | Platform-dependent | Lambda: 15 min. Cloud Run: 60 min. Container Apps: varies. | | Queue triggers per worker | 1 | A worker can be triggered by at most one queue. | | Concurrent invocations | Platform-dependent | Lambda: 1,000 default (requestable). Cloud Run: per-instance concurrency configurable. | ## Platform Details [#platform-details] ### AWS (Lambda) [#aws-lambda] * Runtime: Container image on ARM64 (Graviton) for better price-performance. * Cold starts: typically 1-3 seconds for the first invocation after idle. * Payload limits: 6 MB synchronous, 256 KB asynchronous invocation. * Max execution time: 15 minutes. * Queue triggers: SQS event source mapping. One message per invocation. Visibility timeout is auto-calculated: `max(30s, min(12h, worker_timeout × 6))`. * If the worker's ECR image is in a different region than the worker, Alien automatically handles cross-region image rewriting. ### GCP (Cloud Run) [#gcp-cloud-run] * Runtime: Container image. * Cold starts: typically 1-2 seconds. * Payload limits: 32 MB request/response. * Max execution time: 60 minutes. * Queue triggers: Pub/Sub push subscription. * Supports per-instance concurrency (multiple requests per container). ### Azure (Container Apps) [#azure-container-apps] * Runtime: Container image. * Queue triggers: Service Bus integration via KEDA. ### Kubernetes / On-Prem [#kubernetes--on-prem] * Runs as a Pod with service discovery via internal DNS. * **Queue triggers are not currently supported on Kubernetes.** ### Local [#local] * Runs as a native process extracted from the built container image. * Dynamic port assignment via `--port` flag. * Automatic restart on crash via background monitor. * Triggers supported via LocalTriggerService. * Full environment variable injection and OTLP telemetry configuration. ## Trigger Support Matrix [#trigger-support-matrix] | Trigger Type | AWS | GCP | Azure | | ---------------- | ---------------- | ----------------- | ------------------------- | | Queue → Worker | SQS event source | Pub/Sub push | Service Bus + KEDA | | Storage → Worker | S3 notifications | GCS notifications | Dapr blob storage binding | | Cron → Worker | EventBridge | Cloud Scheduler | Dapr cron binding | ## Design Decisions [#design-decisions] **One queue source per worker.** A worker can consume from at most one queue, but can have multiple triggers of different types (e.g. a queue trigger + a cron trigger). If you need to consume from multiple queues, create multiple workers. **Concurrency is optional.** By default, Alien lets the cloud provider manage scaling. You can set `.concurrencyLimit()` to cap concurrent executions — this maps to reserved concurrency on Lambda, max instances on Cloud Run, and max replicas on Container Apps. # Environment Variables (/docs/infrastructure/worker/environment-variables) ## Setting Variables [#setting-variables] ```typescript const api = new alien.Worker("api") .environment({ LOG_LEVEL: "info", API_ENDPOINT: "https://api.example.com", }) .build() ``` These are available as regular environment variables at runtime. ## Secrets [#secrets] Credentials like API keys and database passwords are stored in the cloud's vault service (SSM, Secret Manager, Key Vault) — never in logs or config files. They're configured per-deployment when onboarding customers, not in `alien.ts`. Alien syncs per-deployment secret environment variables into an internal vault named `secrets`. That vault is reserved for Alien-managed deployment secrets. For customer-owned secrets that should never be visible to the management identity, declare and link your own [Vault](/docs/infrastructure/vault) resource, then have the customer create secrets directly in their cloud vault service. For secrets your app manages at runtime, use a [Vault](/docs/infrastructure/vault) resource. ## Per-Deployment Overrides [#per-deployment-overrides] Different customers often need different values. Set them when creating the deployment: ```bash alien deployments create --name acme-prod --project my-project \ --deployment-group acme --platform aws \ --env LOG_LEVEL=warn --secret STRIPE_KEY=sk_live_... ``` `--env-targeted` and `--secret-targeted` (`KEY=VALUE:pattern1,pattern2`) scope a variable to specific workers. ## Built-In Variables [#built-in-variables] Alien injects automatically: | Variable | Description | | ----------------------- | ---------------------------------------------- | | `ALIEN_DEPLOYMENT_TYPE` | Platform: `aws`, `gcp`, `azure` | | `ALIEN_{NAME}_BINDING` | Connection parameters for each linked resource | The `ALIEN_*_BINDING` variables are how your code discovers resources. See [Accessing Resources](/docs/resource-apis). # Events & Triggers (/docs/infrastructure/worker/events-and-triggers) Workers can be triggered automatically by events — a message in a queue, a file uploaded to storage, or a cron schedule. ## Queue Triggers [#queue-triggers] The most common trigger. One message = one worker invocation: ```typescript // alien.ts const tasks = new alien.Queue("tasks").build() const worker = new alien.Worker("processor") .trigger({ type: "queue", queue: tasks.ref() }) .build() ``` ```typescript // worker code import { onQueueMessage } from "@alienplatform/sdk" onQueueMessage("tasks", async (message) => { console.log(message.payload) // auto-acknowledged on success, re-delivered on failure }) ``` ## Storage Triggers [#storage-triggers] React to objects being created or deleted: ```typescript import { onStorageEvent } from "@alienplatform/sdk" onStorageEvent("uploads", async (event) => { console.log(event.eventType, event.objectKey) // "created", "photos/image.jpg" }) ``` ## Cron Triggers [#cron-triggers] ```typescript const cleanup = new alien.Worker("cleanup") .trigger({ type: "schedule", cron: "0 0 * * *" }) // daily at midnight .build() ``` ## Multiple Triggers [#multiple-triggers] A worker can have multiple triggers of different types. For example, process queue messages during the day and run a cleanup on a schedule: ```typescript const worker = new alien.Worker("processor") .trigger({ type: "queue", queue: tasks.ref() }) .trigger({ type: "schedule", cron: "0 0 * * *" }) .build() ``` Each trigger independently invokes the worker. Note: a worker can consume from at most **one queue**, but can combine a queue trigger with storage or cron triggers. ## Platform Support [#platform-support] | Trigger | AWS | GCP | Azure | | ------- | ---------------- | ------------------- | ------------------------- | | Queue | SQS → Lambda | Pub/Sub → Cloud Run | Service Bus + KEDA | | Storage | S3 notifications | GCS notifications | Dapr blob storage binding | | Cron | EventBridge | Cloud Scheduler | Dapr cron binding | # Overview (/docs/infrastructure/worker) A Worker is Alien's stateless, event-driven compute resource. It runs as AWS Lambda on AWS, Google Cloud Run on GCP, Azure Container Apps on Azure, or a Deployment and Service on Kubernetes. Use a Worker for HTTP requests, Commands, queue messages, storage events, or scheduled work. Workers scale with load and can scale to zero when idle. ## Platform Mapping [#platform-mapping] | Platform | Backing Service | Provisioned by | | -------------------- | --------------------------- | -------------- | | AWS | AWS Lambda (ARM64/Graviton) | Alien | | GCP | Google Cloud Run | Alien | | Azure | Azure Container Apps | Alien | | Kubernetes / On-Prem | Deployment + Service | Alien Operator | \| Local | Native process | Alien | ## When to Use [#when-to-use] Use Worker for event-driven, stateless compute — HTTP APIs, webhook handlers, background processors, queue consumers, scheduled tasks. Don't use Worker for long-running stateful workloads, persistent WebSocket connections, or services that need internal DNS discovery. ## Quick Start [#quick-start] ```typescript // alien.ts const api = new alien.Worker("api") .code({ type: "source", src: "./api", toolchain: { type: "typescript" } }) .publicEndpoint("api") .permissions("execution") .build() ``` ## Public Endpoints [#public-endpoints] Workers are private by default. Add named public endpoints when the worker should receive HTTPS traffic. ```typescript const api = new alien.Worker("api") .publicEndpoint("api") .build() ``` Use endpoint names for roles such as `"api"`, `"webhooks"`, or `"admin"`. See [External URLs](/docs/external-urls). ## Worker-to-Worker Invocation [#worker-to-worker-invocation] There is no app-facing worker-to-worker binding — worker invocation is a provider-internal mechanism, not something an app calls. To reach another worker, use one of two supported patterns. Both work identically from Rust and TypeScript. **Call the peer's HTTP endpoint.** Give the target worker a named public endpoint and call its URL with an ordinary HTTP client. Resolve the URL from the deployment info API (`GET /v1/deployments/:id/info` returns `publicEndpoints` per resource); see [External URLs](/docs/external-urls). **Send it a command.** For request/response work that doesn't need a public endpoint, target the worker with the commands client. The target handles it with a registered `command()` handler (see [Commands](/docs/commands)): ```typescript import { CommandsClient } from "@alienplatform/commands" const commands = new CommandsClient({ managerUrl, deploymentId, token }) const result = await commands.target("image-processor").invoke("resize", payload) ``` ## Configuration [#configuration] | Method | Default | Description | | ----------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.code(code)` | required | Source code or pre-built image. See [Toolchains](/docs/infrastructure/worker/toolchains). | | `.publicEndpoint(name, options?)` | — | Adds a named HTTPS endpoint. Options: `hostLabel` (host label on the deployment domain, `"@"` for the apex) and `wildcardSubdomains`. Omit for private workers. See [External URLs](/docs/external-urls). | | `.memoryMb(number)` | `512` | Memory allocation. 128–32,768 MB. | | `.timeoutSeconds(number)` | `180` | Max execution time. 1–3,600 seconds. | | `.concurrencyLimit(number)` | platform default | Max concurrent executions. Maps to reserved concurrency (Lambda), max instances (Cloud Run), or max replicas (Container Apps). | | `.commandsEnabled(boolean)` | `false` | Enable the [remote command protocol](/docs/commands). | | `.readinessProbe({ method, path })` | — | Health check after deploy. Only used when the worker has public endpoints. | | `.environment(Record)` | `{}` | [Environment variables](/docs/infrastructure/worker/environment-variables). | | `.link(resource)` | — | Connect to a resource for binding access. Can be called multiple times. | | `.trigger(trigger)` | — | Add an [event trigger](/docs/infrastructure/worker/events-and-triggers). Can be called multiple times. | | `.permissions(string)` | required | [Permission profile](/docs/permissions) name. | ## Triggers [#triggers] ```typescript // Queue trigger — one message per invocation const worker = new alien.Worker("processor").trigger({ type: "queue", queue: tasks.ref() }).build() // In worker code: import { onQueueMessage, onStorageEvent } from "@alienplatform/sdk" onQueueMessage("tasks", async (msg) => { /* ... */ }) onStorageEvent("uploads", async (event) => { /* ... */ }) ``` # Pricing (/docs/infrastructure/worker/pricing) Alien provisions Workers in your customer's cloud. The customer pays the cloud provider for the underlying infrastructure, and Alien charges a small management fee. See [Pricing](/pricing) for Alien's current rates. *Prices shown for US East regions. Last verified: April 2026.* ## Compute Pricing [#compute-pricing] | Provider | Service | Per million invocations | Per GB-second | Per vCPU-second | | -------- | -------------- | ----------------------- | ------------- | --------------- | | AWS | Lambda | $0.20 | $0.0000166667 | — | | GCP | Cloud Run | $0.40 | $0.0000025 | $0.000024 | | Azure | Container Apps | $0.40 | $0.000003 | $0.000024 | Lambda charges per GB-second (memory × duration). Cloud Run and Container Apps charge separately for vCPU and memory. ## Example: 1M Invocations, 200ms Average, 256 MB Memory/month [#example-1m-invocations-200ms-average-256-mb-memorymonth] | Provider | Invocations | Compute | **Total** | | -------------------- | ----------------- | ------------- | --------- | | AWS Lambda | $0.00 (free tier) | $0.83 | **$0.83** | | GCP Cloud Run | $0.00 (free tier) | $1.20 + $0.63 | **$1.83** | | Azure Container Apps | $0.00 (free tier) | $1.20 + $0.15 | **$1.35** | *Compute calculated as: 1M × 0.2s × 0.25 GB = 50,000 GB-seconds.* ## Free Tiers [#free-tiers] * **AWS Lambda**: 1M invocations + 400,000 GB-seconds/month (always free). * **GCP Cloud Run**: 2M invocations + 180,000 vCPU-seconds + 360,000 GiB-seconds/month (always free). * **Azure Container Apps**: 2M invocations + 180,000 vCPU-seconds + 360,000 GiB-seconds/month (always free). ## Platform Notes [#platform-notes] * **Lambda** uses ARM64 (Graviton) processors by default, which are \~20% cheaper than x86. * **Cloud Run** charges for vCPU and memory separately. Per-instance concurrency means one container can handle multiple requests, reducing per-request compute cost. * **Container Apps** pricing is similar to Cloud Run. Idle containers in "consumption" mode are charged at reduced rates. ## Sources [#sources] * [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/) * [GCP Cloud Run Pricing](https://cloud.google.com/run/pricing) * [Azure Container Apps Pricing](https://azure.microsoft.com/en-us/pricing/details/container-apps/) # Toolchains (/docs/infrastructure/worker/toolchains) The `.code()` method on a Worker tells Alien where your source code is and how to build it. There are two approaches: point at source code with a toolchain, or use a pre-built container image. ## Source Code [#source-code] Point at a directory and pick a toolchain. Alien compiles the code and packages it into a container image automatically. ### TypeScript [#typescript] ```typescript new alien.Worker("api") .code({ type: "source", src: "./api", toolchain: { type: "typescript" }, }) .build() ``` Alien detects your package manager (bun, pnpm, or npm), installs dependencies, and compiles to a single executable using `bun build --compile`. The output is a standalone binary — no `node_modules`, no `dist/` folder. Use any HTTP framework (Hono, Express, Fastify). Export your app as the default export: ```typescript import { Hono } from "hono" const app = new Hono() app.get("/health", (c) => c.json({ ok: true })) export default app ``` **Options:** | Option | Default | Description | | ------------ | ------------------- | ---------------------------- | | `binaryName` | package.json `name` | Name of the compiled binary. | ### Rust [#rust] ```typescript new alien.Worker("agent") .code({ type: "source", src: "./agent", toolchain: { type: "rust", binaryName: "my-agent" }, }) .build() ``` Alien produces a single statically linked binary. On Linux, install `musl-tools` to build a target that matches the host architecture with Cargo. Alien uses `cargo-zigbuild` and Zig only when the Linux target differs from the host. Windows cross-builds use `cargo-xwin`; macOS targets require a macOS host and the Apple SDK. **Options:** | Option | Default | Description | | ------------ | -------- | ------------------------------------------------------ | | `binaryName` | required | Name of the binary to build (the Cargo binary target). | ### Docker [#docker] ```typescript new alien.Worker("service") .code({ type: "source", src: "./service", toolchain: { type: "docker" }, }) .build() ``` Alien builds using `docker buildx` with multi-architecture support. Use this when you need full control over the build process or have dependencies that don't fit the TypeScript/Rust toolchains. **Options:** | Option | Default | Description | | ------------ | -------------- | ------------------------------------------ | | `dockerfile` | `"Dockerfile"` | Path to the Dockerfile, relative to `src`. | | `buildArgs` | `{}` | Build arguments passed to `docker build`. | | `target` | — | Multi-stage build target. | ```typescript // Custom Dockerfile with build args .code({ type: "source", src: "./service", toolchain: { type: "docker", dockerfile: "Dockerfile.prod", buildArgs: { NODE_ENV: "production" }, target: "runtime", }, }) ``` ## Pre-Built Image [#pre-built-image] Skip the build entirely and use an existing container image: ```typescript new alien.Worker("api") .code({ type: "image", image: "ghcr.io/myorg/myimage:latest" }) .build() ``` The image is pulled and deployed directly. Use this when you build images in your own CI/CD pipeline or want to use a third-party image. # Wait Until (/docs/infrastructure/worker/wait-until) `waitUntil` lets you run async work after the response has been sent. The worker stays alive until all registered tasks complete — even during scale-down or redeployment. Use it for work that doesn't need to block the response: logging, analytics, cache updates, notifications. ## Usage [#usage] ```typescript import { waitUntil } from "@alienplatform/sdk" app.post("/api/checkout", async (c) => { const order = await processOrder(c.req) waitUntil( (async () => { await analytics.track("order.completed", { orderId: order.id }) await cache.invalidate(`user:${order.userId}:orders`) })() ) return c.json({ orderId: order.id, status: "confirmed" }) }) ``` ```rust async fn checkout(ctx: &AlienContext, order: Order) -> Response { let order_id = order.id.clone(); let user_id = order.user_id.clone(); ctx.wait_until(move || async move { analytics::track("order.completed", &order_id).await; cache::invalidate(&format!("user:{}:orders", user_id)).await; })?; Response::json(&json!({ "orderId": order.id, "status": "confirmed" })) } ``` The response returns immediately. The background task runs to completion in the same process. ## API [#api] ```typescript function waitUntil(promise: Promise): void ``` Pass an already-started promise. The runtime tracks it and keeps the worker alive until it resolves or rejects. Errors inside the promise are logged to stderr. They don't affect the response (it was already sent). ```rust fn wait_until(&self, task_fn: F) -> Result<()> where F: FnOnce() -> Fut + Send + 'static, Fut: Future + Send + 'static, ``` Pass a closure that returns a future. The runtime spawns it immediately. Returns `Err` if the runtime is already shutting down. ## Lifecycle [#lifecycle] 1. Your handler calls `waitUntil`. The task starts running immediately. 2. The response is sent to the caller. 3. The worker instance stays alive while tasks are pending. 4. During graceful shutdown (scale-down, redeployment), the runtime waits for all tasks to finish before terminating. You can call `waitUntil` multiple times in a single request. Each task is tracked independently — a failure in one doesn't cancel others. ## When to Use [#when-to-use] **Good fit:** logging, analytics, cache invalidation, sending notifications, writing audit trails, cleanup. **Not a good fit:** work that *must* succeed before the user sees a result. If you're charging a credit card or writing a critical database record, do it before sending the response. `waitUntil` background tasks don't currently execute in TypeScript workers on Windows. Rust workers and all other platforms (Linux, macOS) work correctly. # Agent Sessions (/docs/reference/api/agent-sessions) {/* */} # Api Keys (/docs/reference/api/api-keys) {/* */} # Auth (/docs/reference/api/auth) {/* */} # Billing (/docs/reference/api/billing) {/* */} # Cloud Regions (/docs/reference/api/cloud-regions) {/* */} # Commands (/docs/reference/api/commands) {/* */} # Debug Sessions (/docs/reference/api/debug-sessions) {/* */} # Deployment (/docs/reference/api/deployment) {/* */} # Deployments (/docs/reference/api/deployments) {/* */} # Domains (/docs/reference/api/domains) {/* */} # Events (/docs/reference/api/events) {/* */} # Gateways (/docs/reference/api/gateways) {/* */} # REST API (/docs/reference/api) The REST API is generated from the same OpenAPI document used to build the TypeScript SDK. Endpoint pages include authentication, parameters, request and response schemas, and an interactive request builder. ## Base URL [#base-url] ```text https://api.alien.dev ``` ## Authentication [#authentication] Send an Alien API key as a Bearer token: ```bash curl "https://api.alien.dev/v1/projects?workspace=my-workspace" \ -H "Authorization: Bearer $ALIEN_API_KEY" ``` Create keys with the smallest scope and role the integration requires. Keep them in server-side secret storage and never expose them to a browser. ## Errors [#errors] API errors include a machine-readable code, human-readable message, retryability, request ID, and optional remediation hint. Include the request ID when contacting support. Use the [TypeScript SDK](/docs/reference/typescript-sdk) when you want generated request and response types, retries, and typed errors. The source document is available at [/openapi.json](/openapi.json). # Machines (/docs/reference/api/machines) {/* */} # Managers (/docs/reference/api/managers) {/* */} # Operations (/docs/reference/api/operations) {/* */} # Packages (/docs/reference/api/packages) {/* */} # Platform (/docs/reference/api/platform) {/* */} # Projects (/docs/reference/api/projects) {/* */} # Release Channels (/docs/reference/api/release-channels) {/* */} # Releases (/docs/reference/api/releases) {/* */} # Remote Bindings (/docs/reference/api/remote-bindings) {/* */} # Resolve (/docs/reference/api/resolve) {/* */} # Resources (/docs/reference/api/resources) {/* */} # Setup Links (/docs/reference/api/setup-links) {/* */} # Slack Integration (/docs/reference/api/slack-integration) {/* */} # Sync (/docs/reference/api/sync) {/* */} # User (/docs/reference/api/user) {/* */} # Workspaces (/docs/reference/api/workspaces) {/* */} # Authentication for BYOC apps (https://alien.dev/blog/byoc-authentication) When your app runs in the customer's cloud, where does login happen and where do API keys live? Auth stays in your control plane; the data plane only verifies tokens. Your SaaS uses a hosted auth provider: Auth0, Clerk, WorkOS, or something like them. One application, one callback URL, one tenant, one dashboard where all your users live. It works so well you stopped thinking about it. Then a customer asks you to deploy into their cloud, and two questions come up in the first architecture call: 1. **Where do API keys and user records live** — your cloud or the customer's? 2. **How does a deployment in the customer's cloud authenticate back to yours?** Almost nobody plans for this. Auth is usually discovered mid-deployment, when the login redirect fails inside the customer's network. This post is the answer we give, and then the two alternatives for stricter environments. ## Auth stays in the control plane [#auth-stays-in-the-control-plane] Auth stays entirely in your cloud. That is the point of the control-plane/data-plane split: identity is vendor state, not customer data, so it lives on the side you operate. * **User records, sessions, and API keys live in your control plane.** Nothing identity-related is copied into the customer's account. * **The data plane holds no auth state.** It verifies short-lived tokens issued by your control plane, or checks an API key against it. That's all. * **There are no per-customer auth callbacks.** The customer's deployment never registers redirect URLs, never holds client secrets for your auth provider, never runs a login box. Walking through it: the user signs in against your control plane — your existing hosted auth, unchanged, with enterprise SSO federating there too (1). The control plane issues a short-lived JWT scoped to that deployment — minutes of validity, not days (2). The browser calls the data plane directly with that token (3). Step 4 is what makes this scale: the data plane verifies the token's *signature*, locally, using the control plane's public key. The public key isn't a secret — it ships with the deployment like any other config. So there is nothing to call back to on the request path, nothing stateful to operate, and revocation is the tokens expiring in minutes. ## How code calls it [#how-code-calls-it] The browser flow above covers humans using your dashboard. Programmatic access (a customer's service using your SDK) is simpler: Notice what's absent: no identity server in the customer's account, no secrets to distribute per deployment, no callback URLs to keep in sync as deployments come and go. A fair objection, with a clean answer: the control plane decides who gets in, but it has no path to the data. A customer who wants a hard guarantee restricts network access to the data plane — an allowlist of their own offices or VPN. Then a valid token is necessary but not sufficient: even the vendor, holding the keys it issued itself, cannot reach the data plane to use them. ## What breaks if you copy auth into each deployment [#what-breaks-if-you-copy-auth-into-each-deployment] The instinct to run login inside every customer environment sounds isolated and safe. In practice it multiplies everything: **Callback URLs multiply.** OAuth redirects the user back to a registered URL. In SaaS there is one. Per-deployment login means one per customer, often on internal domains, each registered with the provider and kept in sync as deployments come and go. **Secrets and tenants multiply.** Client secrets and signing keys now need to exist inside each customer environment: delivered encrypted, rotated without a redeploy, and invisible in the artifacts customers inspect. And where hosted auth modeled all your customers inside one application, you now run one auth tenant per customer — or one shared tenant that mixes every customer's identity data, which many will reject on sight. Everything that used to be one thing becomes N things. **Licensing costs multiply.** Hosted auth pricing scales with tenants and enterprise SSO connections. Per-deployment tenants times per-connection fees turns a rounding error into real COGS on exactly the deals BYOC exists to win. ## The two stricter patterns [#the-two-stricter-patterns] Control-plane auth assumes the customer accepts one thing: login traffic and identity metadata (not data) touch your infrastructure. Most do. For those that don't, there are two fallbacks. **Per-deployment IdP configuration.** The deployed app speaks OIDC or SAML directly to the customer's Okta or Entra. Login never leaves their network. Enterprises like it because it matches how they onboard any internal app: their IdP team creates a registration and hands you the values. The cost is yours: N configurations to collect, store, and rotate, and IdP misconfiguration becomes a support category. **Self-contained auth in the data plane.** For fully private and air-gapped environments, login has to happen locally. Resist the instinct to ship Keycloak or Dex — a stateful, security-critical identity server you now patch in every customer account. An embedded auth library inside your app (better-auth or your framework's equivalent) gets the same "login never leaves their network" property with a fraction of the moving parts. If the environment can make a single outbound HTTPS call to your control plane, use control-plane auth and don't run identity in the data plane at all. The fallbacks exist for environments that can't. ## Where Alien fits [#where-alien-fits] Alien is built around the control-plane/data-plane split, so "auth lives in the control plane, the data plane verifies a token" is the default, not something you engineer. Identity is stateful, security-critical logic — it belongs on the side you operate and update continuously, not copied into a hundred environments you can't reach. For the fallback patterns, where some configuration genuinely lives in each environment (an IdP issuer, client ID, client secret), those are per-deployment [inputs](/docs/inputs): typed, validated, encrypted where they're secret, and collected at setup through the portal or CLI instead of emailed afterward. The IdP handshake itself is always between your product and your customer's identity team; anyone claiming a deployment tool does that part for you is describing a demo. What a platform can do is make the sane default cheap — and the sane default is that identity never leaves your side of the boundary. *** *The deployment model this all sits on is in [what is BYOC?](/blog/what-is-byoc), and the component most teams already run in customer environments without an auth story at all is in [the agent nobody operates](/blog/the-agent-nobody-operates).* # The agent nobody operates (https://alien.dev/blog/the-agent-nobody-operates) Most B2B products ship a component that customers run inside their own environment. It usually has no owner, no release process, and more access than anything else you ship. Somewhere in your product's docs, there is a page called "Install the agent." ![A product docs sidebar with an "Install the agent" page highlighted](https://xeasjgeebnyslfxx.public.blob.vercel-storage.com/images/blog/blog-hidden-lambda-v10.png) Maybe yours calls it the collector, the connector, or the gateway. It's the component customers run inside their own environment so your product can reach data that will never be sent to you: the analytics proxy, the security collector in the VPC, the log forwarder, the on-prem gateway, the webhook relay for the enterprise that won't open an inbound port. It runs in networks you can't see, with credentials to systems you don't own. And at most companies, nobody operates it. ## How it's born [#how-its-born] No roadmap meeting approves the agent. It starts as a workaround: By the last step, every node of this system lives in a different company's infrastructure, and nobody is running it. Some customers are eighteen months behind. One granted it admin because that made the install error go away. Another put it behind a proxy you learned about during the outage it caused. When it breaks, you debug through screenshots and pasted logs. ## The most access, the least engineering [#the-most-access-the-least-engineering] Your main product has release engineering, on-call, and a threat model. The agent has none of that: no roadmap approved it, no budget created it, no team owns it. It is also the component with the most access: it sits inside the customer's network, holds credentials to their systems, and ships their data out. The least-engineered component holds the most sensitive position. That shows up in three places: **Security reviews.** The agent is the part of your product a security team reads most carefully — it's the part inside their network. If its permissions accumulated one support ticket at a time and you can't state which versions run where, the review goes badly. And it goes badly on your largest deals, because those are the customers who require the agent. **Incidents.** An outage in your cloud pages someone. An outage in the agent arrives as a support ticket — "the dashboard stopped updating" — hours later, and the investigation runs through the customer's hands. **Ownership.** Every other production system in your company has a team. The agent has whoever touched it last. Versioning, upgrades, and permission scoping are nobody's job, so they don't happen. Deployment into customer environments, updates you don't control, permissions a reviewer will read, observability without access — that is the BYOC problem set. Most teams have it without ever deciding to do BYOC. ## The only decision is whether it's designed [#the-only-decision-is-whether-its-designed] You don't get to decide whether you deploy into customer environments. If your product needs data that can't leave, and enterprise customers exist, you already do. The only decision left is whether the component is designed. Designed means the agent is treated as production software: a defined footprint (what it runs on, what it stores, what it can reach), a release process, health you can observe, a security story you can present, and an owner. Undesigned means what most teams have today: a version spread nobody can state, permissions nobody would volunteer in a security review, and a support queue that fills with "the agent stopped sending data." Closing that gap does not require a rewrite. Most teams assume it does, so they postpone it. But the component usually works. What it's missing is a control plane: something you run that knows what's deployed where, which versions are live, whether each instance is healthy, and can say so in a security review. The agent was always a data plane running in someone else's cloud. It never had the other half. ## Where Alien fits [#where-alien-fits] That's the half [Alien](/docs/how-alien-works) provides. It attaches over an outbound-only connection to the component you already ship and gives you the inventory, health, and update path it never had — no rewrite, no migration, no new install story for the customers already running it. When the agent is worth treating as a full product, you define it in `alien.ts` and Alien handles its releases, permissions, and rollbacks. Either way, the first step is the same: treat the agent as part of the product. For your largest customers, it already is. *** *The deployment model the agent grows into is in [what is BYOC?](/blog/what-is-byoc), and the first design question it forces — where login happens and where credentials live — is in [authentication for BYOC apps](/blog/byoc-authentication).* # What is BYOC? Bring your own cloud, explained (https://alien.dev/blog/what-is-byoc) BYOC means the vendor's software runs in the customer's cloud account while the vendor still operates it — and the second half is the part people miss. BYOC stands for bring your own cloud. It is a deployment model where a vendor's software runs inside the customer's cloud account, and the vendor still operates it: deployment, updates, monitoring, debugging, the whole day-2 job. The second half of that definition is the part people miss. Plenty of software ends up in customer accounts: a Docker image they pull, a Helm chart they install, an AMI they launch. That's self-hosting, not BYOC. The difference is who runs it afterward: in BYOC, the vendor keeps operating the product, the same way they run their own SaaS. The usual architecture splits the product in two. The control plane stays in the vendor's cloud: dashboard, configuration, orchestration, billing. The data plane, the part that actually touches customer data, runs in the customer's account and connects out. The customer's data never leaves their account. The vendor's ability to operate never leaves either. Both sides keep the thing they care about most. ## BYOC and its neighbors [#byoc-and-its-neighbors] BYOC keeps getting confused with its neighbors, and the confusion is expensive in sales calls. Cut through it with two questions: does the data stay with the customer, and does the vendor keep operating the product? **Single-tenant SaaS** gives the customer a dedicated instance: their own VPC, their own database, sometimes a dedicated account. But it is the vendor's account, so the data still leaves — and for the security teams driving these requirements, that is the entire question. It solves noisy neighbors, not data residency. That's why its row above matches SaaS exactly. **Self-hosting** puts the software in the customer's environment and the operations on the customer's plate. They install it, they upgrade it, they debug it at 2am. Beyond a simple binary, that asks every customer to become an expert operator of your product. Most won't, and the deployment freezes at whatever version they installed. **On-prem** is self-hosting's older form: the customer's physical data center instead of a cloud account. Same operating burden, minus the cloud APIs that make remote management possible at all. **BYOC** is the only model that answers yes to both questions. The customer provides the account and the boundary. The vendor provides the software and the operations. That split is what the term means, and losing it is what makes most "BYOC" offerings self-hosting with better marketing. ## Why BYOC exists [#why-byoc-exists] Three forces, and all three got stronger in the last few years. **Data gravity.** The most valuable data does not move. Codebases, internal documents, proprietary databases, systems behind the firewall with no public endpoint. AI made this acute: the data agents need most is exactly the data that cannot leave. If the data won't come to your software, your software goes to the data. **Security review reality.** "Your SaaS receives our data" triggers the longest possible review: subprocessor agreements, data flow diagrams, months. "Your software runs in our account, connects outbound only, and here is the exact IAM policy" is a review a security team can actually finish. The approval isn't about trust in the vendor. It's about how small and inspectable the trust has to be. **Cloud commits.** BYOC infrastructure runs on the customer's own AWS, GCP, or Azure bill. That spend burns down their committed-use contracts and reserved-instance discounts. Procurement teams sometimes prefer BYOC for this reason alone, before security says a word. ## The three trust models [#the-three-trust-models] Every BYOC implementation answers one question: how much access does the vendor get to the customer's account? A decade of vendor engineering blogs sorts into three answers. **Cross-account push.** The customer creates an IAM role; the vendor's control plane assumes it and drives resources in their account directly. This is how Databricks launched Spark clusters into customer accounts for over a decade, and it is the fastest model to build. It also aged the worst: broad standing access into customer accounts is exactly what security teams have stopped approving, and Databricks itself has spent years migrating compute back out. **Outbound-only agent.** A small vendor component inside the customer's environment dials out over HTTPS, pulls desired state, and applies it locally. Nothing dials in. No inbound ports, no VPN, and the customer keeps a kill switch: one firewall rule cuts the vendor off completely. This is where nearly every vendor that iterated on BYOC ended up, from Redpanda's agent to Kong's gateways to ClickHouse's support tunnel, because the direction of the connection is the thing security teams can approve. **Zero-access.** Redesign the software so the vendor needs no access at all: stateless components in the customer's account, all state in customer-owned storage, only metadata crossing the boundary. WarpStream is the canonical case. It is the strongest trust story and the hardest to retrofit, because it is an architecture, not a deployment option. Not sure which model a given customer will accept? Two questions usually settle it: ## When BYOC is the wrong answer [#when-byoc-is-the-wrong-answer] BYOC is an enterprise deal structure, and it costs like one. Some honest cases against it: **No enterprise pull yet.** If no customer has asked, don't build it. BYOC exists to close deals that SaaS cannot close. Just know that when the pull comes, it tends to come from the largest companies first. **One cloud, one customer.** A single deployment in a single customer's AWS account doesn't need a platform or a category. It needs a Terraform module and an engineer who answers the phone. The economics change at customer three, when the environments start multiplying. **The reason isn't nameable.** If neither you nor the customer can say in one sentence why the data must stay in their account, SaaS is simpler for everyone. An account boundary is not a security guarantee by itself — what matters is who can deploy code and reach data, which is why the trust models above are the real conversation. ## What Alien does [#what-alien-does] BYOC's definition has two halves: the software runs in the customer's account, and the vendor still operates it. Alien is an open-source platform for the second half. You describe your stack in one file, and Alien deploys it into each customer's cloud through whatever their security team will approve — a scoped cross-account role, an outbound-only operator, or an airgapped bundle. From your side, every customer looks the same: one command releases everywhere, health and telemetry come back without customer data, and debugging happens through audited commands instead of SSH. The architecture is in [how Alien works](/docs/how-alien-works). Most people read the first half of the definition and start writing Terraform. The install is a weekend. It's the second clause, "the vendor still operates it," that takes years, and it's the half that makes BYOC a product instead of a handoff. *** *The component most teams already ship into customer environments without calling it BYOC is in [the agent nobody operates](/blog/the-agent-nobody-operates), and the question every BYOC deal eventually hits — where login happens and where user records live — is in [authentication for BYOC apps](/blog/byoc-authentication).*