# ifhost — Impossible Hosting > CLI + API for deploying websites and backend services to the cloud. > API base URL: https://innstance.impossibuild.ai > > Docs: > CLI Guide: https://innstance.impossibuild.ai/docs > Advanced: https://innstance.impossibuild.ai/docs/advanced > llms.txt: https://innstance.impossibuild.ai/llms.txt ## Install CLI installer="$(mktemp)" curl --fail --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 https://innstance.impossibuild.ai/install --output "$installer" sed -n '1,240p' "$installer" # inspect downloaded code before executing it sh "$installer" rm -f "$installer" # Windows (PowerShell): download to a new local file, inspect, then execute it $Installer = Join-Path ([IO.Path]::GetTempPath()) ("ifhost-install-" + [guid]::NewGuid() + ".ps1") Invoke-WebRequest https://innstance.impossibuild.ai/install.ps1 -OutFile $Installer Get-Content $Installer & $Installer Remove-Item $Installer ## Auth For normal production use, run `innstance login` directly. Only when using staging or a self-hosted environment, set its API URL before signing in or deploying, and use an account and token belonging to that environment: export IMPOSSIBLE_API_URL=https://innstance.impossibuild.ai PowerShell: $env:IMPOSSIBLE_API_URL = "https://innstance.impossibuild.ai" innstance login # Interactive sign-in (opens browser) printf '%s' "$IFHOST_TOKEN" | innstance login --token - # Read token from stdin innstance login --from-file /run/secrets/ifhost-token # Or a local secret file IMPOSSIBLE_API_TOKEN # CI secret env var; overrides credentials file Credentials stored at: ~/.impossible/credentials.json Token format: imp_ + 64 hex chars. Server stores SHA-256 hash only. ## Deploy (the core flow) Every deploy starts with `innstance init` — generates `impossible.toml` with machine specs (app name, port, memory, cpus, autostop, min-machines, storage). `innstance deploy` errors if there is no `impossible.toml` in the current directory. Agents must pick specs explicitly, not rely on silent defaults. innstance init --app my-app --port 3000 --memory 512 innstance deploy # reads impossible.toml Agent decision tree: innstance deploy boots a generic shell VM. Drive install via `machines exec` / `machines write`, or `machines console` for interactive setup (see "Runner + console" section below). Run the project's own install steps — no build, no stack detection. Required init flags (agents: pass all of these, no prompts): --app, --port, --memory, --cpus, --autostop, --min-machines, --storage Optional: --cpu-kind (shared | performance), --cmd (startup override) The CLI auto-creates the app on first deploy. Apps run on a single machine (multi-machine scaling is on the roadmap). Use `innstance machines --app ` to list the machine's state. ## CLI Commands ### Account (no --app needed) innstance login # Interactive sign-in (opens browser) printf '%s' "$IFHOST_TOKEN" | innstance login --token - # Read token from stdin innstance login --switch # Switch between stored accounts innstance logout # Remove active account innstance status # Login email, plan, all apps, CLI version innstance init --app --port N --memory N --cpus N --autostop=BOOL --min-machines N --storage MODE # Create impossible.toml — REQUIRED before first deploy innstance deploy # Boot a shell VM to install your app step-by-step innstance apply --app # Push config (memory/cpus/env/secrets) to running machines without rebuilding innstance regions # List available regions innstance version # Show CLI version; auto-updates hourly by default innstance update # Explicit independently signed update innstance skill sync # Verify/cache latest SKILL.md + RUNBOOK.md innstance billing plan # Show current plan innstance billing usage # Show resource usage ### Machine management (requires --app) innstance machines --app # List machines innstance machines start --app # Start all machines innstance machines stop --app # Stop all machines innstance machines restart --app # Restart with fresh env/secrets innstance machines logs --app # Live tail (runs until Ctrl+C) innstance machines logs --app --since 1h # Last hour, then exit innstance machines logs --app --lines 50 # Last 50 lines, then exit innstance machines logs --app --grep "ERROR" # Filter (works in both modes) innstance machines logs --app --level error --lines 100 # Error-level only, historical innstance machines logs --app --json # Structured JSON per line innstance machines env set KEY=VALUE --app # Set env var (add --restart to apply) innstance machines env list --app # List env vars innstance machines secrets set KEY=@env:NAME --app # Secret source: @env, @file, or @stdin innstance machines secrets list --app # List secret keys (values hidden) innstance machines volumes create --mount /data --size 5 --app # Create volume innstance machines volumes list --app # List volumes innstance machines volumes rm --app # Delete volume innstance machines domains add example.com --app # Add custom domain + TLS innstance machines domains list --app # List domains innstance machines exec --app -- [args...] # Run command (picks first running machine) innstance machines exec --app --machine -- # Target a specific machine innstance machines destroy --app --yes-irreversible # Delete app and all resources (irreversible) Global flags: --json (structured output on all commands) ## Status (overview across all your apps) `innstance status` groups your account's apps by project and lists each project's URL, status, region, and current machine IDs (running + standby). Agents should run `status` first — the returned machine IDs are inputs for `machines exec --machine ` and `machines console start --app `. innstance status # human-readable list innstance status --json # structured — pipe to jq for scripts ## Exec (run commands inside the running container) innstance machines exec --app my-app -- ls / innstance machines exec --app my-app -- env innstance machines exec --app my-app -- cat /app/config.json innstance machines exec --app my-app -- python manage.py migrate innstance machines exec --app my-app -- sh -c "df -h && free -m" Target a specific machine: innstance machines --app my-app # list machine IDs with name/state innstance machines exec --app my-app --machine 32d41... -- # pin exec to one Without `--machine`, exec picks the first running machine. Runs inside the machine. Available tools depend on the image: - node:20 → has node, npm, sh, ls, cat, curl - python:3.12 → has python, pip, sh, ls, cat, curl - alpine → has sh, ls, cat (no bash, no curl unless installed) - nginx:alpine → has sh, ls, cat, nginx - distroless/scratch → NO shell, exec will fail Limitations: - ~10-minute timeout per command (enforced server-side, cannot be raised). For anything longer, use the fire-and-poll pattern: launch with `nohup > /tmp/log 2>&1 < /dev/null &`, then poll with further exec calls until done. - No interactivity (no vim, htop, tmux) — output returned as text - No stdin — cannot pipe input - Machine must be running (start with: innstance machines start --app ) - Runs as the container's default user (usually root unless the image sets a non-root USER) Use cases for agents: - Debug: check env vars, inspect files, test connectivity - Migrate: run database migrations after deploy - Config: read/verify runtime configuration - Health: check disk usage, memory, running processes ## Runner + console (for projects with multi-step / interactive setup) Use when the upstream project's install is too complex or interactive to capture up front (`hermes setup`-style wizards, multi-step CLIs, custom auth flows, agent-managed setup). Flow: 1. innstance init --app --memory 1024 --min-machines 1 --storage local # Create impossible.toml. storage=local gives a persistent /data. # min-machines=1 so the daemon stays up; autostop=true would kill idle bots. 2. innstance machines secrets set K=@env:K K2=@file:/secure/k2 --app # Inject creds without literal argv values 3. innstance deploy --app # Boots a generic shell VM 4. innstance deploy --app # ONE more time after secrets — re-applies init config 5. innstance machines console start --app -- bash # Returns { session_id, machine_id } 6. Drive the install through console input/output (see below) The runner is a generic Debian shell environment with tmux preinstalled. ### Driving an install through the console SESSION="ifhost-01..." # from `console start` innstance machines console input --app "$SESSION" "" innstance machines console input --app "$SESSION" --key Enter innstance --json machines console output --app "$SESSION" --lines 80 innstance machines console end --app "$SESSION" # sessions persist until ended (no auto-cleanup) Echo a unique marker after each long command and poll `console output` until the marker (with rc) appears, so you know success/failure WITHOUT timing out: cmd='apt-get install -y --no-install-recommends curl git python3 ...; echo __PHASE1_DONE__rc=$?' # send cmd, send Enter # poll until output matches: __PHASE1_DONE__rc=(\d+) For background daemons (gateways, message bus loops), launch inside a NAMED tmux session inside the container so the process survives `console` disconnect: tmux new-session -d -s app "cd /data/src && PYTHONUNBUFFERED=1 exec ./bin/run 2>&1 | tee /data/app.log" Always set `PYTHONUNBUFFERED=1` (or equivalent) for piped Python output — otherwise log files stay empty for minutes due to block-buffering. ### Common gotchas - `innstance machines exec` holds a synchronous connection open for the command's ~10-minute budget, but for anything more than a minute or two (apt installs, npm, pip with native compiles), use fire-and-poll or the console + polling pattern above instead of holding a synchronous exec open. - `shared-cpu-1x` is throttled. A "kitchen sink" apt install (build-essential + ffmpeg + nodejs) takes 5-10 min. Slim down to ONLY the packages you actually need — for a Telegram bot: `curl ca-certificates git python3 python3-venv python3-pip tmux` (~50 MB, ~1 min). Skip ffmpeg unless TTS, skip build-essential unless pip needs to compile, skip nodejs unless the project uses it. For genuine heavy installs, consider bumping `[resources] cpus = 2` or `cpu_kind = "performance"`. Both are capped by the account's plan (see Pricing tiers), so check before you raise them. - Setting secrets may drop the runner's init config. Always run `innstance deploy` ONE more time after the secrets are set, then start the console. Verify with `innstance machines exec --app -- env` that the secret keys are present in the container env. - `innstance deploy` after `innstance machines destroy` works on a fresh app — the previous machine + volume are gone, fresh `/data` is auto-created from `storage = "local"`. Same toml; no extra flags. - For projects that ship optional dependency extras (`pip install .[all]`): install only the extras you NEED (e.g. `pip install python-telegram-bot` not the whole `[messaging]` extra). Saves ~5 min per redeploy. Look at the project's pyproject.toml `[project.optional-dependencies]` to find minimal extra names. - If the project has a `gateway`/daemon command that writes a PID file: use ` --replace` on restart, or run ` stop --all` first, otherwise the new instance refuses to start while the old PID is alive. - For projects driven by `OPENROUTER_API_KEY` / model providers: set `model.default` and `model.provider` explicitly via the project's config CLI before first message. An unset model often fails with confusing errors (e.g. OpenRouter 404 "No endpoints found for ."). Don't rely on "auto-detect" defaults. ### Verifying the bot/daemon is alive innstance machines exec --app -- ps -ef | grep innstance machines exec --app -- tail -50 /data/.log innstance machines exec --app -- cat /data/logs/agent.log # if the project has its own log dir For Telegram-style bots, the canonical "it's working" signal in logs: INFO ✓ telegram connected INFO Gateway running with 1 platform(s) If logs show empty or just a banner: the daemon may be alive but Python output is buffered. Re-launch with `PYTHONUNBUFFERED=1` and check `/data/logs/*.log` (most projects also write structured logs there). ## Config File innstance deploy requires impossible.toml in the current project directory. Run innstance init to create it, then edit the settings for your app. Supported command flags override the corresponding file settings for that invocation. app = "my-app" region = "iad" [service] internal_port = 8080 autostop = true min_machines = 0 [resources] cpu_kind = "shared" # "shared" always; "performance" on Pro and Team cpus = 1 # 1, 2, 4, 8, capped per plan (see Pricing tiers) memory_mb = 256 # 256, 512, 1024, 2048, ... [[volumes]] name = "data" size_gb = 5 mount_path = "/data" [env] NODE_ENV = "production" ## REST API All endpoints require: Authorization: Bearer imp_xxx Content-Type: application/json ### Auth POST /auth/clerk/exchange { token } → Clerk browser session becomes an Innstance web session; new account: 403 { terms_required, signup_token } POST /auth/clerk/accept { signup_token, terms_accepted } → create a terms-stamped account and web session POST /auth/device/start { device_name } → { device_code, user_code, verification_url, interval, expires_in } GET /auth/device/lookup ?user_code=XXXX-XXXX → { user_code, device_name, status } for the approval page POST /auth/device/approve { user_code, clerk_token, action? } → approve or deny a CLI device grant; new account: 403 { terms_required, signup_token } POST /auth/device/poll { device_code } → 202 pending; 200 { status: approved|denied, token?, email? }; approved token is one-use GET /auth/google/cli-config (legacy compatibility only) → Desktop client_id for pre-device-flow CLIs POST /auth/google/cli (legacy compatibility only) → Google loopback + PKCE exchange GET /auth/me → { id, email } POST /auth/token { "name": "..." } → { token, name } GET /auth/tokens → { tokens: [...] } DELETE /auth/tokens/{id} → { revoked: id } ### Apps GET /apps → { apps: [...] } POST /apps { "name": "x", "region": "iad" } → { id, name, url } GET /apps/{name} → { id, name, status, url, cpu_kind, cpus, memory_mb, ... } PATCH /apps/{name} { "memory_mb": 512, ... } → { updated: [...] } DELETE /apps/{name} → { deleted: name } ### Deploy POST /apps/{name}/runner-deploy (no body) → { id, status, url, machine_id } GET /apps/{name}/deployments → { deployments: [...] } ### Machines POST /apps/{name}/stop → { stopped: N } POST /apps/{name}/start → { started: N } POST /apps/{name}/restart → { restarted: N } GET /apps/{name}/machines → { machines: [...] } POST /apps/{name}/exec { "cmd": ["ls", "/data"] } → { stdout, stderr, exit_code } ### Env & Secrets GET /apps/{name}/env → { vars: { K: V, ... } } PUT /apps/{name}/env { "vars": { "K": "V" } } → { set: N, restarted: bool } GET /apps/{name}/secrets → { keys: ["K1", "K2"] } PUT /apps/{name}/secrets { "secrets": { "K": "V" } } → { set: N, restarted: bool } ### Volumes GET /apps/{name}/volumes → { volumes: [...] } POST /apps/{name}/volumes { "name": "data", "size_gb": 5, "mount_path": "/data" } → { ... } DELETE /apps/{name}/volumes/{vol} → { removed: vol } ### Domains GET /apps/{name}/domains → { domains: [...] } POST /apps/{name}/domains { "hostname": "example.com" } → { hostname, tls_status, cname_target } DELETE /apps/{name}/domains/{h} → { removed: hostname } ### Logs GET /apps/{name}/logs ?format=json&since=1h&no-follow=true → text/event-stream ### Health GET /apps/{name}/health → { app, status, healthy, machines_total, machines_running, uptime_seconds, http: { total_requests, error_rate, avg_response_ms, p99_response_ms }, machines: [...] } ### CLI Version (no auth) GET /cli/version → { build_id: "20260416-024938" } ### Regions (no auth) GET /regions → { regions: [{ code, name, area }] } ### Billing GET /billing/plans (no auth) → { plans: [{ id, name, price_cents, price_display, limits: { apps, apps_unlimited, pool_memory_mb, pool_volume_gb, bandwidth_gb_mo, cpus_per_app, cpu_kind, compute_hours_mo, compute_hours_mo_unlimited } }] } GET /billing/plan → { plan_tier, limits, usage (incl. estimated_cost_cents) } GET /billing/usage → { period, entries: [{ app, type, quantity, unit }], totals: { : { total, unit, display } } } GET /apps/{name}/usage → { ... per-app usage } PUT /billing/alert { "max_cents": 2000 } → { max_cents, display } GET /billing/alert → { max_cents, current_cents, exceeded, display } DELETE /billing/alert → { status: "removed" } ## Storage modes (--storage flag on deploy) innstance deploy --app my-app # stateless (default — no persistent storage) innstance deploy --app my-app --storage local # local /data volume, single machine Two modes: ### empty (default) — stateless - No persistent disk. Container filesystem is ephemeral; survives restarts but resets on redeploy. - Use this for HTTP services, workers, or anything that keeps state in a managed DB (Supabase, Neon, Upstash, Turso) instead of locally. ### local — for embedded state (SQLite, file caches) - 1 GB volume mounted at /data, persists across redeploys (grow with `volumes extend`) - ⚠ Volumes are per-machine — one machine, one disk - ⚠ Region-locked at first deploy - Use only when the app must write to disk and you accept single-machine operation. For Postgres/MySQL/Redis at scale, use a managed DB instead. Object/cloud storage (auto-provisioned S3 buckets) was REMOVED. If your app needs blob storage, sign up directly with Tigris/S3/R2/B2 and pass credentials via --secret. ## Database recommendations (DO NOT use volumes for databases beyond SQLite) For databases, use a MANAGED service: Supabase — managed Postgres, free tier, dashboard included Neon — serverless Postgres, scales to zero, free tier Turso — managed SQLite at the edge, distributed reads PlanetScale — managed MySQL, branching workflow Connect via DATABASE_URL env var: innstance deploy --secret DATABASE_URL=@env:DATABASE_URL These scale independently of your app. Your app stays stateless — the recommended architecture for Innstance apps. ### Subscription GET /subscription → { plan_tier, status, ... } POST /subscription/checkout { "plan": "pro" } → { checkout_url } # Hosted page: card + crypto (USDC). Card is # temporarily unavailable — crypto works today. POST /subscription/cancel → { cancelled: true } GET /subscription/invoices → { invoices: [...] } ### Pay-as-you-go top-up (x402) GET /subscription/topup/preview ?plan=

→ { deposit_address, plan, monthly_price_usdc, rate_days_per_dollar, minimum_credit_usdc, bound_wallets, expires_at_current } GET /subscription/topup/recent ?limit=N → { credits: [...] } GET /subscription/topup/x402 ?amount_usd=N (x402 payment header) → 402 { accepts: [...] } or { status: "credited", days_granted, expires_at } # innstance billing topup : HTTP 402 challenge → # signed EIP-3009 payment, or a manual USDC # transfer to the deposit address from a bound wallet. ## Architecture ifhost CLI (Go binary) → ifhost API Server (Go) → Cloud VMs │ durable managed datastore - CLI: Go + cobra. Single binary, no runtime deps. - API: Go + chi. Handles auth, deploy orchestration, secrets encryption. - DB: durable managed datastore. Secrets encrypted AES-256-GCM with per-app HKDF keys. - Infra: lightweight VMs — per-second billing, scale-to-zero, 30+ regions. ## Env var handling - Non-sensitive (NODE_ENV, PORT): innstance machines env set → injected into container env - Sensitive (DB_URL, API_KEY): `innstance machines secrets set K=@env:K` → values never returned by the API, only keys listable - Both can also be set during deploy: `innstance deploy --env K=V --secret SECRET=@file:/secure/value` - Setting env/secrets is staged by default; pass `--restart` or restart once after all changes - Never put literals in impossible.toml or argv. Use `@env`, `@file`, or `@stdin`; a tracked `[secrets]` config is refused and never uploaded. ## Pricing tiers Free: $0/mo, 1 app, 1 GB RAM pool, 1 shared CPU, 100 GB traffic Hobby: $15/mo, 2 apps, 2 GB RAM pool, 2 shared CPUs, 100 GB traffic Pro: $49/mo, 8 apps, 8 GB RAM pool, 4 performance CPUs, 500 GB traffic Team: $149/mo, 20 apps, 24 GB RAM pool, 8 performance CPUs, 1024 GB traffic