# Stategraph Documentation > Full text of the Stategraph documentation, concatenated for LLM consumption. Each page below is delimited by an H1 title and its source URL. --- # Documentation Source: https://stategraph.com/docs Stategraph documentation. Set up Velocity for parallel Terraform execution and explore infrastructure insights, inventory, cost, and orchestration. New to Stategraph? Start with the [Introduction](/docs/getting-started/introduction) for what it is and how it works, skim the [Core concepts](/docs/getting-started/concepts), then follow the [Quickstart](/docs/getting-started/quickstart) to import a state and explore it in about ten minutes. ## Prerequisites Get access to Stategraph below — use the hosted service or [deploy it yourself](/docs/deployment) (Docker Compose, Kubernetes, or ECS) — then explore the platform capabilities. ## Get Started with Stategraph Stategraph runs two ways: as a fully hosted service we manage for you, or self-hosted in your own infrastructure where your state and data never leave it. Choose whichever fits your team. - [Hosted (SaaS)](https://app.stategraph.cloud): Get started in minutes with no infrastructure to run. We manage the server, upgrades, and availability so you can focus on your Terraform. - [Self-Hosted Deployment](/docs/deployment): Run Stategraph in your own environment with Docker Compose, Kubernetes, or Amazon ECS. Your state and data stay entirely under your control. - [Talk to Us](https://stategraph.com/contact): The self-hosted server image is distributed privately so we can make sure your team has the support and onboarding it needs. Reach out and we'll get you set up and aligned on your deployment. ## Platform Capabilities - [Velocity](/docs/velocity): Parallel plan and apply for Terraform and OpenTofu. Graph-aware execution with resource-level locking, multi-state transactions, and concurrent execution. Drop-in replacement for terraform plan and terraform apply. - [Insights](/docs/insights): Timeline, search, blast radius analysis, and dependency graph visualization for your Terraform infrastructure. - [Inventory](/docs/inventory): Infrastructure catalog with SQL queries, dashboards, gap analysis, and resource browsing across all states. - [Cost Analysis](/docs/cost): Estimate monthly and hourly infrastructure costs from your Terraform state, with per-resource breakdowns, tenant rollups, history, and SQL queries over cost data. - [Orchestration](/docs/orchestration): GitOps workflows with PR-based plans, policy enforcement, cost estimates, and drift detection. - [AI Agents](/docs/ai-agents): Drive Stategraph from Claude Code and other LLM agents via SKILL.md workflow skills for query, cost, change, import, and refactor. ## Key Features | Feature | What it does | Docs | |---------|-------------|------| | **Parallel plan/apply** | Graph-aware execution runs independent resource subgraphs concurrently | [Velocity](/docs/velocity) | | **Multi-state transactions** | Atomic operations spanning multiple Terraform states | [Multi-state](/docs/velocity/multi-state) | | **Resource-level locking** | Fine-grained locks instead of full state locks, no more lock contention | [Resource locking](/docs/velocity/resource-locking) | | **Blast radius analysis** | See what a change will affect before you apply it | [Blast radius](/docs/insights/blast-radius) | | **Dependency graph** | Visualize cross-resource and cross-state dependencies | [Graph explorer](/docs/insights/graph-explorer) | | **SQL queries** | Query your infrastructure like a database using SQL | [Query](/docs/inventory/query) · [SQL syntax](/docs/reference/sql-syntax) | | **Search & filter** | Find instances by type, module, provider, and attributes across states | [Search](/docs/insights/search) | | **Transaction timeline** | Immutable audit log of every plan, apply, and state change | [Timeline](/docs/insights/timeline) | | **Gap analysis** | Find unmanaged resources and coverage gaps | [Gap analysis](/docs/inventory/gap-analysis) | | **Cost analysis** | Estimate infrastructure costs per resource, state, and tenant | [Cost Analysis](/docs/cost) | | **Dashboards** | Pre-built and custom views of your infrastructure | [Dashboards](/docs/inventory/dashboards) | | **Drift detection** | Detect when infrastructure diverges from state | [Orchestration](/docs/orchestration) | | **PR automation** | Automatic plans on pull requests with GitOps workflows | [Orchestration](/docs/orchestration) | | **REST API** | Programmatic access to all Stategraph data | [API reference](/docs/reference/api) | | **CLI** | Full command-line interface for all operations | [CLI reference](/docs/cli) | | **AI Agents** | Drive Stategraph from an LLM via SKILL.md workflow skills | [AI Agents](/docs/ai-agents) | | **SSO/OIDC** | Google OAuth, OIDC, and local authentication | [Authentication](/docs/authentication) | ## Support - **Discord** — [Join our community](/discord) for questions, discussions, and support - **Email** — [support@stategraph.com](mailto:support@stategraph.com) for direct assistance --- # Introduction Source: https://stategraph.com/docs/getting-started/introduction Stategraph is graph-based state management for Terraform and OpenTofu. Learn what it is, how it works, and what you can do with it. Stategraph is a state management and visualization system for Terraform and OpenTofu. It replaces the flat state file (and the whole-state lock that comes with it) with a dependency graph of your infrastructure, stored on a server you can query, lock at the resource level, and reason about. You keep writing normal Terraform. Stategraph changes where your state lives and what you can do with it. ## Why Stategraph A typical Terraform setup keeps state as a single blob in S3. That has two costs: - **One lock per state.** Any `apply` locks the entire state, so your team's changes serialize — even when they touch completely unrelated resources. - **No visibility.** The state is an opaque JSON file. There is no good way to ask "what depends on this?", "what would this change affect?", or "what is this costing me?". Stategraph models state as a graph of resources and their dependencies. That single change unlocks the rest: - [Velocity](/docs/velocity) — parallel plan and apply with resource-level locking. Changes that touch disjoint resources run concurrently instead of waiting on each other. It's a drop-in for `terraform plan` / `terraform apply`. - [Insights](/docs/insights) — blast-radius analysis, a dependency graph explorer, search, and a transaction timeline. - [Inventory](/docs/inventory) — query your whole estate with SQL, and find cloud resources Terraform isn't managing yet (gap analysis). - [Cost Analysis](/docs/cost) — per-state and per-tenant cost, attribution, and a cost delta at plan time. ## How it works You run two things: the CLI (local) and the Server (a coordinator backed by PostgreSQL). 1. You run `stategraph tf plan` or `stategraph tf apply` from the CLI. 2. The CLI sends your configuration and the requested change to the Server. 3. The Server records the change as a transaction and returns the minimal, self-contained bundle of configuration the change needs. 4. The CLI runs Terraform/OpenTofu locally against your cloud with your own credentials, then reports the outcome back. 5. The CLI shows you the result. The important part: your authoritative state lives on the Server, but your cloud credentials never leave the machine running the CLI. The Server never runs Terraform and never sees your cloud. See [Architecture](/docs/reference/architecture) for the full picture. ## Where to start - New to the model? Read [Core concepts](/docs/getting-started/concepts) — the vocabulary you'll see everywhere. - Want to get hands-on? Follow the [Quickstart](/docs/getting-started/quickstart) — install, connect, import a state, and explore in about ten minutes. Stategraph runs as a [hosted service](https://app.stategraph.cloud) we manage, or [self-hosted](/docs/deployment) in your own environment where your state and data never leave it. --- # Core concepts Source: https://stategraph.com/docs/getting-started/concepts The core Stategraph concepts — state graph, states, groups, tenants, transactions, blast radius, and resource-level locking — in one place. These are the terms you'll see throughout the docs and the CLI. Read this once and the rest of the documentation reads faster. For a fuller alphabetical list, see the [Glossary](/docs/reference/glossary). ## The state graph The central idea. Instead of storing your state as a flat file, Stategraph stores it as a graph: every resource is a node, and every dependency between resources is an edge. Terraform already computes this graph to order its operations — Stategraph keeps it, so you can lock, query, and analyze against it. Everything else below is something the graph makes possible. ## State A state is one Terraform state — the resources of a single root module — stored on the Server and identified by a UUID. A state has a human-readable name, belongs to a workspace, and is wired to a directory of configuration through a group. This is the rough equivalent of one S3 state file, but graph-shaped and server-managed. ## Group and workspace A group ties a configuration directory to its state. The mapping is recorded in a `stategraph.json` file in that directory (written for you on import), so the CLI knows which state a directory drives. A workspace is the same concept as a Terraform workspace — a named variant of a configuration (for example `default`, `staging`, `production`). One group can contain several workspaces, each with its own state. ## Tenant A tenant is the top-level isolation boundary — typically an organization or team. Every state lives in exactly one tenant, and access, billing sources, and gap analysis are all scoped per tenant. Most CLI commands take a `--tenant` (or the `STATEGRAPH_TENANT_ID` environment variable). ## Transaction Every plan and apply is a transaction. It moves through a lifecycle — open, previewing, committing, then committed (or failed/aborted) — and the Server records it in a timeline you can inspect. Transactions are how Stategraph coordinates concurrent changes safely. See [Transactions](/docs/velocity/transactions). ## Resource-level locking Because the state is a graph, Stategraph locks at the level of individual resources rather than the whole state. Two transactions that touch disjoint sets of resources can plan and apply at the same time; only genuinely overlapping changes are serialized, with conflicts detected at commit time. This is what makes [Velocity](/docs/velocity) parallel. ## Resource and instance A resource is a single `resource` block in your configuration (for example `aws_instance.web`). An instance is a concrete object that block produces — a block with `count` or `for_each` has several instances (`aws_instance.web[0]`, `aws_instance.web[1]`, …). Inventory queries and blast radius operate at the instance level. ## Blast radius The blast radius of a resource is its dependency cone — everything that could be affected if you change it. Stategraph computes it from the graph so you can assess impact *before* you apply. See [Blast Radius](/docs/insights/blast-radius). ## Gap analysis Gap analysis finds cloud resources that exist in your account but aren't managed by any Terraform state — drift in the other direction. It's how you discover what's running outside of code. See [Gap Analysis](/docs/inventory/gap-analysis). ## Querying with SQL Stategraph exposes your estate as a set of SQL-queryable tables (`resources`, `instances`, `states`, and more). You ask questions about your infrastructure the way you'd query a database. Start with `stategraph sql schema` to discover the tables, then `stategraph query "..."`. See [Query](/docs/inventory/query). ## Cost Stategraph estimates the cost of the resources in your states, attributes spend by tag/owner/provider, and can show a cost delta at plan time. With a [FOCUS](https://focus.finops.org/) billing source connected, it also reconciles estimates against your actual cloud spend. See [Cost Analysis](/docs/cost). ## Access and capabilities Users and service accounts authenticate with API keys (access tokens). Tokens can be capability-scoped — granted only the specific rights they need (for example commit access to a single state) rather than blanket admin. See [Authentication](/docs/authentication). --- # Quickstart Source: https://stategraph.com/docs/getting-started/quickstart Install the Stategraph CLI, connect to a server, import an existing Terraform state, and explore your infrastructure in about ten minutes. This walks you from zero to querying your own infrastructure in about ten minutes: install the CLI, connect it to a server, import an existing Terraform state, and explore it. If the vocabulary here is unfamiliar, skim [Core concepts](/docs/getting-started/concepts) first. ## Before you start You need two things: - **A Stategraph server.** Use the [hosted service](https://app.stategraph.cloud) (nothing to run), or [self-host one](/docs/deployment) with Docker Compose, Kubernetes, or ECS. - **An existing Terraform project** with a state file (`terraform.tfstate` or a pulled remote state). Stategraph onboards your real infrastructure — you don't start from scratch. ## Step 1 — Install the CLI The fastest way on macOS or Linux is the install script: ```bash curl -sSL https://get.stategraph.com/install.sh | sh ``` Or use Homebrew on macOS: ```bash brew tap stategraph/stategraph brew install stategraph ``` For APT (Debian/Ubuntu), a binary download, or Docker, see the [CLI installation options](/docs/cli). Verify the install: ```bash stategraph --help ``` ## Step 2 — Connect to a server Point the CLI at your server and authenticate with an API key. Create a key in the console under **Settings → API Keys**, then set three environment variables: ```bash export STATEGRAPH_API_BASE="https://your-server.example.com" # your hosted or self-hosted URL export STATEGRAPH_API_KEY="" # from Settings → API Keys export STATEGRAPH_TENANT_ID="" # shown in stategraph info ``` Confirm the connection: ```bash stategraph info ``` ``` key value -------- -------------------------------------------- User Jane Doe (jane@example.com) Server 2.3.9 Client 2.3.9 Tenants 1 Tenant Production (7e45a833-24db-4847-b3da-bbb6f699f201) --- What you can do next --- stategraph sql schema # Discover queryable tables stategraph query "SELECT * FROM resources" # Browse your infrastructure stategraph states list --tenant ID # List Terraform states stategraph gaps --tenant ID --provider aws # Find unmanaged resources stategraph blast-radius ADDR --state ID # Check impact before changes ``` `stategraph info` is your orientation command — it shows who you are, which server and tenant you're talking to, and suggested next steps. ## Step 3 — Import an existing state From the directory that holds your Terraform configuration and its state file, import both into a new Stategraph state in one step: ```bash stategraph import tf \ --tenant "$STATEGRAPH_TENANT_ID" \ --workspace default \ --name my-app \ terraform.tfstate ``` Stategraph first reminds you that it manages state independently of your existing Terraform backend — answer `y` to continue. It then creates a state, imports your state and HCL, and writes a `stategraph.json` in the directory so the CLI knows which state this configuration drives. Verify it landed: ```bash stategraph states summary --state ``` For the full onboarding flow — remote state, modules, and attached files — see [Import](/docs/cli/import). ## Step 4 — Explore your infrastructure List the states in your tenant: ```bash stategraph states list ``` ``` id name workspace group_id created_at ------------------------------------ --------------- --------- ------------------------------------ -------------------- 9cb4ba52-02a4-4c8a-84d8-9034a26af546 my-app default 09a29aea-971a-4917-91e6-db5198dd793c 2026-06-27T00:00:00Z ``` Query your whole estate with SQL. Start by discovering the tables, then ask a question: ```bash stategraph sql schema stategraph query "SELECT type, count(*) AS n FROM resources GROUP BY type ORDER BY n DESC LIMIT 5" ``` ``` type n ------------------- --- aws_iam_role 42 aws_instance 18 aws_security_group 15 aws_subnet 12 aws_s3_bucket 9 ``` Check the blast radius of a resource — everything a change to it could affect — before you touch it: ```bash stategraph blast-radius aws_subnet.public --state ``` ``` resource_address address index distance ----------------------------- ----------------------------- ----- -------- aws_subnet.public aws_subnet.public 0 aws_instance.web aws_instance.web[0] 0 1 aws_instance.web aws_instance.web[1] 1 1 aws_route_table_association.a aws_route_table_association.a 1 ``` See what a state is costing you: ```bash stategraph cost state --state ``` ``` address type provider region monthly hourly supported no_price ---------------------- ------------------ -------- --------- ---------- -------- ------------ -------- (total) 790.736000 1.083200 18/21 priced aws_instance.analytics aws_instance aws us-east-1 367.920000 0.504000 true false aws_instance.web[0] aws_instance aws us-east-1 70.080000 0.096000 true false aws_security_group.app aws_security_group aws true true ``` ## Step 5 — Plan a change With `stategraph.json` in place, Stategraph is a drop-in for the Terraform workflow. Edit your `.tf` files, then: ```bash stategraph tf plan ``` This compares your configuration against the current state and shows the diff — no `--out` needed for a read-only preview. To apply, run `stategraph tf apply` (it can plan and apply in one step, or apply a saved plan file). Because Stategraph locks at the resource level, teammates changing unrelated resources can plan and apply at the same time. See [Velocity setup](/docs/velocity/setup) for wiring multiple workspaces and CI. ## Next steps - [Core concepts](/docs/getting-started/concepts) — the model behind what you just did - [Velocity](/docs/velocity) — parallel plan and apply with resource-level locking - [Insights](/docs/insights) — blast radius, the graph explorer, and the transaction timeline - [Inventory](/docs/inventory) — query your estate with SQL and find unmanaged resources - [Cost Analysis](/docs/cost) — per-state and per-tenant cost, attribution, and plan-time deltas --- # Introduction Source: https://stategraph.com/docs/velocity Parallel plan and apply for Terraform and OpenTofu. Graph-aware execution with resource-level locking and multi-state transaction support. Velocity is Stategraph's parallel execution engine for Terraform and OpenTofu. It uses the dependency graph to identify independent subgraphs and execute them concurrently. > **Velocity includes a 30-day free trial.** [Set up Velocity](/docs/velocity/setup) to get started. ## Features ### Graph-Aware Parallel Execution Velocity analyzes the Terraform dependency graph to find independent subgraphs — groups of resources with no dependencies between them. These subgraphs are planned and applied concurrently, reducing execution time without changing behavior. ```bash # Drop-in replacement for terraform plan/apply stategraph tf plan --tenant --out plan.json stategraph tf apply plan.json ``` ### Resource-Level Locking Traditional backends lock the entire state file during operations. Velocity locks individual resources, so multiple engineers can work on different parts of the same state simultaneously. Conflicts are detected at the resource level at commit time (when you run apply). ### Multi-State Transactions Coordinate plan and apply across multiple Terraform states in a single atomic transaction. Changes to networking, compute, and application layers can be planned together and applied as one unit. ```bash stategraph tf mtx --tenant --out plan.json ./networking ./compute ./application ``` ### Drop-In Replacement Velocity uses the same Terraform and OpenTofu binaries you already have installed. No provider changes, no HCL modifications — just replace `terraform plan` with `stategraph tf plan`. ## How It Works ``` ┌──────────────┐ ┌─────────────────┐ ┌────────────┐ │ Terraform │ ◀─────▶ │ Stategraph │ ──────▶ │ PostgreSQL │ │ / OpenTofu │ │ Velocity │ │ │ └──────────────┘ └─────────────────┘ └────────────┘ │ │ │ 1. Parse graph │ 2. Identify subgraphs │ 3. Plan in parallel │ 4. Lock resources ▼ ▼ 5. Apply concurrently ``` 1. **Parse dependency graph** - Velocity reads the Terraform configuration and builds the resource dependency graph 2. **Identify subgraphs** - Independent groups of resources are identified for parallel execution 3. **Plan in parallel** - Each subgraph is planned concurrently using your Terraform/OpenTofu binary 4. **Lock resources** - Individual resources are locked (not the entire state file) 5. **Apply concurrently** - Independent changes are applied in parallel, respecting dependency order ## Getting Started 1. [Deploy Stategraph](/docs/deployment) if you haven't already 2. [Set up Velocity](/docs/velocity/setup) — import or create a state, then plan and apply ## Key Concepts | Topic | Description | |-------|-------------| | [Transactions](/docs/velocity/transactions) | Transaction lifecycle: create, preview, commit | | [Resource-Level Locking](/docs/velocity/resource-locking) | Conflict detection and concurrent transaction handling | | [Multi-State Operations](/docs/velocity/multi-state) | Coordinating plan/apply across multiple Terraform states | ## Documentation | Topic | Description | |-------|-------------| | [Setup](/docs/velocity/setup) | Prerequisites, installation, and CLI reference | | [Transactions](/docs/velocity/transactions) | Transaction lifecycle: create, preview, commit | | [Resource-Level Locking](/docs/velocity/resource-locking) | Conflict detection and concurrent transaction handling | | [Multi-State Operations](/docs/velocity/multi-state) | Coordinating plan/apply across multiple Terraform states | --- # Setup Source: https://stategraph.com/docs/velocity/setup Set up Stategraph Velocity for parallel Terraform execution. Import existing state, configure your project, and start using parallel plan and apply. This guide shows you how to set up Velocity for parallel Terraform and OpenTofu execution. ## Prerequisites - Stategraph running (see [Deployment guide](/docs/deployment)) - Stategraph CLI installed (see [CLI documentation](/docs/cli)) - Terraform or OpenTofu installed - A Terraform project to configure ## Step 1: Create an API Key 1. Open Stategraph at 2. Log in (via local authentication or OAuth) 3. Navigate to **Settings** 4. In the **API Keys** section, click **Create API Key** 5. Copy the generated token and set it as an environment variable: ```bash export STATEGRAPH_API_KEY="" ``` ## Step 2: Configure the CLI Point the CLI at your Stategraph server: ```bash export STATEGRAPH_API_BASE="http://localhost:8080" ``` List your tenants to get your tenant ID: ```bash stategraph user tenants list ``` Output: ``` 550e8400-e29b-41d4-a716-446655440000 my-org ``` You can set `STATEGRAPH_TENANT_ID` to avoid passing `--tenant` on every command: ```bash export STATEGRAPH_TENANT_ID="550e8400-e29b-41d4-a716-446655440000" ``` ## Step 3: Navigate to Your Root Module All Velocity commands run from the root of the Terraform module you want to manage: ```bash cd /path/to/your/terraform/module ``` ## Step 4: Import Existing State (or Create a New One) **If you have existing Terraform state** (in S3, GCS, Terraform Cloud, or any backend), pull it to a local file and import it into Stategraph. This is a one-time operation — after this, Velocity manages state through the CLI: ```bash # Pull your current state to a local file terraform state pull > terraform.tfstate # Import it into Stategraph # The state name must not already exist — this command creates it stategraph import tf \ --tenant \ --name \ --var-file \ --var key=value \ terraform.tfstate ``` Run `stategraph import tf` from the root module directory and pass the path to the state file. If your Terraform configuration uses variables, pass the same `--var-file` and `--var` flags you use when running `terraform plan` or `terraform apply` so that Stategraph correctly captures your variable definitions. ### Replacing an existing state By default, `stategraph import tf` refuses to overwrite a state that already exists — the name must be new. If you need to re-import on top of an existing state (for example, after a failed first import or when migrating a state between environments), pass `--overwrite`: ```bash stategraph import tf --overwrite \ --tenant \ --name \ terraform.tfstate ``` This replaces the HCL and state data for that state in-place. Use it deliberately — the previous contents are gone after the import completes. **If you're starting fresh** (no existing state), create an empty state instead: ```bash stategraph states create --tenant --name ``` ### Adding workspaces If you use [Terraform workspaces](https://developer.hashicorp.com/terraform/language/state/workspaces) to manage environment variants (e.g., staging, production), create additional states in the same group. When `stategraph.json` already exists in the directory, the group ID is read automatically: ```bash # First state creates the group and writes stategraph.json stategraph states create --tenant --name my-state # Additional workspaces join the same group stategraph states create --tenant --name my-state-staging --workspace staging stategraph states create --tenant --name my-state-prod --workspace production ``` Then pass `--workspace` when planning or applying: ```bash stategraph tf plan --tenant --workspace staging --out plan.json stategraph tf apply plan.json ``` Stategraph rewrites `terraform.workspace` references in your HCL to the correct workspace value, so workspace-conditional logic works as expected. ## Step 5: Plan and Apply Once your state is set up, use Velocity to plan and apply changes: ```bash # Plan changes stategraph tf plan --tenant --out plan.json # Apply the plan stategraph tf apply plan.json ``` You can also apply in a single step — like `terraform apply` — by running `stategraph tf apply` with no plan file. It runs a fresh plan, shows the diff, and prompts for approval before committing: ```bash stategraph tf apply --tenant ``` Pass `--auto-approve` to skip the prompt. With a non-tty stdin or `--silent` (`STATEGRAPH_SILENT`) the apply fails unless `--auto-approve` is also given, so use both together in CI: ```bash stategraph tf apply --tenant --auto-approve ``` ## Version Control `stategraph.json` Velocity commands generate a `stategraph.json` file in your module directory. This file stores the group ID for your module. The CLI combines this group ID with the `--workspace` flag (defaulting to `"default"`) to resolve the correct state at runtime. **Commit this file to version control** so your team shares the same Stategraph state binding: ```bash git add stategraph.json git commit -m "Add Stategraph configuration" ``` ## Using Stategraph as a Terraform HTTP backend The `stategraph tf` commands above are the recommended workflow. Alternatively, you can point **vanilla `terraform`/`tofu`** at Stategraph using Terraform's HTTP backend — useful in CI/CD where you want to keep running `terraform apply` directly. Add a backend block to your configuration. The address ends in your group ID (a UUID, found in `stategraph.json`) and, optionally, a workspace (defaults to `default`): ```hcl terraform { backend "http" { address = "http://localhost:8080/api/v1/states/backend//" username = "stategraph" # any value; only the password (token) is used } } ``` Supply the token as the backend password via `TF_HTTP_PASSWORD` — either an API key or a per-run session token from `stategraph tx create-with-session`: ```bash # Use an API key… export TF_HTTP_PASSWORD="$STATEGRAPH_API_KEY" # …or mint a session token scoped to a single run export TF_HTTP_PASSWORD="$(stategraph tx create-with-session --tenant --tag pipeline=ci)" terraform init terraform apply ``` The HTTP backend implements GET and POST only — there is no Terraform `lock`/`unlock`. Stategraph manages concurrency with [transactions](/docs/velocity/transactions) (commit-time conflict detection), so you do not configure `lock_address`. ## Usage Velocity is used through the `stategraph tf` commands, which replace `terraform plan` and `terraform apply`: ```bash # Plan changes stategraph tf plan --tenant --out plan.json # Apply a saved plan stategraph tf apply plan.json # Plan and apply in one step, no saved plan file (--auto-approve skips the prompt) stategraph tf apply --tenant --auto-approve # Plan across multiple states in a single transaction stategraph tf mtx --tenant --out plan.json ./networking ./compute ./application ``` ### Configuration By default, Velocity uses `tofu` (OpenTofu) as the execution binary. Set the `TF_CMD` environment variable to use a different binary. ### Plan Options | Option | Description | |--------|-------------| | `--tenant` | Tenant ID (required, or set `STATEGRAPH_TENANT_ID`) | | `--out` | Output file for the plan. Optional for `tf plan` — omit it for a read-only preview (no file written); required for `tf mtx`. | | `--state` | State ID (auto-discovered from `stategraph.json` if not set) | | `--workspace` | Workspace name (default: `"default"`) | | `--var` | Set a variable (`key=value`, repeatable) | | `--var-file` | Path to variable file | | `--force` | Force a resource address into the plan, supports globs (e.g., `data.*`) | | Environment Variable | Description | |---------------------|-------------| | `STATEGRAPH_TENANT_ID` | Tenant ID (alternative to `--tenant` flag) | | `TF_CMD` | Path to Terraform/OpenTofu binary (default: `tofu`) | See [Terraform Commands](/docs/cli/tf) for the complete flag reference, including `--force-files`, `--skip-refresh`, and the cost-preview options. ## Other CLI Commands Lower-level transaction management is available via `stategraph tx`: ```bash # Create a transaction manually stategraph tx create --tenant # List transactions stategraph tx list --tenant # Abort a transaction stategraph tx abort --tx # View transaction logs stategraph tx logs list --tx ``` See [Transaction Commands](/docs/cli/transactions) for the full CLI reference and [API Reference](/docs/reference/api) for the REST API. --- # Transactions Source: https://stategraph.com/docs/velocity/transactions The Velocity transaction lifecycle for Terraform plan and apply: create a transaction, preview changes, check for conflicts, and commit updates. A Velocity transaction captures a set of infrastructure changes and tracks them through plan and apply. ## Lifecycle ``` Create → Preview (Plan) → Commit (Apply) ``` ### Transaction States | State | Description | |-------|-------------| | `open` | Transaction created, changes recorded | | `previewing` | Plan is being generated | | `committing` | Apply is in progress | | `committed` | Apply succeeded, state updated | | `failed` | Error occurred during preview or commit | | `aborted` | Cancelled by user | ## Create `stategraph tf plan` creates a transaction automatically when it detects HCL changes. You can also create transactions manually: ```bash stategraph tx create --tenant ``` Or mint a session token to use as the Terraform HTTP backend password (useful for CI/CD, where you run `terraform apply` directly): ```bash export TF_HTTP_PASSWORD=$(stategraph tx create-with-session --tenant ) ``` This token is the backend password — see [Using Stategraph as a Terraform HTTP backend](/docs/velocity/setup#using-stategraph-as-a-terraform-http-backend) for the required `backend "http"` block. ## Preview (Plan) ```bash stategraph tf plan --out plan.json ``` When a [pricing service](/docs/cost/setup) is configured, `stategraph tf plan` also prints a [cost delta](/docs/cost/plan-time) for the change — current vs planned monthly cost — right under the diff. Multiple previews can run concurrently against the same state. Each operates on an independent set of resources. For multi-state plans across directories: ```bash stategraph tf mtx --out plan.json ./networking ./compute ``` ## Commit (Apply) ```bash stategraph tf apply plan.json ``` You can also commit without a saved plan file: `stategraph tf apply` with no plan argument runs a fresh plan, shows the diff, and prompts for approval before committing (like `terraform apply`). Pass `--auto-approve` to skip the prompt — with a non-tty stdin or `--silent` (`STATEGRAPH_SILENT`) the apply fails unless `--auto-approve` is also given: ```bash stategraph tf apply --tenant --auto-approve ``` Before applying, Stategraph runs a [conflict check](/docs/velocity/resource-locking). If another transaction has committed changes to overlapping resources since this transaction was created, the commit is rejected with the conflicting transaction IDs. Re-run `stategraph tf plan` to pick up the new state and retry. ## Abort Cancel a transaction at any point before commit completes: ```bash stategraph tx abort --tx ``` ## Inspect ```bash # List transactions for a tenant stategraph tx list --tenant # View logs for a specific transaction stategraph tx logs list --tx ``` See [Transaction Commands](/docs/cli/transactions) for the full CLI reference including options, output formats, and CI/CD integration examples. ## API Reference The CLI wraps these API endpoints. Use the API directly for custom integrations. | Endpoint | Method | Description | |----------|--------|-------------| | `/api/v1/tenants/{tenant_id}/tx/create` | POST | Create a new transaction | | `/api/v1/tx/{tx_id}/preview` | POST | Start a preview (plan) | | `/api/v1/tx/{tx_id}/commit` | POST | Commit (apply) the transaction | | `/api/v1/tx/{tx_id}/abort` | POST | Abort the transaction | --- # Resource-Level Locking Source: https://stategraph.com/docs/velocity/resource-locking How Velocity uses resource-level conflict detection instead of state-level locks, enabling concurrent operations on the same Terraform state. Velocity uses resource-level conflict detection instead of state-level locks. Two transactions only conflict if they modify overlapping resources. ## Conflict Detection At commit time, Stategraph checks whether any other concurrent transaction touches the same resources. A transaction conflicts with another if: - The other transaction is currently committing and touches overlapping resources - The other transaction committed after the current transaction was created and touches overlapping resources Transactions that touch different resources never conflict, regardless of whether they operate on the same state. There is one refinement: if two transactions overlap only on a resource that each merely *depends on* — neither one modifies it — they do not conflict. Sharing a read-only dependency is a safe direction. ## Conflict Handling If a conflict is detected at commit time: 1. The commit is rejected (HTTP 409) 2. The response includes the conflicting transaction IDs 3. Re-run `stategraph tf plan` to incorporate the latest state 4. Retry the commit ## Concurrency Non-overlapping transactions run in parallel through the entire lifecycle: - **Concurrent plans** — Multiple transactions can plan simultaneously against the same state - **Concurrent applies** — Transactions touching different resources apply simultaneously - **Cross-state** — Conflict detection works across state boundaries for [multi-state transactions](/docs/velocity/multi-state) --- # Multi-State Operations Source: https://stategraph.com/docs/velocity/multi-state How Velocity coordinates plan and apply across multiple Terraform states in a single transaction with cross-state dependency resolution. A single transaction can span multiple Terraform states. Cross-state dependencies are resolved automatically. ## Usage Use `stategraph tf mtx` to plan across multiple state directories: ```bash stategraph tf mtx --tenant --out plan.json ./networking ./compute ./application ``` Each directory must contain a `stategraph.json`. `mtx` resolves that file to its group and plans **all workspaces in the group**, so a directory contributes every workspace in its group, not necessarily a single state. Apply works the same as a single-state plan: ```bash stategraph tf apply plan.json ``` ## Cross-State Dependencies When a transaction spans states, `terraform_remote_state` data sources are resolved automatically. Resources from different states that reference each other are planned and applied together in the correct dependency order. ## Guarantees - **Single plan, single apply** — All states are planned and applied as a single Terraform run, each state wired in as a module, not state-by-state - **Cross-state conflict detection** — [Resource-level locking](/docs/velocity/resource-locking) detects overlapping changes across states at commit time - **Automatic dependency resolution** — `terraform_remote_state` references between states are resolved within the merged plan --- # Introduction Source: https://stategraph.com/docs/insights Analysis and exploration tools for your Terraform infrastructure: timeline, search, blast radius, graph visualization, and dependency tracking. Insights provides analysis and exploration tools for understanding your Terraform-managed infrastructure. Track changes over time, search across resources, analyze impact, and visualize dependencies. ## Features ### Timeline Track every change to your Terraform state: - Transaction history with timestamps - User activity tracking - Filter by date range or user - See exactly what changed [View Timeline](/docs/insights/timeline) ### Search Find resource instances across all your states by field: - Filter by type, module, provider, or address - Match attribute values - Search spans every state - Jump straight to instance details, blast radius, or the graph [Use Search](/docs/insights/search) ### Blast Radius Understand the impact of changing a resource: - See all dependent resources - Identify high-impact changes - Plan modifications safely - Visualize impact scope [Learn Blast Radius](/docs/insights/blast-radius) ### Graph Explorer Interactive dependency visualization: - Pan and zoom - Click to select resources - Filter by type or module - Trace dependency chains [Explore Dependencies](/docs/insights/graph-explorer) ## Navigation In the Stategraph UI, the Insights section is accessible from the left sidebar: ``` Insights ├── Timeline → Transaction history ├── Search → Find instances by field ├── Blast Radius → Impact analysis └── Graph → Dependency visualization ``` ## Use Cases ### Change Tracking Use Timeline to: - Review what changed and when - Identify who made changes - Audit infrastructure modifications - Debug unexpected state changes ### Resource Discovery Use Search to: - Find resources by name or type - Locate configuration values - Cross-reference across states - Quick navigation to resources ### Change Planning Use Blast Radius to: - Assess impact before making changes - Identify critical dependencies - Plan safe modification order - Communicate scope to stakeholders ### Architecture Understanding Use Graph Explorer to: - Visualize resource relationships - Understand module structure - Identify isolated resources - Document infrastructure ## Quick Start ### 1. View Recent Changes Navigate to **Insights** > **Timeline** to see recent transactions: - Each row shows a state change - Click to expand and see details - Filter by time period ### 2. Search for Resources Use **Insights** > **Search** to find resources: - Type a resource name or type - Results appear instantly - Click to navigate to resource ### 3. Check Impact Before changing a resource, use **Blast Radius**: 1. Select a state 2. Choose a resource 3. View dependent resources 4. Assess change impact ### 4. Explore Dependencies Use **Graph Explorer** to visualize: 1. Select a state 2. View the dependency graph 3. Click nodes to see details 4. Filter to focus on specific resources ## Insights vs Inventory **Insights** focuses on analysis and exploration: - Timeline, Search, Blast Radius, Graph Explorer - Answering "what happened?" and "what if?" **Inventory** focuses on cataloging and querying: - Instances, Modules, Types, Query, Dashboards, Gap Analysis - Answering "what do we have?" ## Documentation | Topic | Description | |-------|-------------| | [Timeline](/docs/insights/timeline) | Transaction history | | [Search](/docs/insights/search) | Find instances by type, module, provider, address | | [Blast Radius](/docs/insights/blast-radius) | Impact analysis | | [Graph Explorer](/docs/insights/graph-explorer) | Dependency visualization | | [Dashboard Metrics](/docs/insights/dashboard-metrics) | Metrics on the home Infrastructure Overview dashboard | --- # Timeline Source: https://stategraph.com/docs/insights/timeline Track transaction history and changes to your Terraform state. Visual timeline of all infrastructure modifications with detailed change tracking. The Timeline provides a complete history of all changes to your Terraform states, enabling you to track who changed what and when. ## Overview Every Terraform operation that modifies state is recorded as a transaction: - `terraform apply` operations - State imports - Resource modifications - State migrations Timeline shows these transactions chronologically with detailed information about each change. ## Accessing Timeline ### Via UI 1. Navigate to **Insights** > **Timeline** in the sidebar 2. Browse transactions chronologically 3. Click a transaction to see details 4. Use filters to narrow results ### Via CLI First, get your tenant ID: ```bash stategraph user tenants list ``` Output: ``` 550e8400-e29b-41d4-a716-446655440000 my-org ``` List transactions for a tenant: ```bash stategraph tx list --tenant 550e8400-e29b-41d4-a716-446655440000 ``` Response: ```json { "results": [ { "id": "455fe705-f27f-4335-9355-dbe8f14098df", "created_at": "2024-01-15T10:30:00Z", "created_by": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "completed_at": "2024-01-15T10:30:05Z", "completed_by": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "state": "committed", "tags": { "desc": "Backend update" } } ] } ``` Get transaction logs: ```bash stategraph tx logs list --tx 455fe705-f27f-4335-9355-dbe8f14098df ``` Response: ```json { "results": [ { "id": "log-123", "action": "state_set", "object_type": "instance", "created_at": "2024-01-15T10:30:00Z", "state_id": "state-123", "data": { ... } } ] } ``` ## Transaction Properties | Property | Description | |----------|-------------| | `id` | Unique transaction ID | | `created_at` | When the transaction started | | `created_by` | User or system that initiated | | `completed_at` | When the transaction finished | | `completed_by` | User or system that completed | | `state` | Transaction state (open, committed, aborted) | | `tags` | Metadata tags (pipeline info, commit, etc.) | ## Transaction States | State | Description | |-------|-------------| | `open` | Transaction in progress | | `committed` | Successfully finished | | `aborted` | Cancelled or failed | ## Filtering Timeline ### By Time Period Filter transactions by date range: - Last hour - Last 24 hours - Last 7 days - Last 30 days - Custom range ### By User Filter to see changes by specific users: ``` Created by: user@example.com ``` ### By State Filter to transactions affecting specific states: ``` State: networking/production ``` ### By Tags Filter by transaction tags: ``` Tag: pipeline=github-actions ``` ## Transaction Details Clicking a transaction shows: ### Summary - Transaction ID - Start and end times - Duration - User information - Tags ### Logs Detailed log of changes: | Field | Description | |-------|-------------| | `action` | Type of change (state_set, hcl_set, tfvar_set, file_set, and their `_delete` variants) | | `object_type` | What was changed (instance, resource, output, provider, state_metadata) | | `state_id` | Affected state | | `data` | Change details | ### Affected Resources List of resources modified in this transaction: - Resources added - Resources modified - Resources removed ## Use Cases ### Audit Trail Track who made changes for compliance: 1. Filter by time period 2. Export transaction list 3. Review user activity 4. Document changes ### Debugging Understand unexpected state changes: 1. Identify when a change occurred 2. See what was changed 3. Find who made the change 4. Review the context (tags, pipeline) ### Rollback Planning Before reverting changes: 1. Find the transaction that caused issues 2. Review what was changed 3. Plan the rollback 4. Execute and verify ## Transaction Tags Tags provide context about transactions: ### CI/CD Integration When running Terraform from CI/CD, add tags: ```hcl # In your Terraform backend config # Tags are passed via the API during state operations ``` Common tags: | Tag | Example | Purpose | |-----|---------|---------| | `pipeline` | `github-actions` | CI/CD system | | `commit` | `abc123` | Git commit | | `branch` | `main` | Git branch | | `run_id` | `12345` | CI run ID | | `triggered_by` | `push` | Trigger type | ### Creating Transactions with Tags Via CLI: ```bash stategraph tx create \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --tag pipeline=github-actions \ --tag commit=abc123 \ --tag branch=main ``` ## Best Practices ### 1. Use Meaningful Tags Add context to transactions: ```json { "tags": { "pipeline": "terraform-ci", "commit": "abc123", "ticket": "INFRA-456", "environment": "production" } } ``` ### 2. Regular Review - Check Timeline weekly for unexpected changes - Monitor for unauthorized modifications - Verify CI/CD transactions complete successfully ### 3. Export for Compliance Periodically export transaction data: - Monthly audit reports - Compliance documentation - Change management records ### 4. Correlate with Incidents When issues occur: 1. Note the time of the incident 2. Check Timeline for recent changes 3. Identify potentially related transactions 4. Review change details ## Monitoring ### Open Transactions Monitor for stuck transactions: ```bash stategraph tx list --tenant 550e8400-e29b-41d4-a716-446655440000 | \ jq '.results[] | select(.state == "open")' ``` ### Abort Stuck Transactions If a transaction is stuck: ```bash stategraph tx abort --tx 455fe705-f27f-4335-9355-dbe8f14098df ``` ## Next Steps - [Search](/docs/insights/search) - Find resources quickly - [Blast Radius](/docs/insights/blast-radius) - Analyze change impact --- # Search Source: https://stategraph.com/docs/insights/search Find and filter resource instances across all your states by type, module, provider, address, and attribute values from the Insights view. Search is the Insights view for quickly finding resource **instances** across all your states. It filters by structured fields — resource type, module, provider, address, and attribute values — and links straight to instance details, blast radius, and the dependency graph. Search is field-scoped, not free-form full-text: you match on specific fields rather than indexing arbitrary text. For aggregation, joins, and complex conditions, use [Query (SQL)](/docs/inventory/query). ## What you can filter by | Field | Matches | |-------|---------| | `type` | Resource type (e.g. `aws_instance`) | | `module` | Module path | | `provider` | Provider | | `address` | Full instance address | | `resource_address` | Resource address (without index) | | `attr::` | An attribute value (JSON containment) | ## Via the UI 1. Navigate to **Insights → Search** in the sidebar. 2. Add filters for type, module, provider, or address. 3. Click a result to open instance details — and from there, [Blast Radius](/docs/insights/blast-radius) or the [Graph Explorer](/docs/insights/graph-explorer). The UI searches across every state in the tenant, so a single filter surfaces matching instances wherever they live. ## Via the API Search is backed by the instances endpoint, which accepts a `q` tag-query of space-separated `key:value` terms (per state): ```bash # EC2 instances curl "http://localhost:8080/api/v1/states/$STATE_ID/instances?q=type:aws_instance" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" # Instances in a module with a specific attribute value curl "http://localhost:8080/api/v1/states/$STATE_ID/instances?q=module:vpc%20attr:instance_type:t3.micro" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" ``` Supported keys: `type`, `module`, `provider`, `address`, `resource_address`, and `attr::` (matches instances whose `attributes` JSON contains that key/value). Combine terms with spaces. Results are paginated with `page` and `limit` (default 20). ## Search vs Query | | Search | [Query (SQL)](/docs/inventory/query) | |--|--------|--------------------------------------| | Input | Field filters (`type:`, `module:`, `attr:…`) | SQL / MQL | | Best for | Quickly finding and navigating to instances | Aggregation, joins, complex filters, export | | Scope | Instances | Any table (resources, instances, transactions, costs, …) | Use **Search** to find a resource fast; use **Query** when you need to aggregate, join, or export. ## Next Steps - [Blast Radius](/docs/insights/blast-radius) - Analyze the impact of a change - [Graph Explorer](/docs/insights/graph-explorer) - Visualize dependencies - [Query (SQL)](/docs/inventory/query) - Full SQL/MQL analysis - [Instances](/docs/inventory/instances) - Browse all instances --- # Blast Radius Analysis Source: https://stategraph.com/docs/insights/blast-radius Understand the impact of changing resources before making modifications. Analyze dependencies and blast radius to prevent unexpected failures. Blast radius analysis shows all resources that would be affected if a specific resource changes or is destroyed. ## What is Blast Radius? When you modify a resource in Terraform, dependent resources may also need to be updated or recreated. The "blast radius" is the set of all resources affected by a change. ``` Change to aws_vpc.main │ ▼ ┌────────────────────────────────────────┐ │ Blast Radius │ │ │ │ aws_subnet.public │ │ aws_subnet.private │ │ aws_security_group.web │ │ aws_instance.web │ │ aws_db_subnet_group.main │ │ aws_rds_cluster.database │ │ │ └────────────────────────────────────────┘ ``` ## Accessing Blast Radius ### Via UI 1. Navigate to a state 2. Select a resource (click on it in the list or graph) 3. Click **Blast Radius** or **Impact Analysis** 4. View all affected resources ### Via CLI First, get your tenant and state IDs: ```bash # List your tenants stategraph user tenants list ``` Output: ``` 550e8400-e29b-41d4-a716-446655440000 my-org ``` ```bash # List states for a tenant stategraph states list --tenant 550e8400-e29b-41d4-a716-446655440000 --format json ``` Output: ```json { "results": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "networking", "workspace": "production" } ] } ``` Then query blast radius: ```bash # For aws_instance.web stategraph states instances blast-radius \ --state a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ "aws_instance.web" # For module.vpc.aws_subnet.public[0] stategraph states instances blast-radius \ --state a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ "module.vpc.aws_subnet.public[0]" ``` Output: ``` aws_instance.web aws_instance.web 0 1 aws_eip.web aws_eip.web 0 2 ``` ## Understanding Results ### Distance The `distance` field indicates how many hops away a resource is in the dependency chain: | Distance | Meaning | |----------|---------| | 1 | Directly depends on the target resource | | 2 | Depends on something that depends on the target | | 3+ | Further downstream in the chain | ### Impact Levels Resources closer in distance are more likely to be affected: - **Distance 1**: Will definitely be affected - **Distance 2-3**: Likely to be affected - **Distance 4+**: May be affected depending on change type ## Use Cases ### Pre-Change Assessment Before modifying critical infrastructure: 1. Select the resource you plan to change 2. View blast radius 3. Identify all affected resources 4. Plan change window based on impact ### Risk Assessment Identify high-risk resources: 1. Compare blast radius sizes across resources 2. Resources with large blast radius are higher risk 3. Prioritize extra caution for these changes ### Outage Planning When planning maintenance: 1. Check blast radius of each component 2. Identify if changes cascade to critical services 3. Plan communication and rollback procedures ## High-Risk Patterns ### Network Foundation Resources VPCs, subnets, and security groups often have large blast radius: ``` aws_vpc.main ├── aws_subnet.public (20+ resources) ├── aws_subnet.private (30+ resources) ├── aws_security_group.web (15+ resources) └── aws_internet_gateway.main (10+ resources) ``` ### IAM Roles Roles used by many services: ``` aws_iam_role.application ├── aws_lambda_function.api (Distance 1) ├── aws_ecs_task_definition.worker (Distance 1) ├── aws_codebuild_project.build (Distance 1) └── ... (many more) ``` ### Data Resources Databases and storage with dependent services: ``` aws_rds_cluster.main ├── aws_rds_cluster_instance.primary ├── aws_rds_cluster_instance.replica ├── aws_secretsmanager_secret.db_credentials └── aws_lambda_function.processor ``` ## Minimizing Blast Radius ### Design Patterns **Loose Coupling**: Use data sources instead of direct references: ```hcl # Higher coupling (larger blast radius) resource "aws_instance" "web" { subnet_id = aws_subnet.main.id } # Lower coupling data "aws_subnet" "main" { tags = { Name = "main" } } resource "aws_instance" "web" { subnet_id = data.aws_subnet.main.id } ``` **Module Boundaries**: Encapsulate related resources: ```hcl module "networking" { source = "./modules/networking" } module "compute" { source = "./modules/compute" subnet_id = module.networking.subnet_id # Single connection point } ``` **Smaller States**: Split large states into smaller, focused states: ``` networking/ → VPCs, subnets, security groups compute/ → EC2 instances, ASGs data/ → RDS, DynamoDB ``` ## Comparing Blast Radius ### Between Resources Compare blast radius to prioritize changes: | Resource | Blast Radius Size | Risk Level | |----------|------------------|------------| | aws_vpc.main | 45 resources | High | | aws_security_group.web | 12 resources | Medium | | aws_instance.worker | 2 resources | Low | ### Between Workspaces Same resource may have different blast radius across environments: | Workspace | aws_vpc.main Blast Radius | |-----------|--------------------------| | Production | 150 resources | | Staging | 50 resources | | Dev | 10 resources | ## SQL Queries ### Find Resources with Large Blast Radius Currently, blast radius requires CLI calls per resource. You can script this: ```bash #!/bin/bash STATE_ID="a1b2c3d4-e5f6-7890-abcd-ef1234567890" # Get all instance addresses stategraph states instances query --state "$STATE_ID" --format json -i "true" | \ jq -r '.results[].address' | while read instance; do count=$(stategraph states instances blast-radius --state "$STATE_ID" "$instance" 2>/dev/null | wc -l) echo "$instance: $count" done | sort -t: -k2 -rn | head -20 ``` ### Find Highly Connected Resources ```sql -- Resources with many dependencies (many things depend on them) SELECT address, array_length(dependencies, 1) as dep_count FROM instances WHERE dependencies IS NOT NULL ORDER BY dep_count DESC LIMIT 20 ``` ## Best Practices 1. **Check before apply** - Always check blast radius for production changes 2. **Prefer small changes** - Multiple small changes are safer than one large change 3. **Document high-risk resources** - Mark resources with large blast radius 4. **Test in staging** - Verify changes don't cascade unexpectedly 5. **Plan rollback** - Know how to recover if blast radius was underestimated ## Limitations - Blast radius shows structural dependencies, not runtime dependencies - External dependencies (DNS, external APIs) are not tracked - Changes that don't affect the dependency graph may still cause issues ## Next Steps - [Graph Explorer](/docs/insights/graph-explorer) - Visualize dependencies - [Timeline](/docs/insights/timeline) - Track changes - [Query](/docs/inventory/query) - Query with SQL --- # Graph Explorer Source: https://stategraph.com/docs/insights/graph-explorer Visualize resource dependencies in your Terraform state. Root-centric graph exploration with dependency tracing and blast-radius links. Graph Explorer provides interactive visualization of dependency relationships between resources in your Terraform state. ## Overview Terraform tracks dependencies between resources to determine the order of operations. Stategraph parses these dependencies and provides interactive visualization to help you understand your infrastructure. ## How Dependencies Work ### Explicit Dependencies Resources declare dependencies in Terraform configuration: ```hcl resource "aws_instance" "web" { ami = "ami-12345" instance_type = "t3.micro" subnet_id = aws_subnet.main.id # Creates dependency } ``` ### Implicit Dependencies Terraform creates dependencies when resources reference each other: ```hcl resource "aws_security_group" "web" { vpc_id = aws_vpc.main.id # Depends on VPC } resource "aws_instance" "web" { security_groups = [aws_security_group.web.id] # Depends on SG } ``` ### Dependency Chain Dependencies form a directed acyclic graph (DAG): ``` aws_vpc.main │ ├── aws_subnet.public │ │ │ └── aws_instance.web │ └── aws_security_group.web │ └── aws_instance.web ``` ## Accessing Graph Explorer ### Via UI 1. Navigate to **Insights** > **Graph** in the sidebar 2. Select a state, then choose a specific resource instance to root the graph 3. Stategraph renders a focused dependency graph around that instance ### Via State Page 1. Navigate to a state in the UI 2. Click the **Graph** or **Visualization** tab 3. Explore the state's dependency graph ## Graph Interaction ### Navigation - **Pan**: Click and drag the background - **Zoom**: Mouse wheel or pinch gesture - **Reset**: Double-click background to reset view ### Node Selection - **Click node**: Select and highlight - **Double-click**: Expand/collapse connected nodes - **Right-click**: Context menu with actions ### Scoping the Graph Graph Explorer is root-centric: it renders the dependency graph around a single instance you select, not the entire state at once. To keep graphs readable, it enforces a connection-count limit. When the graph around your chosen instance would be too densely connected, Graph Explorer refuses to render it and points you to the [Blast Radius](/docs/insights/blast-radius) page instead, which is built for large impact sets. ## Understanding the Visualization ### Node Representation | Element | Meaning | |---------|---------| | Node color | Relationship to the selected root (root, dependency, or dependent) | | Node size | Number of connections | | Node label | Resource address | ### Edge Representation | Element | Meaning | |---------|---------| | Arrow direction | Dependency direction (A → B means A depends on B) | | Edge thickness | Can indicate relationship strength | | Edge color | Dependency type | ### Color Coding Nodes are colored by their relationship to the selected root instance, not by resource-type category: | Color | Node | Meaning | |-------|------|---------| | Accent | Root | The instance you selected to focus the graph | | Neutral (dependency shade) | Dependency | A resource the root depends on | | Neutral (dependent shade) | Dependent | A resource that depends on the root | ## Common Patterns ### Hub Resources Resources with many dependents (high in-degree): ``` ┌── aws_instance.web1 │ aws_vpc.main ── aws_instance.web2 │ └── aws_rds_cluster.db ``` These are critical resources - changes affect many others. Use [Blast Radius](/docs/insights/blast-radius) to assess impact. ### Leaf Resources Resources with no dependents (zero out-degree): ``` aws_security_group.web ── aws_instance.web (leaf) ``` These are safer to modify - fewer downstream effects. ### Module Clusters Resources within modules form natural clusters: ``` ┌─────────────────────────┐ │ module.vpc │ │ ┌───────┐ ┌────────┐ │ │ │ vpc │──│ subnet │ │ │ └───────┘ └────────┘ │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ module.compute │ │ ┌──────────┐ │ │ │ instance │ │ │ └──────────┘ │ └─────────────────────────┘ ``` ## Use Cases ### Understanding Architecture See how your infrastructure is organized: 1. Open the graph for a state 2. Identify major resource groups 3. Trace data flow through components ### Change Planning Before modifying a resource: 1. Select the resource in the graph 2. View all dependent resources 3. Assess impact of the change 4. See [Blast Radius](/docs/insights/blast-radius) for detailed analysis ### Documentation Generate architecture diagrams: 1. Select a root resource 2. Pan and zoom to frame the relationships 3. Export or screenshot ### Debugging Understand why operations happen in certain order: 1. Select a resource 2. View its dependencies 3. Understand the execution graph ## CLI Access ### Get Instances with Dependencies ```bash stategraph states instances query \ --state a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ --format json \ -i "true" ``` Response includes dependency information: ```json { "results": [ { "address": "aws_instance.web", "type": "aws_instance", "dependencies": [ "aws_subnet.main", "aws_security_group.web" ] } ] } ``` ### Query Dependencies with SQL ```sql -- Find all dependencies of a resource SELECT dependencies FROM instances WHERE address = 'aws_instance.web' -- Find resources that depend on a specific resource SELECT address FROM instances WHERE 'aws_vpc.main' = ANY(dependencies) ``` ## Graph Layout The graph uses a hierarchical (dagre) layout: - Nodes are arranged in dependency tiers (top-to-bottom or left-to-right) - Edges flow in a consistent direction - Related resources stay near their dependencies Pan and zoom to navigate. The layout is computed automatically and cannot be rearranged by dragging nodes. ## Tips 1. **Start from a focused root** - Pick a single instance to anchor the graph 2. **Focus on modules** - Module boundaries provide natural groupings 3. **Follow the arrows** - Dependencies point from dependent to dependency 4. **Use blast radius** - For detailed impact analysis 5. **Compare states** - View graphs for different workspaces side by side ## Limitations - Densely connected graphs are refused rather than rendered; use [Blast Radius](/docs/insights/blast-radius) for large impact sets - Some dependencies may not be visible if they're implicit - Module dependencies are shown, but module internals require expanding ## Next Steps - [Blast Radius](/docs/insights/blast-radius) - Detailed impact analysis - [Search](/docs/insights/search) - Find resources quickly - [Query](/docs/inventory/query) - Query dependencies with SQL --- # Dashboard Metrics Source: https://stategraph.com/docs/insights/dashboard-metrics Reference for every metric on the Infrastructure Overview dashboard. Detailed explanations of resource counts, state health, and infrastructure statistics. The Infrastructure Overview dashboard shows metrics computed from the dependency graph and state metadata. This page explains what each metric means, how it is calculated, and what to look for. ## Control Plane Status These four cards reflect the operational state of your Terraform transactions. ### Active Transactions Number of Terraform transactions currently in progress against this state. An active transaction means a `terraform plan` or `terraform apply` is running. A count of zero means the system is idle. ### Last Transaction The outcome of the most recent completed transaction: | Status | Meaning | |--------|---------| | **Committed** | The transaction was applied successfully. | | **Aborted** | The transaction was manually cancelled before completion. | | **Failed** | The transaction encountered an error during apply. | The relative time shown below indicates how recently the transaction completed. ### Recent Changes A summary of transaction activity over a rolling time window (typically the last 24 hours). Shows the count of committed, aborted, and failed transactions. Use this to gauge how actively the state is being modified and whether failures are occurring. ### Last Change Impact The blast radius of the most recent committed transaction — the number of resources that were directly or transitively affected by the change. A high number indicates a wide-reaching modification; zero means no recent changes. ## State Structure These four cards summarize the shape and size of your Terraform state. ### Resources Total number of resource instances tracked in the current Terraform state. Each instance maps to one real infrastructure object (e.g., an S3 bucket, an IAM role). ### Modules Number of distinct Terraform modules used in this state. Modules group related resources into reusable, composable units. A high count may indicate a well-factored configuration. ### Providers Number of different cloud or service providers (e.g., AWS, GCP, Azure) referenced by resources in this state. Multi-provider states have broader infrastructure scope. ### Graph Density Total dependency edges divided by total resource instances, expressed as **edges/resource**. | Value | Interpretation | |-------|---------------| | **< 1.0** | Sparse — most resources are standalone or have few dependencies. | | **~1.0** | Each resource has roughly one dependency on average. Common for simple configurations. | | **> 2.0** | Heavily interconnected — changes are more likely to cascade across multiple resources. | For example, a state with 50 resources and 75 dependency edges has a density of 1.50 edges/res. ## Risk & Dependency Analysis These cards highlight structural risks in the dependency graph. ### Largest Dependency Blast Radius The single resource whose change would affect the most other resources. Stategraph finds this by traversing all transitive dependencies for every resource and selecting the one with the largest reach. Click the card to open a blast radius visualization for that resource. ### Top Dependency Hotspot The resource that the most other resources depend on (highest inbound reference count). A high count means changes to this resource could cascade widely. This is different from blast radius — hotspot counts how many resources *point to* this one, while blast radius counts how many resources are *reachable from* a given one. ### Graph Structure - **Roots**: Resources with no dependencies — the entry points of the dependency graph. These are typically foundational resources (VPCs, projects, resource groups). - **Leaves**: Resources that nothing else depends on — the terminal nodes. Leaves are often end-user-facing resources (DNS records, load balancers). ## Composition These panels show how your resources are distributed across types and providers. ### Resource Type Distribution Breakdown of resource instances by Terraform type, ranked by count. The top five types are shown with bar charts. Click a row to view all resources of that type in the inventory. A heavily skewed distribution may indicate repetitive infrastructure (e.g., many security group rules). ### Provider Distribution Breakdown of resource instances by cloud or service provider. Single-provider states show just the provider name. Multi-provider states show a stacked bar chart and percentage breakdown. Click a provider to filter the inventory. ## Control Plane Insights These metrics are computed from the dependency graph and state metadata. They surface information that the Terraform CLI does not show. ### Largest Module The Terraform module containing the most resources. Extremely large modules may benefit from decomposition into smaller, focused units for better maintainability and blast radius isolation. ### Most Deployed Module The module that appears the most times across different resource instances. High reuse indicates a well-designed shared module. Low reuse alongside many modules may suggest duplicated patterns that could be consolidated. ### Top Resource Type The Terraform resource type (e.g., `aws_security_group_rule`, `aws_iam_policy`) with the most instances in this state. ### Orphaned Candidates Resources flagged by graph analysis as potentially unused. A resource is considered an orphaned candidate when no other resource depends on it (leaf node) and it is either outside any module or has at most one dependency. Resources inside a module with multiple dependencies are excluded because modules typically bundle related resources intentionally. Not every flagged resource is actually orphaned — review each one to decide whether to remove it or leave it in place. ### Provider Distribution (Chart) A doughnut chart showing the percentage breakdown of resources across providers. The top four providers are shown individually; the rest are grouped as "Other." ### Top Resource Types (Chart) A horizontal bar chart showing the five most common resource types and their instance counts. --- # Introduction Source: https://stategraph.com/docs/inventory Infrastructure catalog and CMDB for your Terraform resources. Browse instances, modules, types, run SQL queries, and build custom dashboards. Inventory provides a complete catalog of your Terraform-managed infrastructure. Browse resources, modules, and types. Query your data with SQL. Build dashboards and find resources that aren't managed by Terraform. ## Features ### Browse Resources - **[Instances](/docs/inventory/instances)** - All resource instances across all states - **[Modules](/docs/inventory/modules)** - Terraform module breakdown - **[Resource Types](/docs/inventory/resource-types)** - Resources grouped by type ### Query & Analyze - **[Query (SQL)](/docs/inventory/query)** - SQL queries across your infrastructure - **[Dashboards](/docs/inventory/dashboards)** - Save and share custom views - **[Gap Analysis](/docs/inventory/gap-analysis)** - Find unmanaged cloud resources ## Navigating Inventory In the Stategraph UI, the Inventory section is accessible from the left sidebar. It provides: ``` Inventory ├── Instances → Browse all resource instances ├── Modules → View Terraform modules ├── Types → Resources by type ├── Query → Run SQL queries ├── Dashboards → Custom dashboard views └── Gap Analysis → Unmanaged resource detection ``` ## Use Cases ### Infrastructure Catalog Use Inventory as your infrastructure CMDB: - Centralized view of all Terraform-managed resources - Search and filter across all states and workspaces - View resource attributes and dependencies - Track resource counts by type, module, or provider ### Compliance & Auditing - Query resources by tags or attributes - Find resources missing required tags - Identify resources by region or account - Generate reports for compliance reviews ### Resource Discovery - Find resources across multiple Terraform states - Identify duplicate or similar resources - Discover resources by naming patterns - Gap analysis to find unmanaged resources ## Quick Examples ### Find all EC2 instances Navigate to **Types** and select `aws_instance`, or run this query: ```sql SELECT i.address, i.attributes->>'instance_type' AS instance_type FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_instance' ``` ### Count resources by type ```sql SELECT type, count(*) AS total FROM resources GROUP BY type ORDER BY type ``` ### Find untagged resources ```sql SELECT address FROM instances WHERE attributes->'tags' IS NULL OR attributes->>'tags' = '{}' ``` ## Getting Started 1. **[Deploy Stategraph](/docs/deployment/docker-compose)** if you haven't already 2. **[Set up Velocity](/docs/velocity/setup)** to populate inventory 3. **Browse Instances** to see your resources 4. **Try a Query** to explore your data ## Next Steps - [Instances](/docs/inventory/instances) - Browse resource instances - [Query Language](/docs/inventory/query) - Learn SQL syntax - [Gap Analysis](/docs/inventory/gap-analysis) - Find unmanaged resources --- # Instances Source: https://stategraph.com/docs/inventory/instances Browse all resource instances across your Terraform states. Search, filter, and inspect detailed resource attributes and metadata. The Instances view provides a comprehensive catalog of all resource instances across all your Terraform states. ## Overview Instances are individual resources managed by Terraform. For example, if you have: ```hcl resource "aws_instance" "web" { count = 3 # ... } ``` This creates three instances: `aws_instance.web[0]`, `aws_instance.web[1]`, `aws_instance.web[2]`. ## Accessing Instances ### Via UI 1. Navigate to **Inventory** > **Instances** in the sidebar 2. Browse the list of all instances 3. Use filters to narrow results 4. Click an instance to view details ### Via API First, get your state ID: ```bash export STATEGRAPH_API_KEY="" TENANT_ID=$(curl -s -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ http://localhost:8080/api/v1/user/tenants | jq -r '.results[0].id') STATE_ID=$(curl -s -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ "http://localhost:8080/api/v1/tenants/$TENANT_ID/states" | jq -r '.results[0].id') ``` Then list instances: ```bash curl "http://localhost:8080/api/v1/states/$STATE_ID/instances" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" ``` Response: ```json { "results": [ { "address": "aws_instance.web[0]", "type": "aws_instance", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "attributes": { ... }, "dependencies": ["aws_subnet.main", "aws_security_group.web"] } ] } ``` A root-module resource has `address`, `type`, `provider`, `attributes`, and `dependencies`. The `module` and `sensitive_attributes` keys are included only when they have a value, so they are omitted for root resources and appear only for resources inside a module (or with sensitive attributes). ## Instance Properties | Property | Description | |----------|-------------| | `address` | Full Terraform address (e.g., `module.vpc.aws_subnet.private[0]`) | | `type` | Resource type (e.g., `aws_instance`, `google_compute_instance`) | | `provider` | Provider that manages this resource | | `module` | Module path if inside a module, null for root | | `attributes` | JSON object of all resource attributes | | `dependencies` | List of resources this instance depends on | ## Filtering Instances When querying with SQL, note that the `instances` table holds per-instance data (`attributes`, `dependencies`, `status`). Resource `type`, `provider`, and `module` live on the **`resources`** table — query `resources` directly, or join on `resource_address` (see [Query](/docs/inventory/query#cross-table-queries)). (The REST `/instances` response above includes `type`/`provider`/`module` for convenience.) ### By Resource Type Filter to show only specific resource types: ``` Type: aws_instance ``` Or use SQL (resource type lives on `resources`): ```sql SELECT * FROM resources WHERE type = 'aws_instance' ``` ### By Provider Filter by cloud provider. The `provider` value is the full Terraform provider source address (e.g. `provider["registry.terraform.io/hashicorp/aws"]`), so match it with `LIKE`: ```sql -- AWS resources SELECT * FROM resources WHERE provider LIKE '%hashicorp/aws%' -- Google Cloud resources SELECT * FROM resources WHERE provider LIKE '%hashicorp/google%' ``` ### By Module Find resources inside modules (the `module` column is on `resources`): ```sql -- All resources in modules SELECT * FROM resources WHERE module IS NOT NULL -- Resources in a specific module SELECT * FROM resources WHERE module = 'module.vpc' -- Root module only SELECT * FROM resources WHERE module IS NULL ``` ### By State Filter by Terraform state. `state_id` is the state's UUID (look it up by name in the `states` table): ```sql SELECT * FROM instances WHERE state_id = '550e8400-e29b-41d4-a716-446655440000' ``` To filter by state name instead, join to `states`: ```sql SELECT * FROM instances AS i INNER JOIN states AS s ON i.state_id = s.id WHERE s.name = 'networking' ``` ### By Address Pattern Search by address: ```sql SELECT * FROM instances WHERE address = 'aws_db_instance.database' SELECT * FROM instances WHERE address LIKE 'module.vpc.%' ``` ## Instance Details Clicking an instance shows: ### Overview - Resource address - Resource type - Provider - Module path - State name and workspace ### Attributes - Full JSON attributes from Terraform state - Nested attribute navigation - Attribute search ### Dependencies - Resources this instance depends on (upstream) - Resources that depend on this instance (downstream) - Visual dependency graph ## Common Use Cases ### Find All EC2 Instances ```sql SELECT i.address, i.attributes->>'instance_type' AS instance_type FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_instance' ``` ### Find Resources by Tag ```sql SELECT address, attributes->'tags'->>'Name' as name FROM instances WHERE attributes->'tags'->>'Environment' = 'production' ``` ### Count by Instance Type ```sql SELECT i.attributes#>>'{instance_type}' AS instance_type, count(*) AS instance_count FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_instance' GROUP BY i.attributes#>>'{instance_type}' ORDER BY i.attributes#>>'{instance_type}' ``` ### Find Resources Without Tags ```sql SELECT address, resource_address FROM instances WHERE attributes->'tags' IS NULL OR attributes->>'tags' = '{}' ``` ### List Security Groups ```sql SELECT i.address, i.attributes->>'name' AS name, i.attributes->>'vpc_id' AS vpc FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_security_group' ``` ## Bulk Operations ### Export to CSV 1. Run a query in the Query view 2. Click **Export** > **CSV** 3. Download the results ### Export to JSON ```bash curl "http://localhost:8080/api/v1/mql?q=SELECT%20*%20FROM%20instances" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ > instances.json ``` ## Best Practices 1. **Use filters** - Don't browse all instances at once for large deployments 2. **Leverage types** - The Types view is better for understanding resource distribution 3. **Save queries** - Use Dashboards to save frequently used instance filters 4. **Check dependencies** - Use blast radius analysis before changing resources ## Next Steps - [Resource Types](/docs/inventory/resource-types) - View by type - [Modules](/docs/inventory/modules) - View by module - [Query](/docs/inventory/query) - Advanced queries - [Blast Radius](/docs/insights/blast-radius) - Impact analysis --- # Modules Source: https://stategraph.com/docs/inventory/modules Browse Terraform modules and their resources. View module usage across states, inspect module configurations, and track dependencies. The Modules view shows Terraform modules used across your states and the resources they contain. ## Overview Terraform modules are reusable containers for multiple resources. Stategraph tracks: - Module hierarchy and nesting - Resources within each module - Instance counts per module - Module usage across states ## Accessing Modules ### Via UI 1. Navigate to **Inventory** > **Modules** in the sidebar 2. Browse the module tree 3. Expand modules to see nested modules and resources 4. Click a module to view details ### Via API List modules (see instances docs for getting STATE_ID): ```bash curl "http://localhost:8080/api/v1/states/$STATE_ID/modules" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" ``` Response: ```json { "results": [ { "name": "module.vpc", "instance_count": 25, "resource_count": 10 }, { "name": "module.vpc.module.subnets", "instance_count": 12, "resource_count": 4 } ] } ``` ## Module Properties | Property | Description | |----------|-------------| | `name` | Full module path (e.g., `module.vpc.module.private_subnets`) | | `instance_count` | Total resource instances in this module | | `resource_count` | Number of resources (resource blocks / distinct resource addresses) in this module | ## Understanding Module Paths Module paths reflect the Terraform module hierarchy: ``` root # Root module (module path is null) ├── module.vpc # VPC module │ ├── module.subnets # Subnets nested in VPC │ └── module.nat # NAT nested in VPC ├── module.eks # EKS module │ └── module.node_groups # Node groups nested in EKS └── module.rds # RDS module ``` In SQL queries (module information is on the `resources` table): ```sql -- Root module resources (not in any module) SELECT * FROM resources WHERE module IS NULL -- Resources in module.vpc SELECT * FROM resources WHERE module = 'module.vpc' ``` ## Module Analysis ### Resources per Module ```sql SELECT module, count(*) as resources FROM resources WHERE module IS NOT NULL GROUP BY module ORDER BY module ``` ### Types per Module ```sql SELECT module, type, count(*) as type_count FROM resources WHERE module IS NOT NULL GROUP BY module, type ORDER BY module, type ``` ### Top-Level Modules List all modules and identify top-level ones (those without nested `module.` segments): ```sql SELECT module, count(*) as resources FROM resources WHERE module IS NOT NULL GROUP BY module ORDER BY module ``` ### Nested Module Depth Find deeply nested modules: ```sql SELECT module FROM resources WHERE module IS NOT NULL GROUP BY module ORDER BY module ``` ## Module Usage Patterns ### Find Duplicate Module Usage Identify where the same module is used multiple times: ```sql -- Count module usage across states. count(DISTINCT ...) is not supported, -- so dedupe (module, state_id) with a CTE, then count. WITH module_states AS ( SELECT module, state_id FROM resources WHERE module IS NOT NULL GROUP BY module, state_id ) SELECT module, count(*) AS states_using FROM module_states GROUP BY module ORDER BY module ``` ### Module Resource Distribution See how resources are distributed between root and modules. MQL does not support `CASE`, so run two queries: ```sql -- Resources in the root module SELECT count(*) AS resources FROM resources WHERE module IS NULL ``` ```sql -- Resources inside modules SELECT count(*) AS resources FROM resources WHERE module IS NOT NULL ``` ### Resources by Module Type ```sql SELECT module, type, count(*) as type_count FROM resources WHERE module = 'module.vpc' GROUP BY module, type ORDER BY type ``` ## Common Module Patterns ### VPC Module Resources ```sql SELECT type, count(*) as type_count FROM resources WHERE module = 'module.vpc' GROUP BY type ORDER BY type ``` ### EKS/Kubernetes Modules ```sql SELECT module, type, count(*) as type_count FROM resources WHERE module = 'module.eks' GROUP BY module, type ORDER BY module, type ``` ### Database Modules ```sql SELECT module, type, count(*) as type_count FROM resources WHERE module = 'module.rds' GROUP BY module, type ORDER BY module, type ``` ## Best Practices ### Module Organization 1. **Consistent naming** - Use clear, descriptive module names 2. **Shallow nesting** - Avoid deeply nested modules (3+ levels) 3. **Single responsibility** - Each module should have a clear purpose 4. **Version tracking** - Track module versions in source control ### Monitoring Module Usage 1. **Track growth** - Monitor instance counts over time 2. **Identify sprawl** - Watch for module proliferation 3. **Review dependencies** - Understand cross-module dependencies ## Module vs Root Resources Compare resources in modules vs root. MQL does not support `CASE` or `count(DISTINCT ...)`, so use two queries: ```sql -- Resources in the root module SELECT count(*) AS resources FROM resources WHERE module IS NULL ``` ```sql -- Resources inside modules SELECT count(*) AS resources FROM resources WHERE module IS NOT NULL ``` Generally, well-organized Terraform should have: - Infrastructure modules (VPC, EKS, RDS) - Application-specific resources in root or app modules - Shared modules for common patterns ## Next Steps - [Instances](/docs/inventory/instances) - Browse all instances - [Resource Types](/docs/inventory/resource-types) - View by type - [Query](/docs/inventory/query) - Advanced module queries - [Graph Explorer](/docs/insights/graph-explorer) - Visualize module dependencies --- # Resource Types Source: https://stategraph.com/docs/inventory/resource-types View resources grouped by type across all states. Browse by provider, filter by category, and access instances of any resource type. The Resource Types view shows all Terraform resource types in your infrastructure, with counts and quick access to instances of each type. ## Overview Resource types are the building blocks of Terraform configurations: - `aws_instance` - EC2 instances - `aws_s3_bucket` - S3 buckets - `google_compute_instance` - GCE VMs - `azurerm_virtual_machine` - Azure VMs Stategraph catalogs every resource type across all your states. ## Accessing Resource Types ### Via UI 1. Navigate to **Inventory** > **Types** in the sidebar 2. Browse the list of all resource types 3. See instance counts for each type 4. Click a type to see all instances of that type ### Via API Get a summary of resource types (see instances docs for getting STATE_ID): ```bash curl "http://localhost:8080/api/v1/states/$STATE_ID/resources/summary" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" ``` Response: ```json { "aws_instance": { "instances": 20 }, "aws_security_group": { "instances": 15 }, "aws_subnet": { "instances": 6 }, "aws_iam_role": { "instances": 12 } } ``` ## Type Analysis ### Count All Types ```sql SELECT type, count(*) as type_count FROM resources GROUP BY type ORDER BY count(*) DESC ``` ### Types by Provider The `provider` value is the full Terraform provider source address (e.g. `provider["registry.terraform.io/hashicorp/aws"]`), so match it with `LIKE`: ```sql -- AWS resources SELECT type, count(*) as type_count FROM resources WHERE provider LIKE '%hashicorp/aws%' GROUP BY type ORDER BY count(*) DESC -- Google Cloud resources SELECT type, count(*) as type_count FROM resources WHERE provider LIKE '%hashicorp/google%' GROUP BY type ORDER BY count(*) DESC -- Azure resources SELECT type, count(*) as type_count FROM resources WHERE provider LIKE '%hashicorp/azurerm%' GROUP BY type ORDER BY count(*) DESC ``` ### Unique Types per State `count(DISTINCT ...)` is not supported; dedupe with a CTE, then count: ```sql WITH state_types AS ( SELECT state_id, type FROM resources GROUP BY state_id, type ) SELECT state_id, count(*) AS types FROM state_types GROUP BY state_id ORDER BY types DESC ``` ## Common Resource Types ### AWS | Type | Description | |------|-------------| | `aws_instance` | EC2 instances | | `aws_security_group` | Security groups | | `aws_iam_role` | IAM roles | | `aws_s3_bucket` | S3 buckets | | `aws_subnet` | VPC subnets | | `aws_vpc` | Virtual private clouds | | `aws_lambda_function` | Lambda functions | | `aws_db_instance` | RDS instances | ### Google Cloud | Type | Description | |------|-------------| | `google_compute_instance` | Compute Engine VMs | | `google_storage_bucket` | Cloud Storage buckets | | `google_container_cluster` | GKE clusters | | `google_sql_database_instance` | Cloud SQL instances | ### Azure | Type | Description | |------|-------------| | `azurerm_virtual_machine` | Virtual machines | | `azurerm_storage_account` | Storage accounts | | `azurerm_kubernetes_cluster` | AKS clusters | | `azurerm_sql_server` | SQL servers | ## Type-Specific Queries ### EC2 Instances by Type ```sql SELECT i.attributes#>>'{instance_type}' AS instance_type, count(*) AS instance_count FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_instance' GROUP BY i.attributes#>>'{instance_type}' ORDER BY count(*) DESC ``` ### S3 Buckets by Region ```sql SELECT i.attributes#>>'{region}' AS region, count(*) AS region_count FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_s3_bucket' GROUP BY i.attributes#>>'{region}' ORDER BY count(*) DESC ``` ### RDS Instances by Engine ```sql SELECT i.attributes#>>'{engine}' AS engine, count(*) AS engine_count FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_db_instance' GROUP BY i.attributes#>>'{engine}' ORDER BY count(*) DESC ``` ### Lambda Functions by Runtime ```sql SELECT i.attributes#>>'{runtime}' AS runtime, count(*) AS runtime_count FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_lambda_function' GROUP BY i.attributes#>>'{runtime}' ORDER BY count(*) DESC ``` ## Resource Categories Group resource types by category: ### Compute Resources ```sql SELECT type, count(*) as type_count FROM resources WHERE type IN ( 'aws_instance', 'aws_lambda_function', 'aws_ecs_service', 'google_compute_instance', 'azurerm_virtual_machine' ) GROUP BY type ORDER BY count(*) DESC ``` ### Storage Resources ```sql SELECT type, count(*) as type_count FROM resources WHERE type IN ( 'aws_s3_bucket', 'aws_ebs_volume', 'google_storage_bucket', 'azurerm_storage_account' ) GROUP BY type ORDER BY count(*) DESC ``` ### Network Resources ```sql SELECT type, count(*) as type_count FROM resources WHERE type IN ( 'aws_vpc', 'aws_subnet', 'aws_security_group', 'aws_network_interface', 'google_compute_network', 'google_compute_subnetwork', 'azurerm_virtual_network', 'azurerm_subnet' ) GROUP BY type ORDER BY count(*) DESC ``` ### Security Resources ```sql SELECT type, count(*) as type_count FROM resources WHERE type IN ( 'aws_iam_role', 'aws_iam_policy', 'aws_iam_user', 'aws_kms_key', 'aws_secretsmanager_secret' ) GROUP BY type ORDER BY count(*) DESC ``` ## Type Distribution Analysis ### Types per Workspace ```sql SELECT state_id, type, count(*) as type_count FROM resources GROUP BY state_id, type ORDER BY state_id, count(*) DESC ``` ### New Resource Types Find types that only appear in certain states (potentially new additions): ```sql WITH type_states AS ( SELECT type, state_id FROM resources GROUP BY type, state_id ) SELECT type, count(*) AS states_count FROM type_states GROUP BY type HAVING count(*) = 1 ORDER BY type ``` ### Most Common Types ```sql SELECT type, count(*) as type_count FROM resources GROUP BY type ORDER BY count(*) DESC LIMIT 10 ``` ## Monitoring Type Growth Track how resource types grow over time by comparing counts periodically: ```sql -- Current counts SELECT type, count(*) as type_count FROM resources GROUP BY type ORDER BY count(*) DESC ``` Save these queries to a Dashboard for regular monitoring. ## Best Practices 1. **Monitor growth** - Track type counts over time 2. **Review distribution** - Ensure resources are distributed appropriately 3. **Check for drift** - Compare expected vs actual type counts 4. **Identify outliers** - Look for unexpected resource types ## Next Steps - [Instances](/docs/inventory/instances) - Browse all instances - [Modules](/docs/inventory/modules) - View by module - [Query](/docs/inventory/query) - Advanced queries - [Dashboards](/docs/inventory/dashboards) - Save type queries --- # Query (SQL) Source: https://stategraph.com/docs/inventory/query Query Terraform infrastructure with SQL. Filter resources by attributes, aggregate counts, and analyze configurations across all states. SQL queries let you explore your Terraform infrastructure across all states. ## Basic Syntax Stategraph SQL follows standard SQL SELECT syntax: ```sql SELECT columns FROM table WHERE conditions ORDER BY column [ASC|DESC] LIMIT n ``` ## Tables ### instances All resource instances across all states: | Column | Type | Description | |--------|------|-------------| | `address` | string | Full instance address | | `resource_address` | string | Resource address (without index) | | `fq_address` | string | Fully-qualified instance address | | `fq_resource_address` | string | Fully-qualified resource address | | `attributes` | jsonb | Resource attributes | | `dependencies` | text[] | List of dependency addresses | | `state_id` | uuid | Parent state ID | | `index_key` | jsonb | Instance index key (for count/for_each) | | `status` | string | Instance status | | `schema_version` | integer | Schema version | | `create_before_destroy` | boolean | Lifecycle setting | | `deposed` | string | Deposed key if applicable | | `sensitive_attributes` | jsonb | Sensitive attribute paths | | `identity` | jsonb | Identity information | | `identity_schema_version` | integer | Identity schema version | | `private` | string | Private data | **Note**: To query by resource type or provider, query the `resources` table directly. ### states All Terraform states: | Column | Type | Description | |--------|------|-------------| | `id` | uuid | State ID | | `name` | string | State name | | `group_id` | uuid | Group identifier | | `workspace` | string | Workspace name | | `tenant_id` | uuid | Tenant ID | | `created_at` | timestamp | Creation time | | `updated_at` | timestamp | Last update time | | `deleted_at` | timestamp | Deletion time (if deleted) | | `deleted_by` | uuid | User who deleted it (if deleted) | ### resources Resource definitions (use this to query by type/provider): | Column | Type | Description | |--------|------|-------------| | `address` | string | Resource address | | `fq_address` | string | Fully-qualified resource address | | `type` | string | Resource type (e.g., `aws_instance`) | | `name` | string | Resource name | | `provider` | string | Provider name | | `module` | string | Module path | | `mode` | string | Resource mode (managed/data) | | `state_id` | uuid | Parent state ID | ### providers Provider information: | Column | Type | Description | |--------|------|-------------| | `name` | string | Provider name | | `state_id` | uuid | Parent state ID | ### outputs State outputs: | Column | Type | Description | |--------|------|-------------| | `address` | string | Output address | | `name` | string | Output name | | `value` | jsonb | Output value | | `type` | jsonb | Output type | | `sensitive` | boolean | Is sensitive | | `state_id` | uuid | Parent state ID | ### transactions Transaction history: | Column | Type | Description | |--------|------|-------------| | `id` | uuid | Transaction ID | | `state` | string | Transaction state | | `tags` | jsonb | Transaction tags | | `tenant_id` | uuid | Tenant ID | | `created_at` | timestamp | Creation time | | `created_by` | uuid | Creator user ID | | `completed_at` | timestamp | Completion time | | `completed_by` | uuid | Completing user ID | | `params` | jsonb | Transaction parameters | ### tenants Tenant information: | Column | Type | Description | |--------|------|-------------| | `id` | uuid | Tenant ID | | `name` | string | Tenant name | | `created_at` | timestamp | Creation time | ### users User information: | Column | Type | Description | |--------|------|-------------| | `id` | uuid | User ID | | `name` | string | User name | | `type` | string | User type | | `created_at` | timestamp | Creation time | ### transaction_logs Transaction log entries: | Column | Type | Description | |--------|------|-------------| | `id` | uuid | Log entry ID | | `tx_id` | uuid | Parent transaction ID | | `action` | string | Action type (e.g., `state_set`) | | `object_type` | string | Type of object affected | | `state_id` | uuid | Associated state ID | | `user_id` | uuid | User who performed action | | `data` | jsonb | Additional action data | | `created_at` | timestamp | When action occurred | ### check_results Terraform check results: | Column | Type | Description | |--------|------|-------------| | `config_addr` | string | Check configuration address | | `object_kind` | string | Kind of object checked | | `state_id` | uuid | Parent state ID | | `status` | string | Check status | ### check_entries Individual check entries: | Column | Type | Description | |--------|------|-------------| | `config_addr` | string | Check configuration address | | `object_addr` | string | Object address being checked | | `state_id` | uuid | Parent state ID | | `status` | string | Entry status | | `failure_messages` | text[] | Failure messages if any | **More tables**: Beyond those above, these are also queryable: `cost_snapshots` and `cost_snapshot_resources` (see [Cost Analysis](/docs/cost#querying-cost-data-with-sql)), plus `hcl`, `hcl_refs`, `files`, `tfvars`, `transaction_subgraphs`, `revision_hashes`, and `revision_tx_hashes`. Run `stategraph sql schema` for the authoritative list of tables and columns. ## Query Examples ### List All Resources ```sql SELECT * FROM instances ``` ### Filter by Resource Type ```sql SELECT * FROM resources WHERE type = 'aws_instance' ``` ### Count Resources by Type ```sql SELECT type, COUNT(*) as type_count FROM resources GROUP BY type ORDER BY COUNT(*) DESC ``` ### Find Resources with Specific Attribute ```sql SELECT address, attributes->>'instance_type' as instance_type FROM instances WHERE attributes->>'instance_type' = 't3.micro' ``` ### Search by Address ```sql SELECT * FROM instances WHERE address = 'module.database.aws_rds_cluster.main' ``` ### Cross-State Query To analyze resources across states, query each table separately. For example, list all states and then query resources within a specific state: ```sql SELECT id, name FROM states ``` ```sql SELECT * FROM resources WHERE type = 'aws_rds_cluster' ``` **Note**: To correlate rows across tables in one query, use a `JOIN` (see [Cross-Table Queries](#cross-table-queries)) on a shared column such as `state_id`. ### Module Analysis ```sql SELECT module, COUNT(*) as resources FROM resources WHERE module IS NOT NULL GROUP BY module ORDER BY resources DESC ``` ## Operators ### Comparison | Operator | Description | Example | |----------|-------------|---------| | `=` | Equal | `type = 'aws_instance'` | | `<>` | Not equal | `type <> 'null_resource'` | | `<` | Less than | `count < 10` | | `>` | Greater than | `count > 100` | | `<=` | Less or equal | `count <= 50` | | `>=` | Greater or equal | `count >= 5` | ### Logical | Operator | Description | Example | |----------|-------------|---------| | `AND` | Both conditions | `type = 'aws_instance' AND module IS NOT NULL` | | `OR` | Either condition | `type = 'aws_s3_bucket' OR type = 'aws_s3_bucket_policy'` | | `NOT` | Negate condition | `NOT type = 'null_resource'` | ### Set Membership | Operator | Description | Example | |----------|-------------|---------| | `IN` | Value in list | `type IN ('aws_instance', 'aws_ebs_volume')` | ### Pattern Matching | Operator | Description | Example | |----------|-------------|---------| | `LIKE` | Case-sensitive pattern match | `type LIKE 'aws_%'` | | `ILIKE` | Case-insensitive pattern match | `name ILIKE '%prod%'` | | `NOT LIKE` | Negated pattern match | `type NOT LIKE 'aws_%'` | Use `%` to match any sequence of characters and `_` to match a single character. ### NULL Handling | Operator | Description | Example | |----------|-------------|---------| | `IS NULL` | Is null | `module IS NULL` | | `IS NOT NULL` | Is not null | `module IS NOT NULL` | ## JSON Operators Access JSON attributes using PostgreSQL-style operators: ### Arrow Operators | Operator | Description | Example | |----------|-------------|---------| | `->` | Get JSON value | `attributes->'tags'` | | `->>` | Get JSON as text | `attributes->>'instance_type'` | | `#>>` | Get nested path as text | `attributes#>>'{tags,Name}'` | ### JSON Path Examples ```sql -- Get instance type SELECT address, attributes->>'instance_type' as instance_type FROM instances WHERE attributes->>'instance_type' IS NOT NULL -- Get nested tag value SELECT address, attributes#>>'{tags,Environment}' as env FROM instances -- Filter by tag SELECT * FROM instances WHERE attributes->'tags'->>'Environment' = 'production' ``` ### JSON Contains ```sql -- Find instances with specific tags SELECT * FROM instances WHERE attributes->'tags' @> '{"Environment": "production"}'::jsonb ``` ## Aggregations ### COUNT ```sql SELECT count(*) FROM instances SELECT type, count(*) FROM resources GROUP BY type ORDER BY type ``` ### With HAVING ```sql SELECT type, count(*) as type_count FROM resources GROUP BY type HAVING count(*) > 10 ORDER BY type ``` ## Cross-Table Queries Stategraph SQL supports `INNER`, `LEFT`, and `RIGHT` joins, so you can correlate data across tables in a single query. Match rows with an `ON` clause — most tables share a `state_id`, and `instances.resource_address` matches `resources.address`. ```sql -- Join instances to their resource definitions to filter by type SELECT i.address, r.type, r.provider FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_instance' ``` ```sql -- Count resources by type within each state SELECT s.name AS state, r.type, COUNT(*) AS type_count FROM resources AS r INNER JOIN states AS s ON r.state_id = s.id GROUP BY s.name, r.type ORDER BY COUNT(*) DESC ``` ## Ordering and Limits ### ORDER BY ```sql SELECT * FROM instances ORDER BY address ASC, status DESC ``` ### LIMIT ```sql SELECT * FROM instances LIMIT 100 ``` A query with no `LIMIT` of its own returns at most **20 rows** (the default), and an explicit `LIMIT` is capped at **1000 rows**. Add an explicit `LIMIT` whenever you need a predictable result size. ## Common Patterns ### AWS Resources by Type ```sql SELECT type, COUNT(*) as type_count FROM resources WHERE type IN ('aws_instance', 'aws_s3_bucket', 'aws_lambda_function', 'aws_rds_cluster') GROUP BY type ORDER BY COUNT(*) DESC ``` **Tip**: Use `IN` for a set of exact types, or `LIKE` for prefix matches such as `type LIKE 'aws_%'`. ### Find Untagged Resources ```sql SELECT address, resource_address FROM instances WHERE attributes->'tags' IS NULL OR attributes->>'tags' = '{}' ``` ### Resources by State ```sql SELECT state_id, COUNT(*) as resources FROM instances GROUP BY state_id ``` ### Security Groups ```sql SELECT address, attributes->>'name' as name FROM instances WHERE resource_address = 'aws_security_group.main' ``` **Note**: To filter instances by resource type, join `instances` to `resources` on `resource_address` (see [Cross-Table Queries](#cross-table-queries)). ## Tips 1. **Use LIMIT** - Always add LIMIT when exploring large datasets 2. **Specific columns** - Select only needed columns for faster queries 3. **Index usage** - Filters on `type` and `address` are fast 4. **JSON paths** - Use `#>>` for nested JSON access 5. **Test incrementally** - Build complex queries step by step ## Error Messages | Error | Cause | Solution | |-------|-------|----------| | "Syntax error" | Invalid SQL syntax | Check query structure | | "Unknown column" | Column doesn't exist | Check column names | | "Type mismatch" | Wrong data type in comparison | Cast values appropriately | ## Next Steps - [Instances](/docs/inventory/instances) - Browse resource instances - [SQL Syntax Reference](/docs/reference/sql-syntax) - Complete grammar reference - [API Reference](/docs/reference/api) - Query via API --- # Dashboards Source: https://stategraph.com/docs/inventory/dashboards Create and save custom views of your infrastructure. Build personalized dashboards with filters, queries, and saved searches for quick access. Dashboards let you save and organize views of your infrastructure for quick access. ## Overview Dashboards provide: - **Saved queries** - Store frequently used SQL queries - **Custom views** - Organize information relevant to your workflow - **Quick access** - One-click access to important metrics Dashboards are saved in your browser. To share a view with teammates, share the underlying queries (or export query results from the Query view). ## Creating Dashboards ### Via UI 1. Navigate to **Dashboards** in the Inventory menu 2. Click **Create Dashboard** 3. Enter a name and optional description 4. Add panels with queries or visualizations ### Dashboard Structure ``` Dashboard: Production Overview ├── Panel: Resource Summary │ └── Query: SELECT type, COUNT(*) ... ├── Panel: Recent Changes │ └── Query: SELECT * FROM transactions ... └── Panel: Untagged Resources └── Query: SELECT * FROM instances WHERE attributes->'tags' IS NULL ``` ## Panel Types A panel is a saved SQL query rendered as a table or chart. Available chart types are **bar**, **pie**, and **treemap**. ### Query Panel Display the results of an SQL query as a table: ```sql SELECT type, count(*) as type_count FROM resources GROUP BY type ORDER BY count(*) DESC LIMIT 10 ``` ### Chart Panels Render a query's results as a **bar**, **pie**, or **treemap** chart — for example, resource counts by type or by provider. ## Example Dashboards ### Infrastructure Overview ``` ┌─────────────────────────────────────────────────────────┐ │ Infrastructure Overview │ ├─────────────────────────────┬───────────────────────────┤ │ Total Resources: 1,234 │ States: 15 │ │ Workspaces: 3 │ Last Update: 2 min ago │ ├─────────────────────────────┴───────────────────────────┤ │ Resources by Type │ │ ┌────────────────────────────────────────────────────┐ │ │ │ aws_instance ████████████████████ 234 │ │ │ │ aws_security_group ██████████████ 156 │ │ │ │ aws_iam_role ████████████ 123 │ │ │ │ aws_s3_bucket █████████ 89 │ │ │ └────────────────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────────────┤ │ Recent Changes │ │ • 10:30 - user@company.com - networking/prod │ │ • 10:15 - ci@company.com - kubernetes/staging │ │ • 09:45 - user@company.com - database/prod │ └─────────────────────────────────────────────────────────┘ ``` ### Security Dashboard Resource `type` lives on the `resources` table, so join `instances` to `resources` to filter by type. **Security groups** ```sql SELECT i.address, i.attributes->>'name' AS name FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_security_group' ``` **IAM roles** ```sql SELECT i.address, i.attributes->>'name' AS name FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_iam_role' ``` ### Instance Type Distribution Count instances by EC2 type or RDS class. For actual cost data, see [Cost Analysis](/docs/cost). ```sql SELECT i.attributes#>>'{instance_type}' AS instance_type, COUNT(*) AS instance_count FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_instance' GROUP BY i.attributes#>>'{instance_type}' ORDER BY COUNT(*) DESC ``` ## Useful Queries ### Resource Inventory ```sql -- All resources by type SELECT type, count(*) as type_count FROM resources GROUP BY type ORDER BY type -- Resources by workspace SELECT workspace, count(*) as resources FROM states GROUP BY workspace ORDER BY workspace ``` ### Compliance ```sql -- Resources without tags SELECT address, resource_address FROM instances WHERE attributes->'tags' IS NULL OR attributes->>'tags' = '{}' -- Resources missing required tag SELECT address, resource_address FROM instances WHERE attributes->'tags'->>'Environment' IS NULL ``` ### Security ```sql -- Security groups SELECT i.address, i.attributes->>'name' AS name FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_security_group' -- IAM users (should be minimized) SELECT i.address FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_iam_user' ``` ### Change Tracking ```sql -- States changed recently (via transaction data) SELECT name, workspace, created_at FROM states ORDER BY created_at DESC LIMIT 10 ``` ## Sharing Dashboards Dashboards are saved in your browser, so they aren't automatically shared across users. To share a view with teammates: - **Share the queries** - Copy the SQL behind each panel and have colleagues recreate or save it. - **Export query results** - From the Query view, run a query and export the results (for example, to CSV) to share a point-in-time snapshot. ## Best Practices 1. **Name clearly** - Use descriptive names that indicate purpose 2. **Group related queries** - Keep related information together 3. **Add descriptions** - Document what each panel shows 4. **Use time filters** - Add date ranges where relevant 5. **Review regularly** - Update dashboards as infrastructure evolves ## Tips ### Query Performance - Use LIMIT for large result sets - Filter early in queries - Avoid SELECT * when possible ### Organization - Create dashboards per team or function - Separate operational and security dashboards - Archive outdated dashboards ### Maintenance - Review queries after major changes - Update dashboards when adding new resource types - Remove obsolete panels ## Next Steps - [Query Language](/docs/inventory/query) - SQL syntax - [API Reference](/docs/reference/api) - Programmatic access - [Insights](/docs/insights) - Analysis tools --- # Gap Analysis Source: https://stategraph.com/docs/inventory/gap-analysis Find cloud resources that aren't managed by Terraform. Discover unmanaged infrastructure, identify drift, and bring it under management. Gap analysis discovers resources in your cloud provider that exist but aren't managed by any Terraform state in Stategraph. ## Overview As infrastructure grows, resources can be created outside of Terraform: - Manual console changes - Scripts or CLI commands - Other IaC tools - Forgotten experiments Gap analysis compares your cloud inventory against Terraform state to find these unmanaged resources. ## How It Works ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Cloud Provider │ │ Stategraph │ │ Gap Report │ │ Inventory │───▶│ Compare │───▶│ Unmanaged │ │ │ │ │ │ Resources │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ AWS Resource Explorer • S3 bucket GCP Cloud Asset Inventory • IAM role • Compute instance ``` 1. **Fetch inventory** - Query cloud provider for all resources 2. **Compare with state** - Match against resources in Terraform states 3. **Report gaps** - List resources not found in any state ## Supported Providers | Provider | Inventory Source | Status | |----------|-----------------|--------| | AWS | Resource Explorer | ✓ Supported | | GCP | Cloud Asset Inventory | ✓ Supported | | Azure | Resource Graph | Planned | --- ## AWS Gap analysis uses [AWS Resource Explorer](https://docs.aws.amazon.com/resource-explorer/latest/userguide/welcome.html) to discover resources. ### Setup Requirements 1. **Resource Explorer** must be enabled with at least one index 2. **Aggregator index** recommended for cross-region discovery 3. **Default view** must be configured ### Required IAM Permissions For gap analysis (read inventory): ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "resource-explorer-2:Search", "resource-explorer-2:ListIndexes", "resource-explorer-2:GetDefaultView" ], "Resource": "*" } ] } ``` For generating Terraform configurations (optional): ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "arn:aws:iam::aws:policy/ReadOnlyAccess" ], "Resource": "*" } ] } ``` ### Enable Resource Explorer ```bash # Enable Resource Explorer in current region aws resource-explorer-2 create-index --type LOCAL # Create aggregator index (recommended for multi-region) aws resource-explorer-2 update-index-type \ --arn "arn:aws:resource-explorer-2:us-east-1:123456789012:index/..." \ --type AGGREGATOR # Create default view aws resource-explorer-2 create-view --view-name default aws resource-explorer-2 associate-default-view --view-arn "arn:aws:resource-explorer-2:..." ``` ### Check Configuration ```bash # Set API base (or use --api-base flag) export STATEGRAPH_API_BASE=https://stategraph.example.com stategraph tenant gaps config \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=aws ``` Response: ```json { "status": "ready", "ready_for_gap_analysis": true, "ready_for_terraform_import": true, "has_aggregator": true, "aggregator_region": "us-east-1", "indexed_regions": ["us-east-1", "us-west-2", "eu-west-1"], "index_count": 3, "warnings": [] } ``` --- ## GCP Gap analysis uses [GCP Cloud Asset Inventory](https://cloud.google.com/asset-inventory/docs/overview) to discover resources. ### Setup Requirements 1. **Cloud Asset API** must be enabled 2. **IAM permissions** for Cloud Asset Inventory 3. **Scope access** at project, folder, or organization level ### Required IAM Permissions For gap analysis (read inventory): ```bash # Grant Cloud Asset Viewer role gcloud projects add-iam-policy-binding PROJECT_ID \ --member='serviceAccount:SA_EMAIL' \ --role='roles/cloudasset.viewer' ``` For generating Terraform configurations (optional): ```bash # Grant Viewer role (read-only access to all resources) gcloud projects add-iam-policy-binding PROJECT_ID \ --member='serviceAccount:SA_EMAIL' \ --role='roles/viewer' ``` ### Enable Cloud Asset API ```bash gcloud services enable cloudasset.googleapis.com --project=PROJECT_ID ``` ### Scope Configuration GCP gap analysis can operate at different scopes: | Scope | Environment Variable | Coverage | |-------|---------------------|----------| | Project | `GOOGLE_CLOUD_PROJECT` | Single project | | Folder | `GOOGLE_CLOUD_FOLDER` | All projects in folder | | Organization | `GOOGLE_CLOUD_ORGANIZATION` | Entire organization | If no scope is set, Stategraph auto-detects from Application Default Credentials. ### Check Configuration ```bash # Set API base (or use --api-base flag) export STATEGRAPH_API_BASE=https://stategraph.example.com stategraph tenant gaps config \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=gcp ``` Response: ```json { "status": "ready", "ready_for_gap_analysis": true, "ready_for_terraform_import": false, "scope_type": "project", "scope_id": "projects/my-project", "has_org_access": false, "warnings": [ { "code": "NO_VIEWER_PERMISSION", "message": "Optional: To generate Terraform configurations for unmanaged resources, grant 'roles/viewer' (read-only) to your service account.", "fix": "gcloud projects add-iam-policy-binding my-project \\\n --member='serviceAccount:123456-compute@developer.gserviceaccount.com' \\\n --role='roles/viewer'" } ] } ``` ### Supported GCP Resource Types 72 resource types are supported, including: | Service | Resource Types | |---------|---------------| | Compute Engine | Instances, Disks, Networks, Subnetworks, Firewalls, Load Balancers | | Cloud Storage | Buckets | | Cloud SQL | Instances | | BigQuery | Datasets, Tables | | IAM | Service Accounts, Roles | | Cloud Functions | Functions | | Pub/Sub | Topics, Subscriptions | | Cloud Run | Services | | GKE | Clusters, Node Pools | | Cloud KMS | Key Rings, Crypto Keys | --- ## Accessing Gap Analysis ### Via UI 1. Navigate to **Inventory → Gap Analysis** 2. Select your cloud provider (AWS or GCP) 3. Click **Check Config** to verify setup 4. Click **Analyze** to run gap analysis 5. View unmanaged resources The UI shows: - **Coverage Summary** - Total resources, managed vs unmanaged - **Service Breakdown** - Resources grouped by service - **Resource Table** - Details of each unmanaged resource ### Via CLI Run gap analysis: ```bash # Set API base (or use --api-base flag) export STATEGRAPH_API_BASE=https://stategraph.example.com stategraph tenant gaps analyze \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=aws \ --format=json ``` By default `gaps analyze` prints a human-readable table followed by a `Summary: N managed, M unmanaged` line. Pass `--format=json` for machine-readable output (the JSON examples below use it); `--format=simple` is also available. Gap analysis runs asynchronously. The first call for a provider starts a background scan and returns a `running` status: ```json { "status": "running", "started_at": "2026-06-26T17:30:00Z" } ``` Re-run the same command to retrieve the result once the scan finishes (subsequent calls are served from cache): ```json { "summary": { "total_aws_resources": 512, "managed_by_stategraph": 460, "unmanaged": 49, "phantom_filtered": 3 }, "unmanaged_resources": [ { "arn": "arn:aws:ec2:us-east-1:123456789012:security-group/sg-0abc1234def567890", "service": "ec2", "resource_type": "ec2:security-group", "region": "us-east-1", "owning_account_id": "123456789012" } ], "fetched_at": 1705312800 } ``` > GCP returns provider-specific fields instead: `total_cloud_resources` in the summary, and `asset_name` and `project_id` on each unmanaged resource. The GCP resource type (e.g. `compute.googleapis.com/Instance`) is reported in `resource_type`, the same field AWS uses. Because the scan is asynchronous, pipe to `jq` only after the result has been returned (a `running` response has no `unmanaged_resources` array). ## Understanding Results ### Summary Metrics | Metric | Description | |--------|-------------| | `total_aws_resources` / `total_cloud_resources` | All resources found in cloud inventory (AWS reports `total_aws_resources`; GCP reports `total_cloud_resources`) | | `managed_by_stategraph` | Resources matched to Terraform state | | `unmanaged` | Resources not in any state | | `phantom_filtered` | Provider-managed resources excluded (e.g., default VPCs) | ### Phantom Resources Some resources are automatically filtered because they're provider-managed and can't be imported: **AWS:** - Default Athena workgroups and catalogs - Service-linked IAM roles - Default event buses - Default S3 Storage Lens dashboards **GCP:** - Default compute service accounts - Default VPC networks - Default firewall rules ## Generating Terraform Configurations Select unmanaged resources and generate import-ready Terraform code. ### Via UI 1. Select resources using checkboxes 2. Click **Generate Config** 3. Review generated HCL 4. Copy or download the `.tf` file ### Via CLI Use `gaps analyze` to list the unmanaged resources, then `gaps import` to generate `import` blocks and resource configuration for them: ```bash # Set API base (or use --api-base flag) export STATEGRAPH_API_BASE=https://stategraph.example.com # Get unmanaged resources stategraph tenant gaps analyze \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=gcp --format=json | jq '.unmanaged_resources' > unmanaged.json # Generate Terraform configuration stategraph tenant gaps import \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=gcp \ unmanaged.json ``` Response: ```json { "generated_hcl": "resource \"google_compute_instance\" \"imported_orphan_vm\" {\n name = \"orphan-vm\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n ...\n}\n", "import_blocks": "import {\n to = google_compute_instance.imported_orphan_vm\n id = \"projects/my-project/zones/us-central1-a/instances/orphan-vm\"\n}\n", "provider_hcl": "terraform {\n required_providers {\n google = {\n source = \"hashicorp/google\"\n version = \"~> 5.0\"\n }\n }\n}\n", "supported_count": 1, "unsupported_count": 0, "unsupported_resources": [] } ``` ### Using Generated Code 1. Save generated HCL to a `.tf` file 2. Run `tofu plan` or `terraform plan` to verify 3. Run `tofu apply` or `terraform apply` to import ```bash # Save generated configuration cat > import.tf << 'EOF' import { to = google_compute_instance.imported_orphan_vm id = "projects/my-project/zones/us-central1-a/instances/orphan-vm" } resource "google_compute_instance" "imported_orphan_vm" { name = "orphan-vm" machine_type = "e2-medium" zone = "us-central1-a" # ... generated attributes } EOF # Verify and import tofu plan tofu apply ``` ## Caching Gap analysis results are cached to avoid rate limiting and reduce API calls: | Setting | Default | Description | |---------|---------|-------------| | Cache TTL | 3 hours | How long results are cached | | Cache location | `/var/cache/stategraph/gap-analysis/` | Cache directory | ```bash # Set API base (or use --api-base flag) export STATEGRAPH_API_BASE=https://stategraph.example.com # Use cache (default, fast) stategraph tenant gaps analyze \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=gcp # Force fresh scan (slower) stategraph tenant gaps analyze \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=gcp \ --source=no-cache ``` The `fetched_at` timestamp indicates when the inventory was last fetched from the cloud provider. In the UI, click **Refresh** to bypass the cache. ## Filtering Results ### By Service ```bash stategraph tenant gaps analyze \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=gcp --format=json | jq '.unmanaged_resources | map(select(.service == "compute"))' ``` ### By Region ```bash stategraph tenant gaps analyze \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=gcp --format=json | jq '.unmanaged_resources | map(select(.region == "us-central1-a"))' ``` ### By Resource Type Both AWS and GCP report each resource's type in the `resource_type` field. For AWS it looks like `ec2:instance`; for GCP it is the Cloud Asset type string, e.g. `compute.googleapis.com/Instance`. ```bash # AWS (resource_type, e.g. "ec2:instance") stategraph tenant gaps analyze \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=aws --format=json | jq '.unmanaged_resources | map(select(.resource_type == "ec2:instance"))' # GCP (resource_type, e.g. "compute.googleapis.com/Instance") stategraph tenant gaps analyze \ --tenant=550e8400-e29b-41d4-a716-446655440000 \ --provider=gcp --format=json | jq '.unmanaged_resources | map(select(.resource_type == "compute.googleapis.com/Instance"))' ``` ## Troubleshooting ### AWS: No indexes found ``` "warnings": [{"code": "NO_INDEXES", "message": "No Resource Explorer indexes found"}] ``` **Solution:** Enable Resource Explorer in at least one region: ```bash aws resource-explorer-2 create-index --type LOCAL ``` ### AWS: No aggregator ``` "warnings": [{"code": "NO_AGGREGATOR", "message": "No aggregator index configured"}] ``` **Solution:** Upgrade one index to aggregator type for cross-region discovery: ```bash aws resource-explorer-2 update-index-type --arn "INDEX_ARN" --type AGGREGATOR ``` ### GCP: Permission denied ``` "warnings": [{"code": "PERMISSION_DENIED", "message": "Permission denied for scope projects/my-project"}] ``` **Solution:** Grant Cloud Asset Viewer role: ```bash gcloud projects add-iam-policy-binding my-project \ --member='serviceAccount:YOUR_SA@my-project.iam.gserviceaccount.com' \ --role='roles/cloudasset.viewer' ``` ### GCP: Insufficient OAuth scopes ``` "warnings": [{"code": "INSUFFICIENT_SCOPES", "message": "VM OAuth scopes are insufficient"}] ``` **Solution:** Update VM scopes (requires restart): ```bash gcloud compute instances stop INSTANCE_NAME --zone=ZONE gcloud compute instances set-service-account INSTANCE_NAME \ --zone=ZONE --scopes=cloud-platform gcloud compute instances start INSTANCE_NAME --zone=ZONE ``` ### Terraform import fails with permission denied The gap analysis uses Cloud Asset API, but Terraform import needs actual resource read permissions. **Solution:** Grant `roles/viewer` for Terraform operations: ```bash gcloud projects add-iam-policy-binding my-project \ --member='serviceAccount:YOUR_SA@my-project.iam.gserviceaccount.com' \ --role='roles/viewer' ``` ## Best Practices 1. **Check config first** - Run config check before analysis to verify permissions 2. **Regular scans** - Run gap analysis periodically (weekly or after deployments) 3. **Triage results** - Classify gaps as intentional or concerning 4. **Import systematically** - Bring unmanaged production resources under control 5. **Use organization scope** - For GCP, use organization-level access for complete visibility 6. **Document exceptions** - Record why some resources remain unmanaged ## Environment Variables | Variable | Description | |----------|-------------| | `GOOGLE_CLOUD_PROJECT` | GCP project ID for gap analysis scope | | `GOOGLE_CLOUD_FOLDER` | GCP folder ID for broader scope | | `GOOGLE_CLOUD_ORGANIZATION` | GCP organization ID for full visibility | | `AWS_DEFAULT_REGION` | AWS region for Resource Explorer queries | | `GAP_ANALYSIS_CACHE_TTL` | Cache TTL in seconds (default: 10800 = 3 hours) | --- ## Credential Configuration This section explains how to pass cloud provider credentials to Stategraph for gap analysis. By default, Stategraph uses the standard credential chain for each provider (instance metadata, environment variables, config files). ### AWS Credentials #### Default Behavior Stategraph uses the standard AWS credential chain: 1. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) 2. Shared credentials file (`~/.aws/credentials`) 3. IAM role for Amazon EC2 / ECS task role 4. IAM Roles for Service Accounts (IRSA) in EKS #### Docker Compose **Option 1: Environment Variables** Pass credentials directly (suitable for development): ```yaml services: server: image: :latest # image URL provided when you get access: https://stategraph.com/contact environment: # ... existing config ... AWS_ACCESS_KEY_ID: "AKIAIOSFODNN7EXAMPLE" AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" AWS_DEFAULT_REGION: "us-east-1" # Optional: for temporary credentials # AWS_SESSION_TOKEN: "your-session-token" ``` **Option 2: Mount AWS Credentials File** Mount your local AWS credentials directory (recommended for development): ```yaml services: server: image: :latest # image URL provided when you get access: https://stategraph.com/contact environment: # ... existing config ... AWS_DEFAULT_REGION: "us-east-1" # Optional: specify a named profile # AWS_PROFILE: "my-profile" volumes: - ~/.aws:/home/stategraph/.aws:ro ``` **Option 3: Use .env File** Store credentials in a `.env` file (not committed to git): ```bash # .env AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY AWS_DEFAULT_REGION=us-east-1 ``` ```yaml services: server: image: :latest # image URL provided when you get access: https://stategraph.com/contact env_file: - .env ``` #### Kubernetes **Option 1: Secret with Environment Variables** ```yaml apiVersion: v1 kind: Secret metadata: name: aws-credentials namespace: stategraph stringData: AWS_ACCESS_KEY_ID: "AKIAIOSFODNN7EXAMPLE" AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" --- apiVersion: apps/v1 kind: Deployment metadata: name: stategraph namespace: stategraph spec: template: spec: containers: - name: stategraph envFrom: - secretRef: name: aws-credentials env: - name: AWS_DEFAULT_REGION value: "us-east-1" ``` **Option 2: IAM Roles for Service Accounts (IRSA) - Recommended for EKS** 1. Create an IAM role with the required permissions and trust policy for your EKS cluster 2. Annotate the Kubernetes service account: ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: stategraph namespace: stategraph annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/stategraph-gap-analysis --- apiVersion: apps/v1 kind: Deployment metadata: name: stategraph namespace: stategraph spec: template: spec: serviceAccountName: stategraph containers: - name: stategraph env: - name: AWS_DEFAULT_REGION value: "us-east-1" ``` **Option 3: EC2 Instance Profile** If running on EC2 or using node IAM roles, no additional configuration is needed. Ensure the node's IAM role has the required Resource Explorer permissions. --- ### GCP Credentials #### Default Behavior Stategraph uses the standard GCP credential chain: 1. `GOOGLE_APPLICATION_CREDENTIALS` environment variable (path to service account JSON) 2. Application Default Credentials (ADC) 3. Compute Engine / GKE metadata server 4. Workload Identity (GKE) #### Docker Compose Mount your GCP service account key file and set `GOOGLE_APPLICATION_CREDENTIALS` to point to it: ```yaml services: server: image: :latest # image URL provided when you get access: https://stategraph.com/contact environment: # ... existing config ... GOOGLE_APPLICATION_CREDENTIALS: "/secrets/gcp-sa-key.json" GOOGLE_CLOUD_PROJECT: "my-project-id" volumes: - ./gcp-sa-key.json:/secrets/gcp-sa-key.json:ro ``` This approach: - Uses the standard GCP authentication method via `GOOGLE_APPLICATION_CREDENTIALS` - Mounts your service account key file into the container - Works consistently across Docker Compose and Kubernetes environments > **Note:** Ensure your service account key file is excluded from version control (add to `.gitignore`) #### Kubernetes **Option 1: Secret with Service Account Key** 1. Create a secret from your service account JSON: ```bash kubectl create secret generic gcp-credentials \ --namespace=stategraph \ --from-file=key.json=./gcp-sa-key.json ``` 2. Mount the secret and set the environment variable: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: stategraph namespace: stategraph spec: template: spec: containers: - name: stategraph env: - name: GOOGLE_APPLICATION_CREDENTIALS value: "/secrets/gcp/key.json" - name: GOOGLE_CLOUD_PROJECT value: "my-project-id" volumeMounts: - name: gcp-credentials mountPath: /secrets/gcp readOnly: true volumes: - name: gcp-credentials secret: secretName: gcp-credentials ``` **Option 2: Workload Identity (Recommended for GKE)** 1. Enable Workload Identity on your GKE cluster 2. Create a GCP service account with the required permissions 3. Bind the Kubernetes service account to the GCP service account: ```bash gcloud iam service-accounts add-iam-policy-binding \ stategraph-sa@PROJECT_ID.iam.gserviceaccount.com \ --role="roles/iam.workloadIdentityUser" \ --member="serviceAccount:PROJECT_ID.svc.id.goog[stategraph/stategraph]" ``` 4. Annotate the Kubernetes service account: ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: stategraph namespace: stategraph annotations: iam.gke.io/gcp-service-account: stategraph-sa@PROJECT_ID.iam.gserviceaccount.com --- apiVersion: apps/v1 kind: Deployment metadata: name: stategraph namespace: stategraph spec: template: spec: serviceAccountName: stategraph containers: - name: stategraph env: - name: GOOGLE_CLOUD_PROJECT value: "my-project-id" ``` **Option 3: GCE Metadata Server** If running on GCE or GKE without Workload Identity, the default service account is used automatically. Ensure it has the required Cloud Asset Viewer permissions. --- ### Multi-Cloud Configuration To enable gap analysis for both AWS and GCP simultaneously: **Docker Compose:** ```yaml services: server: image: :latest # image URL provided when you get access: https://stategraph.com/contact environment: # AWS AWS_DEFAULT_REGION: "us-east-1" # GCP GOOGLE_APPLICATION_CREDENTIALS: "/secrets/gcp-sa-key.json" GOOGLE_CLOUD_PROJECT: "my-project-id" volumes: - ~/.aws:/home/stategraph/.aws:ro - ./gcp-sa-key.json:/secrets/gcp-sa-key.json:ro ``` **Kubernetes:** ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: stategraph namespace: stategraph spec: template: spec: serviceAccountName: stategraph # With IRSA and/or Workload Identity containers: - name: stategraph env: # AWS - name: AWS_DEFAULT_REGION value: "us-east-1" # GCP - name: GOOGLE_APPLICATION_CREDENTIALS value: "/secrets/gcp/key.json" - name: GOOGLE_CLOUD_PROJECT value: "my-project-id" volumeMounts: - name: gcp-credentials mountPath: /secrets/gcp readOnly: true volumes: - name: gcp-credentials secret: secretName: gcp-credentials ``` ## Next Steps - [Dashboards](/docs/inventory/dashboards) - Create custom views of your inventory - [Query Language](/docs/inventory/query) - Query resources with SQL - [Environment Variables](/docs/reference/environment-variables) - Full configuration reference --- # Introduction Source: https://stategraph.com/docs/cost Estimate what your Terraform-managed infrastructure costs — per resource, state, and tenant — with attribution, history, and SQL access. Cost Analysis estimates what the infrastructure in your Terraform state costs, per resource, per state, and across your whole tenant. Each estimate is stored as a cost snapshot you can read over the API, attribute by tag, chart over time, and query with SQL. Because Stategraph already holds your fleet's state of record, it can answer questions a per-file editor plugin can't: what a whole environment costs, who owns that spend, and how it is trending. ## Features ### State and Resource Cost Price a single state and read a per-resource, per-component breakdown. [State & Resource Cost](/docs/cost/state-cost) ### Plan-Time Cost See a change's cost delta inline at `stategraph tf plan`, before you apply it. [Plan-Time Cost](/docs/cost/plan-time) ### Cost Attribution Roll up cost by provider, resource type, and tag (owner, team, environment) across every managed state, and track it over time. [Cost Attribution](/docs/cost/attribution) ### Cloud Billing Ingest your actual cloud spend from a FOCUS billing export to work with real billed cost, not just estimates. [Cloud Billing (FOCUS)](/docs/cost/billing-sources) ### Actual Spend Read your ingested FOCUS spend: attribute billed cost to managed resources by provider, and list billed resources no state manages. [Read Actual Spend](/docs/cost/billing-sources#read-actual-spend) ### Cost in the Console Explore per-state cost, the tenant cost aggregation over time, and Billing Connections from the Stategraph console. [Cost in the Console](/docs/cost/console) ## How It Works Stategraph prices each resource by calling a pricing service backed by a price book of cloud pricing data. You enable it once per deployment (see [Setup](/docs/cost/setup)). Each estimate is written as a cost snapshot: per-state totals (cost, coverage, currency) plus a per-resource breakdown of monthly and hourly cost, pricing components, and tags. Snapshots are recomputed on a schedule, after an apply, and on demand. Monetary values are returned and stored as strings to preserve sub-cent precision, so parse them as decimals rather than floats. A money field is absent when nothing in that scope is priced. ## Estimates, Not Bills Cost figures are list-price estimates computed from each resource's type and attributes (instance class, storage size, region), not your metered cloud bill. They are ideal for comparing options, catching expensive changes, and attributing spend, but they will not match your invoice to the cent. To work with real billed spend, connect a [cloud billing export](/docs/cost/billing-sources). ## Coverage Not every resource can be priced. Each resource in a snapshot falls into one of three states: | State | Meaning | |-------|---------| | Priced | Supported, and a cost was found; contributes to the totals | | `no_price` | Recognized, but nothing is billable (for example, an `aws_db_subnet_group`) | | Unsupported | No pricing model exists for the type | `coverage_percent` reports the share of resources that were priced. Only priced resources contribute to the totals, so read a total alongside its coverage, and list the gaps with the [unsupported endpoint](/docs/cost/state-cost#coverage-gaps). ## Documentation | Topic | Description | |-------|-------------| | [Setup](/docs/cost/setup) | Enable cost analysis: the pricing service and price book | | [State & Resource Cost](/docs/cost/state-cost) | Price a state and read the per-resource breakdown | | [Plan-Time Cost](/docs/cost/plan-time) | Preview a change's cost delta before you apply | | [Cost Attribution](/docs/cost/attribution) | Tenant rollups, attribution by tag, and trends | | [Querying Cost Data](/docs/cost/queries) | SQL over the cost snapshot tables | | [Cloud Billing (FOCUS)](/docs/cost/billing-sources) | Ingest your actual cloud spend | | [Read Actual Spend](/docs/cost/billing-sources#read-actual-spend) | Attribute billed FOCUS spend and find unmanaged spend | | [Cost in the Console](/docs/cost/console) | Per-state and tenant cost views, and Billing Connections | --- # Setup Source: https://stategraph.com/docs/cost/setup Enable Stategraph cost analysis durably on any deployment — turn on the in-image pricing service and let it load and refresh the cloud price book itself. Cost analysis is off by default. It has two moving parts: 1. **A pricing service**, shipped inside the server image, that prices each resource. The on switch is `STATEGRAPH_COST_ENABLED`: set it to `true` and `capabilities.costs.enabled` is `true`; leave it unset and cost analysis is off. The server reaches the pricing service at `STATEGRAPH_PRICING_SERVICE_URL`, which defaults to `http://localhost:8090` — the bundled in-image service — so you only set it to point at an external pricing service. 2. **A price book**, the cloud pricing data the service looks resources up in, loaded into a `cloud_pricing` PostgreSQL database. The service loads it automatically on first boot and refreshes it on a schedule, so you don't manage it by hand. So enabling cost is **one durable step**: set `STATEGRAPH_COST_ENABLED=true` in your deployment's environment. Because it lives in your deployment config — not inside the running container — it survives redeploys, restarts, pod reschedules, and ECS task replacements. ## Enable cost Set `STATEGRAPH_COST_ENABLED=true` in your **`server` container's environment**, the same place your other `STATEGRAPH_*` variables live. The bundled pricing service runs in the same container as the server, so `STATEGRAPH_PRICING_SERVICE_URL` already defaults to `http://localhost:8090` and you don't need to set it. ### Docker Compose Add it to your `.env` (or the `server` service's `environment:`): ```bash STATEGRAPH_COST_ENABLED=true ``` Then recreate the container so it boots with the new environment: ```bash docker compose up -d ``` ### Kubernetes and Amazon ECS Set the same variable on the `server`/`stategraph` container — in your Helm values for Kubernetes, or the task definition's container `environment` for ECS — then roll out the change. Because it's part of the Deployment / Task Definition, every new pod or task comes up with cost on. See the [Kubernetes](/docs/deployment/kubernetes) and [ECS](/docs/deployment/ecs) guides for exactly where to put it. ### What happens on first boot When the server comes up with `STATEGRAPH_COST_ENABLED=true`, the pricing service starts automatically and, if the price book is empty, loads it into the `cloud_pricing` database. The first load downloads a large dataset (several hundred MB compressed, millions of prices), so allow a few minutes and make sure the database has room. This runs in the background — **the server and UI are available immediately**; cost surfaces fill in once the first load completes. ## Verify Confirm cost analysis is enabled: ```bash curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ http://localhost:8080/api/v1/capabilities ``` ```json { "costs": { "enabled": true } } ``` If `enabled` is `false`, the server booted without `STATEGRAPH_COST_ENABLED=true`, and `POST .../costs/calculate` returns `503`. Once enabled, produce the first estimate for a state with [`POST .../costs/calculate`](/docs/cost/state-cost#calculate-or-refresh), or wait for the next scheduled run. On first enable the price-book load runs in the background, so cost figures only appear once it finishes (a few minutes). The pricing service reports progress on a readiness endpoint — `200` once the price book is loaded, `503` while it is still loading — which also serves as a Kubernetes/ECS readiness probe: ```bash docker compose exec server curl -fsS http://localhost:8090/readyz ``` ## The price book The price book lives in the `cloud_pricing` database, separate from your main Stategraph database. The pricing service manages it for you: - **Loaded on first boot** when the table is empty (see above). - **Refreshed automatically** every 168 hours (weekly) by default. Override the cadence with `PRICING_REFRESH_HOURS`; set it to `0` to disable automatic refresh. Reloads happen in the background with an atomic swap, so cost analysis keeps serving the previous prices until the new data is ready. - **Persisted in the database**, so it survives restarts. Make sure `cloud_pricing` has durable storage — the bundled Compose stores it on the `db` volume; on Kubernetes or ECS, use a managed or volume-backed PostgreSQL. If the database is wiped, the service simply reloads the book on the next boot. To force an out-of-band reload (for example after changing `PRICING_DATA_URL`), run the loader directly: ```bash docker compose exec server load-pricing-data ``` ## Configuration You only need the variables below to customize the setup — for example, putting the price book in a separate database, changing the refresh cadence, or mirroring the price-book download. | Variable | Default | Description | |----------|---------|-------------| | `STATEGRAPH_COST_ENABLED` | `false` | Master on/off switch for cost analysis. Set to `true` to enable; without it, cost analysis is disabled. | | `STATEGRAPH_PRICING_SERVICE_URL` | `http://localhost:8090` | Endpoint of the pricing service. Defaults to the bundled in-image service; set it only to point at an external pricing service. | | `PRICING_REFRESH_HOURS` | `168` | How often the pricing service reloads the price book. `0` disables automatic refresh. | | `STATEGRAPH_PRICING_DEFAULT_REGION` | `us-east-1` | Region assumed when a resource does not specify one. | | `STATEGRAPH_COST_SCHEDULE_HOURS` | `24` | How often per-state cost snapshots are recomputed on a schedule. | | `STATEGRAPH_COST_EVENT_DEBOUNCE_HOURS` | `6` | Minimum gap before an apply triggers another recompute. | | `STATEGRAPH_COST_PRICING_CALL_TIMEOUT_SECONDS` | `30` | Timeout for a single pricing-service call. | | `PRICING_DB_HOST` / `PRICING_DB_PORT` | server's database | Host and port of the `cloud_pricing` database. Defaults match the bundled Compose database. | | `PRICING_DB_USER` / `PRICING_DB_PASSWORD` | server's database | Credentials for the `cloud_pricing` database. | | `PRICING_DB_NAME` | `cloud_pricing` | Name of the price-book database. | | `PRICING_DATA_URL` | Stategraph's hosted price book | Override the price-book download source, such as your own mirror for air-gapped installs. | See [Environment Variables](/docs/reference/environment-variables) for the full server configuration reference. ## Air-Gapped Installs If the server cannot reach the internet to download the price book, host the `products.csv.gz` file on a reachable URL or internal mirror and set `PRICING_DATA_URL` to it; the service uses it for both the first-boot load and scheduled refreshes. To keep the price book in a database separate from the server's, set the `PRICING_DB_*` variables to point the pricing service and loader at it. ## Next Steps - [State & Resource Cost](/docs/cost/state-cost) - Price a state and read the breakdown - [Cost Attribution](/docs/cost/attribution) - Roll up and attribute spend across your tenant --- # State & Resource Cost Source: https://stategraph.com/docs/cost/state-cost Price a single Terraform state, read the per-resource and per-component cost breakdown, refresh estimates, and inspect coverage gaps. The state endpoints price a single Terraform state and return a per-resource, per-component breakdown. All cost endpoints require authentication and return monetary fields as strings. ``` GET /api/v1/states/{state_id}/costs # latest snapshot + per-resource breakdown POST /api/v1/states/{state_id}/costs/calculate # recompute now GET /api/v1/states/{state_id}/costs/unsupported # resources that didn't contribute (gaps) ``` Each endpoint has a CLI wrapper, shown beside the curl examples below: `stategraph cost state`, `stategraph cost calculate`, and `stategraph cost unsupported`. The `state` and `unsupported` commands accept `--format=table|json|simple`; `calculate` only enqueues a recompute. ## Price a State Fetch the latest snapshot for a state: ```bash stategraph cost state --state $STATE_ID # or, against the API directly: curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ http://localhost:8080/api/v1/states/$STATE_ID/costs ``` ```json { "snapshot_id": "…", "calculated_at": "2026-06-09T06:15:01Z", "currency": "USD", "monthly_cost": "1509.640000", "hourly_cost": "2.067000", "resource_count": 24, "supported_count": 24, "priced_count": 12, "coverage_percent": 50.0, "instance_costs": [ … ] } ``` A `404` means the state has never been priced; [calculate it first](#calculate-or-refresh). The headline fields: | Field | Meaning | |-------|---------| | `monthly_cost` / `hourly_cost` | Total estimated cost of the priced resources (string; absent if nothing is priced) | | `currency` | Currency of the estimate (for example, `USD`) | | `resource_count` | Resources in the state | | `supported_count` | Resources whose type the pricing engine recognizes | | `priced_count` | Resources that produced a non-zero price | | `coverage_percent` | `priced_count / resource_count`; read every total next to this | ## Per-Resource Breakdown `instance_costs[]` holds one entry per resource, and each entry's `components[]` breaks the price into line items such as instance hours, storage, and requests. Here is a real `aws_db_instance`: ```json { "address": "aws_db_instance.primary", "type": "aws_db_instance", "provider": "aws", "monthly_cost": "656.270000", "hourly_cost": "0.899000", "supported": true, "no_price": false, "components": [ { "name": "Database instance (on-demand, Multi-AZ, db.r6g.xlarge)", "unit": "hours", "price": "0.8990000000", "hourly_quantity": "1", "monthly_quantity": "730", "hourly_cost": "0.899000", "monthly_cost": "656.270000" }, { "name": "Storage (general purpose SSD, gp3)", "unit": "GB", "price": "0.0000000000", "monthly_quantity": "200" } ], "tags": { "Team": "data", "Environment": "production", "Project": "acme" }, "cloud_resource_id": "arn:aws:rds:us-east-1:…:db:acme-primary" } ``` Each resource also carries its `tags`, which power [cost attribution](/docs/cost/attribution), and its `cloud_resource_id` when one is known. ## Calculate or Refresh Trigger a fresh recompute. This returns `202` with a task and runs in the background; a new snapshot is written when it finishes. It returns `503` if the server has no [pricing service configured](/docs/cost/setup). ```bash stategraph cost calculate --state $STATE_ID # or, against the API directly: curl -X POST -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ http://localhost:8080/api/v1/states/$STATE_ID/costs/calculate ``` ```json { "id": "ca019a18-…", "state": "pending" } ``` Poll the task until it reaches a terminal state, then re-fetch the cost: ```bash curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ http://localhost:8080/api/v1/tasks/$TASK_ID # { "id": "ca019a18-…", "state": "completed" } ``` Stop when `state` is `completed`; treat `failed` or `aborted` as an error. You normally don't need to do this by hand, since snapshots refresh on a schedule and after applies, but it is useful when cost data is missing or stale. ## Coverage Gaps List the resources that did not contribute to the totals, either unsupported types or recognized types with nothing billable (`no_price`): ```bash stategraph cost unsupported --state $STATE_ID # or, against the API directly: curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ http://localhost:8080/api/v1/states/$STATE_ID/costs/unsupported ``` ```json { "snapshot_id": "…", "calculated_at": "2026-06-09T06:15:01Z", "resources": [ { "address": "aws_db_parameter_group.postgres16", "type": "aws_db_parameter_group", "provider": "aws", "supported": true, "no_price": true }, { "address": "aws_db_subnet_group.main", "type": "aws_db_subnet_group", "provider": "aws", "supported": true, "no_price": true } ] } ``` Surface this whenever you report a total. A high gap count usually means free-to-create resources, such as parameter groups, subnet groups, and IAM, rather than missing prices. ## Next Steps - [Cost Attribution](/docs/cost/attribution) - Roll these per-state numbers up across the tenant - [Querying Cost Data](/docs/cost/queries) - The same data as MQL tables for ad-hoc analysis --- # Plan-Time Cost Source: https://stategraph.com/docs/cost/plan-time Preview the cost impact of a Terraform change before you apply it — Stategraph shows a current-vs-planned cost delta inline in stategraph tf plan. Stategraph prices a pending change before you apply it. When you run [`stategraph tf plan`](/docs/velocity/transactions#preview-plan), it estimates what the plan would cost and prints a current-vs-planned delta right under the diff, so an expensive change is visible in review rather than on next month's bill. ## At Plan Time `stategraph tf plan` fetches the cost delta automatically and prints a `Costs:` block after the plan: ```bash stategraph tf plan --tenant --out plan.json ``` ``` Plan: 1 to add, 0 to change, 0 to destroy. Costs: Totals: Monthly: — → 60.74 USD (+60.74 USD) Hourly: — → 0.08 USD (+0.08 USD) Resources: 0 → 1 (+1) Coverage: 100.0% (unchanged) Per state: cost-preview-demo: +60.74/mo (resources +1) + aws_instance.web +60.74 ``` Each `Totals` line reads `current → planned (±delta)`. A `—` means that side had nothing priced; here the state is new, so its current cost is `—` and the plan adds `$60.74/mo`. Changing existing infrastructure shows both sides, for example `Monthly: 130.82 USD → 140.16 USD (+9.34 USD)`. The delta is best-effort and never blocks the plan. If the preview is not ready yet, you will see a hint instead of the block: ``` Costs: preview not yet ready. Run `stategraph tx costs --tx ` to view it later. ``` `stategraph tf plan` waits up to `--costs-wait` seconds (default `3`) for the delta before falling back to that hint; raise it to wait longer, or set a deployment-wide default with the `STATEGRAPH_COSTS_WAIT_SECONDS` environment variable. The same flag applies to `stategraph tf mtx` and `stategraph tf apply`. Combining the `--costs-wait` flag with `--skip-costs` is a usage error; the environment variable may instead be set alongside `--skip-costs`, which then takes precedence and skips the fetch. On a fetch error, or when pricing is not configured, the plan prints nothing extra. Pass `--skip-costs` to turn the fetch off entirely. ## On Demand The same delta is available any time for a transaction a plan opened, via the standalone command: ```bash stategraph tx costs --tx ``` It prints the identical `Costs:` block. Use it to re-check after the "preview not yet ready" hint, or to inspect a plan's cost later. Only transactions opened through `stategraph tf plan` or `tf mtx` have a preview; a `tx` created by hand has none. ## Reading the Delta The per-state breakdown marks each resource by how the plan changes it: | Marker | Meaning | |--------|---------| | `+` | Added: a new resource; its full planned cost is the delta | | `-` | Removed: a destroyed resource; its cost comes off the total | | `~` | Changed: an in-place update; the delta is the cost difference | A resource with a `+0.00` delta changed in a way that does not affect its price, or is not priced at all. States the plan touches but does not change cost for are listed as `no cost change`. ## API The CLI wraps `GET /api/v1/tx/{tx_id}/costs`: ```bash curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ http://localhost:8080/api/v1/tx/$TX_ID/costs ``` - `200` - a `tx-cost-delta`: `totals` (each metric as `current_*`, `planned_*`, `delta_*`), a per-state `states[]` list with per-resource `resources[]` (each carrying a `change_kind` of `added`, `removed`, or `changed`), and `by_provider` / `by_type` / `by_tag` delta breakdowns. - `202` - `{ "status": "computing" }`: the preview is still being computed in the background, or the transaction was applied from the console rather than `stategraph tf plan`. Retry shortly. - `404` - the transaction was aborted (aborting deletes its planned cost). - `401` - the caller is not a member of the transaction's tenant. ```json { "totals": { "currency": "USD", "current_monthly_cost": null, "planned_monthly_cost": "60.736000", "delta_monthly_cost": "60.736000", "current_resource_count": 0, "planned_resource_count": 1, "delta_resource_count": 1 }, "states": [ { "state_id": "…", "state_name": "cost-preview-demo", "delta_monthly_cost": "60.736000", "resources": [ { "address": "aws_instance.web", "type": "aws_instance", "provider": "aws", "change_kind": "added", "delta_monthly_cost": "60.736000" } ] } ] } ``` Money fields are decimal strings, and `null` or absent when a side has nothing priced. See the [API reference](/docs/reference/api#plan-time-cost) for the full schema. ## Requirements - A [pricing service](/docs/cost/setup) must be configured; otherwise the plan prints no cost block. - The delta is computed from the plan the actuator previews, so it is available for changes planned with `stategraph tf plan` or `tf mtx`. ## Next Steps - [State & Resource Cost](/docs/cost/state-cost) - The current cost a delta is measured against - [Cost Attribution](/docs/cost/attribution) - Roll up and attribute cost across the tenant --- # Cost Attribution Source: https://stategraph.com/docs/cost/attribution Roll up cost across every managed state, attribute spend by tag (team, environment, owner), and track cost trends over time. Because Stategraph holds your whole fleet's state of record, it can answer the question a per-file cost plugin cannot: who owns this spend, and how is it trending? The tenant endpoints roll up the latest snapshot of every live state and break it down by provider, type, or tag, in a single call. ``` GET /api/v1/tenants/{tenant_id}/costs[?tag_key=KEY] # rollup + breakdowns GET /api/v1/tenants/{tenant_id}/costs/tag-keys # tag keys available to group by GET /api/v1/tenants/{tenant_id}/costs/history[?from&to&group_by] # cost over time ``` ## Tenant Rollup ```bash stategraph cost tenant --tenant $TENANT_ID # or, against the API directly: curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs" ``` One call returns the tenant total, coverage, a `by_provider` and `by_type` breakdown, and a per-state list: ```json { "monthly_cost": "1573.964000", "currency": "USD", "coverage_percent": 51.3, "by_provider": [ { "name": "aws", "monthly_cost": "1573.964000", "hourly_cost": "2.156116", "resource_count": 221 } ], "by_type": [ { "name": "aws_db_instance", "monthly_cost": "1179.900000", "resource_count": 5 }, { "name": "aws_elasticache_cluster", "monthly_cost": "360.620000", "resource_count": 2 }, { "name": "aws_instance", "monthly_cost": "31.244000", "resource_count": 4 } ], "states": [ { "state_name": "acme-data-stores", "monthly_cost": "1509.640000", "coverage_percent": 50.0, "resource_count": 24, "state_id": "5156028b-…" } ] } ``` Soft-deleted states are excluded. A never-priced state appears in `states[]` with its identity only and no cost fields, which is different from a `$0` state. ## Attribution by Tag This is the "who owns this spend" view. First discover which tag keys exist across the fleet: ```bash stategraph cost tag-keys --tenant $TENANT_ID # or, against the API directly: curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs/tag-keys" ``` ```json { "tag_keys": ["Environment", "Project", "Role", "Service", "Team", "Tier"] } ``` Then break the rollup down by one of them with `?tag_key=`: ```bash stategraph cost tenant --tenant $TENANT_ID --tag-key Team # or, against the API directly: curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs?tag_key=Team" ``` `by_tag` comes back with one row per value of that key: ```json { "by_tag": [ { "name": "data", "monthly_cost": "1149.020000", "resource_count": 13 }, { "name": "engineering", "monthly_cost": "7.592000", "resource_count": 20 }, { "name": "platform", "monthly_cost": "2.200000", "resource_count": 52 }, { "name": "sre", "resource_count": 17 }, { "name": "untagged", "monthly_cost": "415.152000", "resource_count": 103 } ] } ``` Two things to read carefully: - `untagged` rolls up every resource missing that key. A large `untagged` figure is usually a tagging-hygiene signal, not a real owner. - Absent money is not `$0`. The `sre` row above has 17 resources but no `monthly_cost`, because none of them were priced (free-to-create resources, or unsupported types). A `$0` value means "priced, costs nothing"; an absent field means "nothing here was priced." Do not render them the same way. Group by `Environment`, `Project`, `Service`, or any other key the same way, for example `?tag_key=Environment` to split `production` from `staging`. ## Trends Over Time History is derived from the append-only snapshot trail. Each state's most recent snapshot is carried forward across recompute gaps, so the series is real, not interpolated. ```bash stategraph cost history --tenant $TENANT_ID --group-by provider # or, against the API directly: curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs/history?group_by=provider" ``` ```json { "points": [ { "date": "2026-06-09", "monthly_cost": "1573.964000", "hourly_cost": "2.156116", "groups": [ { "name": "aws", "monthly_cost": "1573.964000", "resource_count": 221 } ] } ] } ``` There is one `points[]` entry per day, oldest first, each carrying the day's tenant total. Useful parameters: | Parameter | Description | |-----------|-------------| | `group_by` | `provider`, `type`, or `tag`, adding a `groups[]` breakdown to each point; with `tag`, set `tag_key` to choose the key | | `tag_key` | Tag key the per-day breakdown groups by when `group_by=tag` | | `from` / `to` | Window bounds, as full ISO 8601 / RFC 3339 timestamps (for example `2026-06-01T00:00:00Z`), not bare dates; default to 30 days ago and now | A date-only value returns `400` with id `INVALID_DATE_PARAM`. ```bash curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" \ "http://localhost:8080/api/v1/tenants/$TENANT_ID/costs/history?from=2026-06-01T00:00:00Z&to=2026-06-09T00:00:00Z" ``` ## Next Steps - [Querying Cost Data](/docs/cost/queries) - Go beyond the prebuilt rollups with SQL - [State & Resource Cost](/docs/cost/state-cost) - Drill into a single state's per-resource breakdown --- # Querying Cost Data Source: https://stategraph.com/docs/cost/queries Query Stategraph cost snapshots with SQL — rank resources by cost, break spend down by type or tag, and find coverage gaps. Cost snapshots are exposed as two MQL tables, so you can go beyond the prebuilt rollups and slice spend any way you like. See [Query Language](/docs/reference/sql-syntax) for MQL basics. | Table | One row per | Key columns | |-------|-------------|-------------| | `cost_snapshots` | state snapshot | `state_id`, `kind`, `monthly_cost`, `hourly_cost`, `currency`, `resource_count`, `supported_count`, `priced_count`, `calculated_at`, `tx_id` | | `cost_snapshot_resources` | resource in a snapshot | `snapshot_id`, `address`, `type`, `provider`, `region`, `monthly_cost`, `hourly_cost`, `supported`, `no_price`, `tags`, `components` | Join them on `cost_snapshot_resources.snapshot_id = cost_snapshots.id`. ## Working With Snapshots A few rules make cost queries correct: - Money is text. The `monthly_cost` and `hourly_cost` columns are strings (to preserve precision) and are `NULL` when a resource has no billable cost. Cast with `::real` for math, and filter `WHERE monthly_cost IS NOT NULL` before ranking or summing. - Snapshots are append-only. Each recompute writes a new row, so a state accumulates many `kind = 'current'` snapshots over time. To avoid double-counting, scope a query to a single `snapshot_id` (shown below), or use the [rollup API](/docs/cost/attribution#tenant-rollup) for tenant-wide totals; it dedupes to each state's latest snapshot server-side. - `kind` distinguishes snapshot types. `current` snapshots are realized or scheduled estimates; `planned` snapshots are plan-time predictions written by `stategraph tf plan` and `tf mtx`, each tied to a `tx_id`. Filter `WHERE kind = 'current'` to exclude plan-time predictions, as the examples below do. To get the latest snapshot id for a state: ```sql SELECT id FROM cost_snapshots WHERE state_id = '5156028b-…' AND kind = 'current' ORDER BY calculated_at DESC LIMIT 1 ``` ## Example Queries ### Per-State Totals ```sql SELECT state_id, monthly_cost, priced_count, resource_count FROM cost_snapshots WHERE kind = 'current' AND monthly_cost IS NOT NULL ORDER BY monthly_cost::real DESC LIMIT 4 ``` ``` state_id monthly_cost priced_count resource_count ------------------------------------ ------------ ------------ -------------- 5156028b-d378-4ab6-8a8b-4a783e633bc6 1509.640000 21 34 3523a799-01bd-47cc-bdc2-7c72f628f24e 130.816000 5 8 2d531a2c-299d-4787-ba47-6c75c9723516 38.472000 3 3 ``` ### Most Expensive Resources in a State ```sql SELECT address, type, monthly_cost FROM cost_snapshot_resources WHERE snapshot_id = '9eb9021d-…' AND monthly_cost IS NOT NULL ORDER BY monthly_cost::real DESC LIMIT 5 ``` ``` address type monthly_cost --------------------------------- ----------------------- ------------ aws_db_instance.primary aws_db_instance 656.270000 aws_db_instance.replica_1 aws_db_instance 328.500000 aws_elasticache_cluster.app_cache aws_elasticache_cluster 240.170000 aws_db_instance.replica_2 aws_db_instance 164.250000 aws_elasticache_cluster.sessions aws_elasticache_cluster 120.450000 ``` ### Monthly Cost by Resource Type `count` is a reserved word in MQL, so alias the count to something else (here, `cnt`) and sort by the aggregate expression rather than the alias: ```sql SELECT type, sum(monthly_cost::real) AS monthly, count(*) AS cnt FROM cost_snapshot_resources WHERE snapshot_id = '9eb9021d-…' AND monthly_cost IS NOT NULL GROUP BY type ORDER BY sum(monthly_cost::real) DESC LIMIT 5 ``` ``` type monthly cnt ----------------------- ------- --- aws_db_instance 1149.02 3 aws_elasticache_cluster 360.62 2 ``` ### Cost by Tag Extract a tag with the `#>>` path operator (use `#>>` rather than `->>` inside `GROUP BY`). A `NULL` group is resources missing that tag: ```sql SELECT tags#>>'{Team}' AS team, sum(monthly_cost::real) AS monthly FROM cost_snapshot_resources WHERE snapshot_id = '9eb9021d-…' AND monthly_cost IS NOT NULL GROUP BY tags#>>'{Team}' ORDER BY sum(monthly_cost::real) DESC ``` ``` team monthly ---- ------- data 1149.02 360.62 ``` For tag attribution across the whole tenant, prefer the [`?tag_key=` rollup endpoint](/docs/cost/attribution#attribution-by-tag); it dedupes states and buckets untagged resources for you. ### Coverage Gaps The resources that did not contribute to the totals (recognized but `no_price`): ```sql SELECT type, count(*) AS cnt FROM cost_snapshot_resources WHERE snapshot_id = '9eb9021d-…' AND no_price GROUP BY type ORDER BY count(*) DESC ``` ## Next Steps - [Cost Attribution](/docs/cost/attribution) - The prebuilt tenant rollups and tag attribution - [Query Language](/docs/reference/sql-syntax) - Full MQL reference --- # Cloud Billing (FOCUS) Source: https://stategraph.com/docs/cost/billing-sources Ingest your actual cloud spend into Stategraph by connecting a FOCUS billing export from AWS, GCP, or Azure. The cost estimates elsewhere in this section are list-price calculations. To work with your actual billed spend, connect a FOCUS billing export, the open [FOCUS](https://focus.finops.org/) billing format that AWS, GCP, and Azure can all emit. Stategraph syncs the export on a schedule and stores it so the console's cost views can show real spend alongside the estimates. Billing sources are admin-only and managed per tenant, either from the console's **Billing Connections** page or the CLI below. ## Connect a Billing Source 1. Create a FOCUS export in your cloud provider (see [Provider Setup](#provider-setup)). 2. Point a Stategraph billing source at the exported files: ```bash stategraph cost billing-source add \ --tenant \ --provider aws \ --source-uri "s3://my-bucket/focus/data/**/*.parquet" ``` ```json { "id": "aef4bd07-…", "provider": "aws", "source_uri": "s3://my-bucket/focus/data/**/*.parquet", "region": "us-east-1", "window_months": 2, "enabled": true, "created_at": "2026-06-09T12:01:17Z", "updated_at": "2026-06-09T12:01:17Z" } ``` Once enabled, the source syncs on a schedule. Trigger one immediately with [`sync`](#manage-sources). Credentials resolve ambiently, from an instance role, workload identity, `az login`, or the standard provider environment variables, so you do not hand secrets to Stategraph. Options for `add`: | Option | Required | Description | |--------|----------|-------------| | `--provider` | Yes | `aws`, `gcp`, or `azure` | | `--source-uri` | Yes | Location of the exported files (Parquet or CSV; globs allowed) | | `--region` | No | Bucket region (AWS). Omit to resolve ambiently | | `--window-months` | No | Trailing months reloaded on each sync (default `2`: current plus previous) | | `--disabled` | No | Create the source without enabling scheduled sync | ## Provider Setup Configure a FOCUS export in your cloud provider, then use its location as `--source-uri`. First files typically arrive within about 24 hours and refresh daily. ### AWS In **Billing and Cost Management** > **Data Exports**, create a FOCUS 1.0 standard export (Parquet) to an S3 bucket. - `--source-uri`: `s3://bucket/prefix//data/**/*.parquet` - [Create a data export](https://docs.aws.amazon.com/cur/latest/userguide/dataexports-create-standard.html) · [FOCUS 1.0 columns](https://docs.aws.amazon.com/cur/latest/userguide/table-dictionary-focus-1-0-aws.html) ### GCP Enable detailed billing export to BigQuery, then export the FOCUS view to a GCS bucket (Parquet or CSV) and point the source at that bucket. - `--source-uri`: `gs://bucket/prefix/**/*.parquet` - [Set up billing export](https://cloud.google.com/billing/docs/how-to/export-data-bigquery-setup) · [FOCUS billing view](https://cloud.google.com/blog/topics/cost-management/new-bigquery-cloud-billing-view-based-on-focus) ### Azure In **Cost Management** > **Exports**, create an export with the Cost and usage details (FOCUS) dataset (Parquet) to a storage account. - `--source-uri`: `abfss://container@account.dfs.core.windows.net/path/**/*.parquet` - [Create an export](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-improved-exports) · [FOCUS schema](https://learn.microsoft.com/en-us/azure/cost-management-billing/dataset-schema/cost-usage-details-focus) ## Manage Sources ```bash # List billing sources in a tenant stategraph cost billing-source list --tenant # Trigger a sync now (optionally backfill from a date) stategraph cost billing-source sync --tenant [--from 2026-01-01] # Change settings (omitted fields are unchanged) stategraph cost billing-source update --tenant --window-months 3 # Pause or resume scheduled sync stategraph cost billing-source disable --tenant stategraph cost billing-source enable --tenant # Remove a source and its loaded billing rows stategraph cost billing-source remove --tenant ``` `list` reports each source's `last_status`, `last_synced_at`, and `last_row_count`, so you can confirm syncs are landing. `remove` deletes the source and the billing rows it loaded; it prompts for confirmation, or pass `--auto-approve` to skip the prompt. ## Read Actual Spend Once a sync has landed, read your billed FOCUS spend attributed to the infrastructure Stategraph manages. Unlike the admin-only ingestion commands above, these reads are available to any tenant member. ``` GET /api/v1/tenants/{tenant_id}/costs/attribution # billed spend attributed to managed resources, by provider GET /api/v1/tenants/{tenant_id}/costs/unmanaged # billed resources no state manages ``` `cost attribution` reports how much of your billed spend Stategraph could match to managed resources: ```bash stategraph cost attribution --tenant ``` | Field | Meaning | |-------|---------| | `total_cost` | Total billed spend in the window | | `attributed_cost` | Spend matched to a resource in a managed state | | `unallocated_cost` | Spend that matched no managed resource | | `unmanaged_cost` | Billed spend for resources no state manages | | `coverage_percent` | Share of billed spend that was attributed | It also returns `attributed_count`, `line_count`, `computed_at`, `matcher_version`, and a `by_provider` breakdown carrying the same fields per provider. `cost unmanaged` lists the billed resources no state manages, so you can find spend that has drifted outside Terraform: ```bash stategraph cost unmanaged --tenant [--limit 20] ``` Each entry in `results[]` carries `provider`, `service_name`, `resource_id`, `resource_type`, `region_id`, `billed_cost`, `line_count`, and `currency`. Both commands accept `--format=table|json|simple`. ## API The CLI wraps admin endpoints under `/api/v1/tenants/{tenant_id}/billing-sources`. A non-admin caller receives `403`. | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/tenants/{tenant_id}/billing-sources` | List sources | | `POST` | `/api/v1/tenants/{tenant_id}/billing-sources` | Create a source | | `PUT` | `/api/v1/tenants/{tenant_id}/billing-sources/{id}` | Update a source | | `POST` | `/api/v1/tenants/{tenant_id}/billing-sources/{id}/sync` | Trigger a sync | | `DELETE` | `/api/v1/tenants/{tenant_id}/billing-sources/{id}` | Delete a source | ## Next Steps - [Cost Analysis](/docs/cost) - The estimate-based cost surface - [Cost Attribution](/docs/cost/attribution) - Attribute estimated spend by team, environment, or owner --- # Cost in the Console Source: https://stategraph.com/docs/cost/console Explore Stategraph cost in the console: per-state cost views, tenant cost aggregation over time, and the Billing Connections page. Stategraph's cost intelligence is available in the console as well as over the [CLI and API](/docs/cost). The console surfaces three cost views. ## Per-State Cost Open a state's detail page and follow its cost link to see that state priced: its totals, coverage, and the per-resource breakdown — the same data the [state cost API](/docs/cost/state-cost) returns. Use it to spot a state's most expensive resources without leaving the UI. ## Tenant Cost The tenant cost view aggregates the latest cost of every state in the tenant into a single rollup, and charts tenant cost over time as a time series. A **Period** selector controls the window the series covers. It is the visual counterpart to the [tenant rollup and history](/docs/cost/attribution) endpoints. ## Billing Connections The **Billing Connections** page manages your [FOCUS billing sources](/docs/cost/billing-sources) — the connections that ingest your actual cloud spend — from the console. Like the CLI billing-source commands, it is admin-only and scoped to a tenant. ## Next Steps - [Cost Analysis](/docs/cost) - The full cost surface across CLI, API, and SQL - [State & Resource Cost](/docs/cost/state-cost) - The per-state data behind the console view - [Cloud Billing (FOCUS)](/docs/cost/billing-sources) - Connect and manage billing sources --- # Introduction Source: https://stategraph.com/docs/orchestration GitOps for Terraform with PR-based automation, policy enforcement, and drift detection. Automated plans, approvals, and infrastructure delivery. Stategraph Orchestration brings GitOps workflows to Terraform. Run infrastructure changes through pull requests with automatic plans, policy enforcement, cost estimates, and drift detection. Powered by [Terrateam](https://terrateam.io). ## Features ### PR-Based Workflows Infrastructure changes flow through your normal Git workflow: 1. **Open a PR** - Push your `.tf` changes to a branch 2. **Auto Plan** - Orchestration runs `terraform plan` and posts results as a PR comment 3. **Review** - Team reviews the plan, cost estimate, and policy checks 4. **Merge to Apply** - Approve and merge—Orchestration applies the changes ### Policy Enforcement Run OPA/Conftest policies on every plan: ```yaml # .terrateam/config.yml workflows: - tag_query: "dir:environments/production/**" plan: - type: init - type: plan - type: conftest ``` Block non-compliant changes before they reach production. ### Cost Estimates See the dollar impact of every change with Infracost integration: ```yaml cost_estimation: enabled: true currency: "USD" ``` Set cost thresholds that require additional approval. ### Drift Detection Scheduled scans detect when infrastructure drifts from code. Get notified and remediate before it becomes a problem. ### RBAC & CODEOWNERS Control who can apply changes to which directories: ```yaml apply_requirements: checks: - tag_query: "dir:environments/production/**" approved: enabled: true any_of: ["team:platform"] - tag_query: "iam in dir" approved: enabled: true all_of: ["team:security"] ``` Automatic approval routing based on file ownership. ## Supported Tools Orchestration works with: - Terraform - OpenTofu - Terragrunt - CDKTF - Pulumi ## Configuration Orchestration is configured via a YAML file in your repository: ```yaml # .terrateam/config.yml # Enable cost estimation in PRs cost_estimation: enabled: true currency: "USD" # Apply requirements for production apply_requirements: checks: - tag_query: "dir:environments/production/**" approved: enabled: true any_of: ["team:platform"] # Custom workflows workflows: - tag_query: "dir:environments/production/**" plan: - type: init - type: plan - type: conftest ``` ## Getting Started Orchestration is available now through Terrateam: 1. **Sign up** at [terrateam.io](https://terrateam.io) 2. **Install** the GitHub App on your repository 3. **Configure** your workflows in `.terrateam/config.yml` 4. **Open a PR** with Terraform changes to see it in action ## Resources - [Terrateam Documentation](https://terrateam.io/docs) - Full Terrateam documentation - [Pricing](/pricing) - See pricing options - [Product Overview](/product/orchestration) - Learn more about Orchestration --- # Introduction Source: https://stategraph.com/docs/ai-agents Drive Stategraph from an LLM. Six SKILL.md files cover query, change, import, refactor, cost, plus a router that dispatches automatically. Stategraph is built to be driven by AI agents (Claude Code, agents using [skills.sh](https://skills.sh), or any tool that loads `SKILL.md` files), not just humans. The integration surface is a set of skill files in [github.com/stategraph/skills](https://github.com/stategraph/skills) that teach the agent the Stategraph workflows, the safe defaults, and the canonical command form. ## Available skills Six skills, one of which is a router that dispatches to the others. | Skill | Purpose | | --- | --- | | `stategraph` | Router. Detects Stategraph tasks and dispatches to one of the five workflow skills below. | | `stategraph-query` | Read-only SQL queries, state summaries, inventory, blast radius, and gap analysis. | | `stategraph-cost` | Cost intelligence: state/tenant cost, attribution by tag/provider, cost history, coverage gaps, and plan-time cost deltas. | | `stategraph-change` | `stategraph tf plan`, `stategraph tf apply`, state deletion, transaction lifecycle. | | `stategraph-import` | Import `.tfstate` or HCL into Stategraph. Wire a Terraform repo to Stategraph. | | `stategraph-refactor` | Interactive address-rewrite workflow. Restructure a repo while preserving state addresses. | The router pattern means users typically only need the top-level `stategraph` skill installed. When the agent detects a Stategraph task, the router selects the right workflow skill and hands off via the agent's Skill tool. Installing all six together also makes each workflow directly addressable: `/stategraph-query`, `/stategraph-cost`, `/stategraph-change`, `/stategraph-import`, `/stategraph-refactor`. ## Install With the [skills CLI](https://skills.sh): ```bash npx skills add stategraph/skills ``` Or install a specific skill: ```bash npx skills add stategraph/skills -s stategraph-query ``` ### Manual install (Claude Code) Clone into `~/.claude/skills/` so each skill lands at `~/.claude/skills//SKILL.md`: ```bash git clone https://github.com/stategraph/skills /tmp/stategraph-skills cp -r /tmp/stategraph-skills/skills/* ~/.claude/skills/ rm -rf /tmp/stategraph-skills ``` ## Requirements - The Stategraph CLI on your `PATH`. See [CLI installation](/docs/cli). - A configured tenant. `stategraph info` should succeed. ## Usage Once installed, ask the agent anything Stategraph-related and the router picks the workflow: - "What S3 buckets do we have?" runs `stategraph-query` - "How much does this tenant cost, broken down by team?" runs `stategraph-cost` - "Plan these changes." runs `stategraph-change` - "Import this terraform.tfstate." runs `stategraph-import` - "Refactor this repo into child modules without losing state." runs `stategraph-refactor` You can also invoke a workflow directly with `/stategraph-query`, `/stategraph-cost`, `/stategraph-change`, `/stategraph-import`, or `/stategraph-refactor`. ## Safety model Two layers, one in the skill files and one in Stategraph itself. **Skill-level: read-only vs mutating.** The `stategraph-query` and `stategraph-cost` skills are read-only by design. They run SQL queries, inventory summaries, and cost reports, and never call `tf plan`, `tf apply`, imports, or refactors (`stategraph-cost`'s only write is triggering a cost recalculation, never an infrastructure change). The other three workflow skills — change, import, and refactor — are mutating, each scoped to its own workflow. **Stategraph-level: plan and apply are gated independently.** Issue an agent an API token scoped to plan only, and the worst case from a confused model is a wrong plan you throw away. There is no path from a confused model to a destroyed database. For autonomous overnight runs, plan-only is the right default. For agents that have already passed a review boundary (a CI runner on a green PR, for example), issue plan and apply. See [Authentication](/docs/authentication) for token scoping. ## See also - [github.com/stategraph/skills](https://github.com/stategraph/skills): the repo, with each skill's `SKILL.md` in full - [Refactor CLI workflow](/docs/cli/refactor): the underlying CLI the refactor skill drives - [`stategraph info`](/docs/cli): the orientation command agents call first - [Authentication](/docs/authentication): scoping API tokens --- # Introduction Source: https://stategraph.com/docs/deployment Deploy Stategraph using Docker Compose, Kubernetes, AWS ECS, or Google Cloud Run. Choose the deployment method that fits your infrastructure and team size. Run Stategraph two ways: as a fully managed hosted service, or self-hosted in your own infrastructure. **[Stategraph Cloud (hosted)](/docs/deployment/hosted)** is the fastest way to start — we run the server, upgrades, and availability. To self-host instead, get access to the server image below and choose a method: - [Get Access to Stategraph](https://stategraph.com/contact): The Stategraph server image — along with the Helm chart and ECS Terraform module — is distributed privately so we can make sure your team has the support and onboarding it needs. Reach out and we'll send you the image URL, chart, and module access and get you set up — you'll need them for the methods below. | Method | Best For | |--------|----------| | [Docker Compose](/docs/deployment/docker-compose) | Development, small teams, quick evaluation | | [Kubernetes](/docs/deployment/kubernetes) | Production, scaling, existing K8s infrastructure | | [Amazon ECS](/docs/deployment/ecs) | AWS-native deployments, managed container orchestration | | [Google Cloud Run](/docs/deployment/cloud-run) | GCP-native, serverless, fully managed containers | Once your server is running, head back to the [documentation index](/docs) to explore the platform capabilities. --- # Hosted (Stategraph Cloud) Source: https://stategraph.com/docs/deployment/hosted Use Stategraph Cloud, the fully managed hosted service. Start a free trial, create an API key, and point the CLI at your hosted instance. Stategraph Cloud is the fully managed option — we run the server, database, upgrades, and availability, so there is no infrastructure for you to operate. If you would rather keep everything in your own environment, see [self-hosted deployment](/docs/deployment) instead. ## Start a free trial 1. Go to [app.stategraph.cloud](https://app.stategraph.cloud) and sign up. 2. Sign in to create your workspace. New accounts include a **30-day free trial**. ## Connect the CLI Once you are signed in, the workflow is the same as self-hosted — you just point the CLI at your hosted instance instead of a server you run. First, create an API key: in the Stategraph dashboard, open **Settings → API Keys** and create one. Then point the CLI at your hosted instance using the base URL shown in your dashboard: ```bash export STATEGRAPH_API_BASE="https://" export STATEGRAPH_API_KEY="" ``` Verify the connection: ```bash stategraph info stategraph user tenants list ``` From here, everything works exactly as documented for self-hosted Stategraph. ## What's the same Hosted and self-hosted Stategraph share the same CLI, API, and features: - [Velocity](/docs/velocity) — parallel plan and apply, and the Terraform HTTP backend - [Inventory](/docs/inventory) and [Cost Analysis](/docs/cost) - [Insights](/docs/insights) — timeline, blast radius, graph explorer - [CLI](/docs/cli) and [REST API](/docs/reference/api) ## Hosted vs self-hosted | | Hosted (Stategraph Cloud) | Self-hosted | |--|---------------------------|-------------| | Who runs the server | Stategraph | You | | Upgrades & availability | Managed for you | Your responsibility | | Where your data lives | Stategraph Cloud | Entirely in your own infrastructure | | Setup | Sign up and connect | [Deploy a server](/docs/deployment) | Want to keep state and data fully in your environment? See [Deployment](/docs/deployment) for Docker Compose, Kubernetes, and Amazon ECS. --- # Docker Compose Source: https://stategraph.com/docs/deployment/docker-compose Deploy Stategraph using Docker Compose for development and small teams. Quick setup with PostgreSQL and optional authentication configuration. Deploy Stategraph using Docker Compose. This is the fastest way to get started and is suitable for development, evaluation, and small team deployments. ## Releases The Stategraph server image is distributed privately so we can make sure every team has the support and onboarding they need. [Reach out through our contact page](https://stategraph.com/contact) and we'll get you set up with access and aligned on your deployment. Once you have access, use the `latest` tag for the most recent stable release, or pin to a specific version. ## Prerequisites - Docker Engine 20.10 or later - Docker Compose v2.0 or later - Port 8080 available ## Quick Start ### 1. Create a project directory ```bash mkdir stategraph && cd stategraph ``` ### 2. Create the docker-compose.yml file ```yaml services: db: image: postgres:17-alpine user: postgres environment: POSTGRES_PASSWORD: "stategraph" POSTGRES_USER: "stategraph" POSTGRES_DB: "stategraph" healthcheck: test: ["CMD", "pg_isready", "-d", "stategraph", "-U", "stategraph"] interval: 3s timeout: 3s retries: 5 volumes: - db:/var/lib/postgresql/data/ networks: - stategraph server: image: :latest # image URL provided when you get access: https://stategraph.com/contact environment: # Database configuration DB_HOST: "db" DB_PORT: "5432" DB_USER: "stategraph" DB_PASS: "stategraph" DB_NAME: "stategraph" # Required: Your public URL STATEGRAPH_UI_BASE: "http://localhost:8080" ports: - "8080:8080" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"] interval: 10s timeout: 5s retries: 5 start_period: 120s depends_on: db: condition: service_healthy networks: - stategraph networks: stategraph: volumes: db: ``` ### 3. Start Stategraph ```bash docker compose up -d ``` ### 4. Verify the deployment ```bash # Check service health docker compose ps # Check the health endpoints curl http://localhost:8080/health/live curl http://localhost:8080/health/ready ``` ### 5. Access the UI Open in your browser. ## Authentication Stategraph supports two authentication modes. Choose based on your use case: | Mode | Use Case | |------|----------| | **Local Authentication** | Direct user management with email/password authentication | | **OAuth** | Organizations using SSO, Google Workspace, enterprise identity providers | ### Option A: Local Authentication (Default) Local authentication is enabled by default and requires no additional configuration. On first access, you'll be guided through creating an admin account. **First-Time Setup:** 1. Navigate to 2. You'll see the setup wizard prompting you to create an admin account 3. Fill in the required fields: - **Email**: your@email.com (used for login) - **Password**: Minimum 8 characters - **Name**: Your Name - **Organization**: Your Organization 4. Click **Create Admin Account** 5. You'll be automatically logged in **Creating Additional Users:** Once logged in as admin: 1. Navigate to **Settings > Users** 2. Click **Create User** 3. Fill in user details (email, password, name) 4. Toggle **Is Admin** if the user needs admin privileges 5. Click **Create** See the [Local Authentication guide](/docs/authentication/local-authentication) for detailed user management instructions. ### Option B: OAuth Authentication (Production) For production deployments, configure OAuth with your identity provider. **Google OAuth:** 1. Create OAuth credentials in [Google Cloud Console](https://console.cloud.google.com/apis/credentials) 2. Set **Authorized redirect URIs** to: `http://localhost:8080/oauth2/google/callback` (or your production URL) 3. Add these environment variables to your `docker-compose.yml`: ```yaml services: server: environment: # ... existing config ... STATEGRAPH_OAUTH_TYPE: "google" STATEGRAPH_OAUTH_CLIENT_ID: "your-client-id.apps.googleusercontent.com" STATEGRAPH_OAUTH_CLIENT_SECRET: "your-client-secret" STATEGRAPH_OAUTH_REDIRECT_BASE: "http://localhost:8080" # Must match your public URL STATEGRAPH_OAUTH_EMAIL_DOMAIN: "yourcompany.com" # Optional: restrict to domain ``` **Generic OIDC (Okta, Auth0, Azure AD, etc.):** 1. Create an application in your OIDC provider 2. Set the callback/redirect URI to: `http://localhost:8080/oauth2/oidc/callback` (or your production URL) 3. Add these environment variables to your `docker-compose.yml`: ```yaml services: server: environment: # ... existing config ... STATEGRAPH_OAUTH_TYPE: "oidc" STATEGRAPH_OAUTH_CLIENT_ID: "your-client-id" STATEGRAPH_OAUTH_CLIENT_SECRET: "your-client-secret" STATEGRAPH_OAUTH_OIDC_ISSUER_URL: "https://your-provider.com" STATEGRAPH_OAUTH_REDIRECT_BASE: "http://localhost:8080" # Must match your public URL STATEGRAPH_OAUTH_EMAIL_DOMAIN: "yourcompany.com" # Optional ``` After adding OAuth configuration, apply the changes: ```bash docker compose up -d ``` This recreates the server container with the new configuration. Users can now log in via OAuth at . See [Authentication](/docs/authentication) for detailed OAuth setup instructions. ## Configuration ### Required Environment Variables | Variable | Description | Example | |----------|-------------|---------| | `STATEGRAPH_UI_BASE` | Public URL for the UI | `http://localhost:8080` | | `DB_HOST` | PostgreSQL hostname | `db` | | `DB_PORT` | PostgreSQL port | `5432` | | `DB_USER` | PostgreSQL username | `stategraph` | | `DB_PASS` | PostgreSQL password | `stategraph` | | `DB_NAME` | PostgreSQL database name | `stategraph` | ### Optional Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `STATEGRAPH_PORT` | `8180` | Internal port the backend listens on | | `STATEGRAPH_ACCESS_LOG` | `off` | Enable nginx access logging (`/dev/stdout` to enable) | | `STATEGRAPH_CLIENT_MAX_BODY_SIZE` | `512m` | Maximum state file size | | `STATEGRAPH_OAUTH_REDIRECT_BASE` | - | OAuth callback URL base (**required for OAuth**, must match `STATEGRAPH_UI_BASE`) | | `DB_CONNECT_TIMEOUT` | `120` | Database connection timeout (seconds) | | `DB_MAX_POOL_SIZE` | `100` | Maximum database connection pool size | | `STATEGRAPH_TRANSACTION_SUBGRAPH_RETENTION_DAYS` | `7` | Days a committed transaction's subgraph is retained before garbage collection (controls DB space) | ### Using an Environment File For easier configuration, create a `.env` file: ```bash # .env STATEGRAPH_UI_BASE=http://localhost:8080 # OAuth (uncomment for production) # STATEGRAPH_OAUTH_TYPE=google # STATEGRAPH_OAUTH_CLIENT_ID=your-client-id # STATEGRAPH_OAUTH_CLIENT_SECRET=your-client-secret # STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com # Database (only needed if using external PostgreSQL) # DB_HOST=your-postgres-host # DB_PORT=5432 # DB_USER=stategraph # DB_PASS=your-secure-password # DB_NAME=stategraph ``` Update docker-compose.yml to use the env file: ```yaml services: server: image: :latest # image URL provided when you get access: https://stategraph.com/contact env_file: - .env environment: DB_HOST: "db" DB_PORT: "5432" DB_USER: "stategraph" DB_PASS: "stategraph" DB_NAME: "stategraph" # ... rest of config ``` ## Enable cost estimation Cost analysis is off by default. The pricing service ships inside the server image; set `STATEGRAPH_COST_ENABLED=true` to turn it on, and because it lives in your Compose config it survives restarts and redeploys. The bundled pricing service runs in the same container, so `STATEGRAPH_PRICING_SERVICE_URL` already defaults to `http://localhost:8090` — set it only to point at an external pricing service. Add it to your `.env`: ```bash # .env STATEGRAPH_COST_ENABLED=true ``` Or set it on the `server` service's `environment:`: ```yaml services: server: environment: # ... existing config ... STATEGRAPH_COST_ENABLED: "true" ``` Then recreate the server container so it boots with cost analysis on: ```bash docker compose up -d ``` On first boot the pricing service loads the price book into the `cloud_pricing` database (stored on the `db` volume) in the background — the server and UI are available immediately. See [Cost Setup](/docs/cost/setup) for verification, refresh cadence, and air-gapped installs. ## Production Considerations For production deployments: ### Use External PostgreSQL For data durability and backup capabilities, use a managed PostgreSQL service or dedicated PostgreSQL server: ```yaml services: server: image: :latest # image URL provided when you get access: https://stategraph.com/contact environment: DB_HOST: "your-postgres-host.example.com" DB_PORT: "5432" DB_USER: "stategraph" DB_PASS: "${DB_PASSWORD}" # Use secrets management DB_NAME: "stategraph" STATEGRAPH_UI_BASE: "https://stategraph.example.com" ``` ### Use HTTPS Place Stategraph behind a reverse proxy with TLS termination, or use a load balancer that handles HTTPS. ### Secure Database Credentials Use Docker secrets or a secrets manager instead of plain text passwords: ```yaml services: server: environment: DB_PASS: "${DB_PASSWORD}" # Set via environment or secrets ``` ## Updating To update to a newer version: ```bash docker compose pull docker compose up -d ``` To pin to a specific version, update the image tag in your `docker-compose.yml`: ```yaml services: server: image: :1.2.3 # pin to a specific version; image URL provided when you get access: https://stategraph.com/contact # ... rest of config ``` Check the [releases page](https://github.com/stategraph/releases/releases) for available versions. ## Troubleshooting ### Container won't start Check the logs: ```bash docker compose logs server docker compose logs db ``` ### Database connection errors Ensure the database container is healthy before the server starts: ```bash docker compose ps ``` The `db` service should show `healthy` status. ### Port already in use Either stop the service using port 8080, or change the port mapping: ```yaml ports: - "8081:8080" # Map to port 8081 instead ``` ## Next Steps - [Set up Velocity](/docs/velocity) — parallel plan and apply for Terraform - [Enable cost estimation](/docs/cost/setup) — surface cost in your terraform plan output - [Set up authentication](/docs/authentication) --- # Kubernetes Source: https://stategraph.com/docs/deployment/kubernetes Deploy Stategraph on Kubernetes using the official Helm chart. Complete installation guide with PostgreSQL, ingress, and authentication setup. Deploy Stategraph on Kubernetes using the official Helm chart. ## Prerequisites - Kubernetes 1.19+ - Helm 3.0+ - `kubectl` configured for your cluster - Ingress controller (optional, for external access) - Access to the Stategraph Helm chart and server image — they're distributed privately so we can make sure your team has the support it needs; [reach out](https://stategraph.com/contact) and we'll get you set up ## Quick Start **1. Add the Helm repository:** ```bash helm repo add stategraph # repo URL provided when you get access: https://stategraph.com/contact helm repo update ``` **2. Install Stategraph:** ```bash helm install stategraph stategraph/stategraph \ --namespace stategraph \ --create-namespace ``` **3. Access Stategraph:** For local testing: ```bash kubectl port-forward -n stategraph svc/stategraph 8080:80 ``` Then access at [http://localhost:8080](http://localhost:8080) > **Cookie Security** > > Stategraph derives cookie security from `STATEGRAPH_UI_BASE`: > > - **`https://` URLs** → cookies are set with the `Secure` flag (recommended for production) > - **`http://` URLs** → cookies are set without the `Secure` flag (suitable for development or internal networks) > > For example, setting `stategraph.ui.base=http://myserver:8080` allows authentication over plain HTTP. ## Production Installation For production with HTTPS and ingress: ```bash helm install stategraph stategraph/stategraph \ --namespace stategraph \ --create-namespace \ --set stategraph.ui.base="https://stategraph.example.com" \ --set stategraph.ui.oauthRedirectBase="https://stategraph.example.com" \ --set ingress.enabled=true \ --set ingress.hosts[0].host="stategraph.example.com" \ --set ingress.hosts[0].paths[0].path="/" \ --set ingress.hosts[0].paths[0].pathType="Prefix" \ --set ingress.tls[0].secretName="stategraph-tls" \ --set ingress.tls[0].hosts[0]="stategraph.example.com" \ --set ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" ``` ## Configuration Options View all available configuration options: ```bash helm show values stategraph/stategraph ``` Common configurations: | Parameter | Description | Default | |-----------|-------------|---------| | `stategraph.image.tag` | Stategraph version | `latest` | | `stategraph.replicaCount` | Number of replicas | `1` | | `stategraph.ui.base` | Public URL | `http://localhost:8080` | | `postgresql.enabled` | Use bundled PostgreSQL | `true` | | `postgresql.auth.existingSecret` | Use external secret | `""` | | `postgresql.persistence.size` | Database storage | `10Gi` | | `ingress.enabled` | Enable ingress | `false` | For all available options, see the chart documentation included with your private chart access, or [reach out](https://stategraph.com/contact) and we'll walk you through it. ## Health Checks Stategraph provides two health check endpoints for Kubernetes probes: | Endpoint | Purpose | Use For | |----------|---------|---------| | `/health/live` | Returns 200 as long as nginx is running | `livenessProbe` | | `/health/ready` | Returns 200 when the backend is ready to serve requests | `readinessProbe` | The Helm chart configures these probes automatically. If you need to customize them: ```yaml # values.yaml livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 10 failureThreshold: 10 ``` During startup, database migrations run before the backend starts. The liveness probe passes immediately (nginx is up), while the readiness probe will fail until migrations complete and the backend is ready. For more details, see the [Health Checks reference](/docs/reference/health-checks). ## Upgrading ```bash helm repo update helm upgrade stategraph stategraph/stategraph -n stategraph ``` ## Uninstalling ```bash helm uninstall stategraph -n stategraph kubectl delete namespace stategraph ``` ## Using External PostgreSQL To use an existing PostgreSQL database instead of the bundled one: ```bash helm install stategraph stategraph/stategraph \ --namespace stategraph \ --create-namespace \ --set postgresql.enabled=false \ --set postgresql.host="your-postgres-host.example.com" \ --set postgresql.port=5432 \ --set postgresql.auth.username="stategraph" \ --set postgresql.auth.existingSecret="external-db-secret" ``` Create the secret with your database password: ```bash kubectl create secret generic external-db-secret \ --from-literal=db-password='your-password' \ -n stategraph ``` ## Authentication To enable OAuth authentication, configure the OAuth settings during installation: ```bash helm install stategraph stategraph/stategraph \ --namespace stategraph \ --create-namespace \ --set stategraph.oauth.enabled=true \ --set stategraph.oauth.type="google" \ --set stategraph.oauth.clientId="your-client-id" \ --set stategraph.oauth.clientSecret="your-client-secret" ``` See the [Authentication guide](/docs/authentication) for detailed OAuth setup instructions. ## Enable cost estimation Cost analysis is off by default. The pricing service ships inside the server image; turn it on by setting `STATEGRAPH_COST_ENABLED=true` on the `stategraph` container. Set it as part of the Deployment so every new or rescheduled pod comes up with cost on. The bundled pricing service runs in the same container, so `STATEGRAPH_PRICING_SERVICE_URL` already defaults to `http://localhost:8090` — set it only to point at an external pricing service. Add it to the container's `env` (values shape may differ by chart version — run `helm show values stategraph/stategraph` to confirm where extra environment variables go): ```yaml # values.yaml env: - name: STATEGRAPH_COST_ENABLED value: "true" ``` Then roll out the change: ```bash helm upgrade stategraph stategraph/stategraph -n stategraph -f values.yaml ``` On first boot the pricing service loads the price book into the `cloud_pricing` database in the background, so the server and UI stay available immediately. Make sure `cloud_pricing` has durable storage (a volume or managed PostgreSQL). See [Cost Setup](/docs/cost/setup) for verification, refresh cadence, and air-gapped installs. ## Troubleshooting ```bash # Check pod status kubectl get pods -n stategraph # View logs kubectl logs -n stategraph -l app.kubernetes.io/name=stategraph # Check events kubectl get events -n stategraph --sort-by='.lastTimestamp' ``` ## Next Steps - [Set up Velocity](/docs/velocity) — parallel plan and apply for Terraform - [Enable cost estimation](/docs/cost/setup) — surface cost in your terraform plan output - [Set up OAuth authentication](/docs/authentication) --- # Amazon ECS Source: https://stategraph.com/docs/deployment/ecs Deploy Stategraph on AWS ECS with Fargate using Terraform. Production-ready setup with RDS PostgreSQL, ALB, and VPC configuration. Deploy Stategraph on AWS ECS Fargate with a complete, production-ready infrastructure stack using Terraform. This guide uses the official Stategraph ECS Terraform module for a simple, maintainable deployment. ## Prerequisites Before deploying Stategraph on ECS, ensure you have: - **Terraform 1.0+** installed - **AWS CLI 2.0+** configured with credentials - **AWS Account** with appropriate permissions - **Existing VPC** with private and public subnets (or use the complete example to create one) - **ACM certificate** for HTTPS - **Domain name** for the application - **Access to the Stategraph server image** — it's distributed privately so we can make sure your team has the support it needs; [reach out](https://stategraph.com/contact) and we'll get you set up ## Architecture Overview Stategraph on ECS uses a managed, AWS-native architecture: ``` Internet │ ▼ Application Load Balancer (HTTPS:443) │ ▼ ECS Service (Fargate Tasks in Private Subnets) │ ├─▶ RDS PostgreSQL (Multi-AZ) ├─▶ Secrets Manager (Credentials) └─▶ CloudWatch Logs (Monitoring) ``` **What the module creates:** - ECS cluster and service (Fargate launch type) - Application Load Balancer with HTTPS listener - RDS PostgreSQL database (Multi-AZ by default) - Security groups (ALB, ECS, RDS) - IAM roles for ECS tasks - Secrets Manager secrets for credentials - CloudWatch Log Group for container logs - Auto Scaling policies (optional) **What you must provide:** - VPC and subnets - ACM certificate for HTTPS - Domain name ## Quick Start ### 1. Request ACM Certificate Before deploying, create an SSL certificate in AWS Certificate Manager: ```bash # Request certificate for your domain aws acm request-certificate \ --domain-name stategraph.example.com \ --validation-method DNS \ --region us-east-1 # Note the CertificateArn from the output ``` Follow the AWS Console instructions to validate the certificate via DNS. This typically takes 5-10 minutes. ### 2. Create Terraform Configuration Create a new directory and a `main.tf` file: ```bash mkdir stategraph-ecs && cd stategraph-ecs ``` ```hcl # main.tf module "stategraph" { source = "?ref=v1.0.0" # module source provided when you get access: https://stategraph.com/contact # Network configuration (use your existing VPC) vpc_id = "vpc-xxxxx" private_subnet_ids = ["subnet-xxxxx", "subnet-yyyyy"] public_subnet_ids = ["subnet-aaaaa", "subnet-bbbbb"] # Domain and certificate domain_name = "stategraph.example.com" certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/xxxxx" # Environment environment = "production" # Tags (optional) tags = { Project = "Stategraph" ManagedBy = "Terraform" } } output "alb_dns_name" { description = "Load balancer DNS - create a CNAME record pointing your domain here" value = module.stategraph.alb_dns_name } output "database_endpoint" { description = "RDS database endpoint" value = module.stategraph.database_endpoint sensitive = true } ``` ### 3. Initialize Terraform ```bash terraform init ``` ### 4. Review the Plan ```bash terraform plan ``` This will show all the resources that Terraform will create. ### 5. Deploy ```bash terraform apply ``` Deployment takes approximately 10-15 minutes. ### 6. Configure DNS After deployment, create a DNS record pointing your domain to the ALB: ```bash # Get the ALB DNS name terraform output alb_dns_name # Create a CNAME record in your DNS provider: # Name: stategraph.example.com # Type: CNAME # Value: ``` Or use a Route53 alias record: ```bash # Get the ALB zone ID for Route53 alias terraform output -raw alb_zone_id ``` ### 7. Access Stategraph After DNS propagation (5-10 minutes), access Stategraph at: ``` https://stategraph.example.com ``` > **Cookie Security** > > Stategraph derives cookie security from the configured URL (`STATEGRAPH_UI_BASE`): > > - **`https://` URLs** → cookies are set with the `Secure` flag (recommended for production) > - **`http://` URLs** → cookies are set without the `Secure` flag > > The ECS module sets this to `https://` by default. Ensure the `domain_name` variable matches your certificate and DNS. ## Configuration Options ### Required Variables | Variable | Description | Example | |----------|-------------|---------| | `vpc_id` | VPC ID where resources will be created | `vpc-xxxxx` | | `private_subnet_ids` | Private subnet IDs for ECS and RDS | `["subnet-xxxxx", "subnet-yyyyy"]` | | `public_subnet_ids` | Public subnet IDs for ALB | `["subnet-aaaaa", "subnet-bbbbb"]` | | `domain_name` | Domain name for the application | `stategraph.example.com` | | `certificate_arn` | ACM certificate ARN for HTTPS | `arn:aws:acm:...` | ### Optional Variables (Common Settings) | Variable | Description | Default | |----------|-------------|---------| | `environment` | Environment name | `production` | | `ecs_task_cpu` | ECS task CPU units | `1024` | | `ecs_task_memory` | ECS task memory (MB) | `2048` | | `ecs_desired_count` | Number of ECS tasks | `2` | | `enable_autoscaling` | Enable ECS autoscaling | `true` | | `database_instance_class` | RDS instance type | `db.t3.medium` | | `database_multi_az` | Enable Multi-AZ for RDS | `true` | | `stategraph_image` | Docker image | `:latest` | For a complete list of variables, see the module documentation included with your private module access, or [reach out](https://stategraph.com/contact) and we'll walk you through it. ## Using an Existing VPC If you already have a VPC with public and private subnets, simply reference them in your configuration: ```hcl module "stategraph" { source = "?ref=v1.0.0" # module source provided when you get access: https://stategraph.com/contact # Reference your existing VPC vpc_id = "vpc-0123456789abcdef0" private_subnet_ids = ["subnet-aaa", "subnet-bbb"] # ECS tasks and RDS public_subnet_ids = ["subnet-ccc", "subnet-ddd"] # ALB # ... other required variables } ``` **VPC Requirements:** - At least 2 private subnets in different availability zones (for ECS and RDS) - At least 2 public subnets in different availability zones (for ALB) - NAT Gateway or NAT instances for private subnet internet access - DNS hostnames enabled ## Creating a New VPC If you need to create a VPC, use the [terraform-aws-modules/vpc](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest) module. A complete working example is included with your private module access — [reach out](https://stategraph.com/contact) if you don't have it yet. Example with VPC creation: ```hcl module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0" name = "stategraph-vpc" cidr = "10.0.0.0/16" azs = ["us-east-1a", "us-east-1b", "us-east-1c"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] enable_nat_gateway = true enable_dns_hostnames = true } module "stategraph" { source = "?ref=v1.0.0" # module source provided when you get access: https://stategraph.com/contact vpc_id = module.vpc.vpc_id private_subnet_ids = module.vpc.private_subnets public_subnet_ids = module.vpc.public_subnets # ... other variables } ``` ## Using External PostgreSQL If you have an existing PostgreSQL database, set `create_database = false` and provide connection details: ```hcl module "stategraph" { source = "?ref=v1.0.0" # module source provided when you get access: https://stategraph.com/contact # ... other required variables create_database = false external_database_host = "postgres.example.com" external_database_port = 5432 external_database_name = "stategraph" external_database_username = var.database_username # Use variables for sensitive data external_database_password = var.database_password } ``` **Requirements:** - PostgreSQL 13+ (17.x recommended) - Network connectivity from ECS tasks (configure security groups) - SSL/TLS support recommended ## Authentication Enable OAuth authentication with Google or OIDC: ```hcl module "stategraph" { source = "?ref=v1.0.0" # module source provided when you get access: https://stategraph.com/contact # ... other required variables oauth_enabled = true oauth_provider = "google" # or "oidc" oauth_client_id = var.oauth_client_id oauth_client_secret = var.oauth_client_secret # For OIDC provider: # oauth_issuer_url = "https://accounts.google.com" } ``` **OAuth Redirect URI** — the path is `/oauth2/{provider}/callback`: ``` https://stategraph.example.com/oauth2/google/callback # for Google https://stategraph.example.com/oauth2/oidc/callback # for OIDC ``` Configure this callback URL in your OAuth provider settings. **Important**: Store OAuth credentials securely using Terraform variables (create a `terraform.tfvars` file and add it to `.gitignore`): ```hcl # terraform.tfvars (do not commit) oauth_client_id = "your-client-id" oauth_client_secret = "your-client-secret" ``` For detailed OAuth configuration, see the [Authentication guide](/docs/authentication). ## Enable cost estimation Cost analysis is off by default. The pricing service ships inside the server image; turn it on by setting `STATEGRAPH_COST_ENABLED=true` on the server container. Set it in the **task definition** so cost survives task replacements and rolling deployments. The bundled pricing service runs in the same container, so `STATEGRAPH_PRICING_SERVICE_URL` already defaults to `http://localhost:8090` — set it only to point at an external pricing service. Add it to the server container's `environment` block in the task definition: ```json "containerDefinitions": [ { "name": "stategraph", "environment": [ { "name": "STATEGRAPH_COST_ENABLED", "value": "true" } ] } ] ``` On first boot the pricing service loads the price book into the `cloud_pricing` database in the background, so the server and UI stay available immediately. Use a managed or volume-backed PostgreSQL so `cloud_pricing` has durable storage. See [Cost Setup](/docs/cost/setup) for verification, refresh cadence, and air-gapped installs. ## Health Checks Stategraph provides two health check endpoints for ALB and ECS configuration: | Endpoint | Purpose | Available | |----------|---------|-----------| | `/health/live` | Liveness probe — returns 200 as long as nginx is running | Immediately on startup | | `/health/ready` | Readiness probe — returns 200 when the backend is ready | After database migrations complete | The module configures these automatically. During container startup, database migrations run before the backend starts. The `/health/live` endpoint remains available throughout, while `/health/ready` will return 502 until migrations are complete. For detailed configuration options, see the [Health Checks reference](/docs/reference/health-checks). ## Scaling ### Auto-Scaling (Enabled by Default) The module configures ECS autoscaling based on CPU and memory utilization: ```hcl module "stategraph" { source = "?ref=v1.0.0" # module source provided when you get access: https://stategraph.com/contact # ... other variables enable_autoscaling = true ecs_min_count = 1 ecs_desired_count = 2 ecs_max_count = 4 autoscaling_cpu_threshold = 70 # Scale when CPU > 70% autoscaling_memory_threshold = 80 # Scale when memory > 80% } ``` ### Manual Scaling To scale manually (or for development), disable autoscaling: ```hcl module "stategraph" { source = "?ref=v1.0.0" # module source provided when you get access: https://stategraph.com/contact # ... other variables enable_autoscaling = false ecs_desired_count = 3 # Fixed count } ``` ### Vertical Scaling Increase task resources for more demanding workloads: ```hcl module "stategraph" { source = "?ref=v1.0.0" # module source provided when you get access: https://stategraph.com/contact # ... other variables ecs_task_cpu = 2048 # 2 vCPU ecs_task_memory = 4096 # 4 GB } ``` ## Upgrading ### Update Stategraph Version 1. Update the image tag in your configuration: ```hcl module "stategraph" { # ... other variables stategraph_image = ":1.2.0" # image URL provided when you get access: https://stategraph.com/contact } ``` 2. Apply the change: ```bash terraform apply ``` ECS will perform a rolling deployment with zero downtime. ### Update Module Version 1. Update the module reference: ```hcl module "stategraph" { source = "?ref=v1.1.0" # module source provided when you get access: https://stategraph.com/contact # ... } ``` 2. Reinitialize and apply: ```bash terraform init -upgrade terraform plan terraform apply ``` ## Monitoring ### View Container Logs ```bash # Get the log group name from Terraform aws logs tail $(terraform output -raw cloudwatch_log_group_name) --follow # Filter by error level aws logs tail /ecs/stategraph-production --filter-pattern "ERROR" ``` ### View ECS Service Status ```bash # Get cluster and service names from Terraform aws ecs describe-services \ --cluster $(terraform output -raw ecs_cluster_name) \ --services $(terraform output -raw ecs_service_name) ``` ### CloudWatch Metrics The module automatically enables Container Insights for enhanced monitoring. View metrics in the CloudWatch Console under **Container Insights > ECS Clusters**. **Key Metrics:** - **CPUUtilization**: Percentage of allocated CPU used - **MemoryUtilization**: Percentage of allocated memory used - **TargetResponseTime**: ALB target response time - **HealthyHostCount**: Number of healthy tasks - **RequestCount**: Number of requests to ALB ### Database Credentials Access database credentials from Secrets Manager: ```bash aws secretsmanager get-secret-value \ --secret-id $(terraform output -raw database_secret_arn) \ --query SecretString --output text | jq -r '.password' ``` ## Complete Example For a complete, working example that includes VPC creation and all optional features, see the `examples/complete` directory included with your private module access. [Reach out](https://stategraph.com/contact) if you need access. ## Module Documentation Detailed module documentation — including all variables, outputs, advanced configuration options, and examples — is distributed privately along with the module. [Reach out through our contact page](https://stategraph.com/contact) and we'll get you set up with access. ## Next Steps - [Set up Velocity](/docs/velocity) — parallel plan and apply for Terraform - [Enable cost estimation](/docs/cost/setup) — surface cost in your terraform plan output - [Set up authentication](/docs/authentication) - [Perform gap analysis on AWS resources](/docs/inventory/gap-analysis#aws) --- # Google Cloud Run Source: https://stategraph.com/docs/deployment/cloud-run Deploy Stategraph on Google Cloud Run with a managed Cloud SQL PostgreSQL backend, a Cloud SQL Auth Proxy sidecar, and Secret Manager. Deploy Stategraph on Google Cloud Run as a serverless, fully managed container. This guide pairs the Stategraph server image with a managed Cloud SQL for PostgreSQL database, connected through a Cloud SQL Auth Proxy sidecar, with the database password stored in Secret Manager. ## Prerequisites Before deploying Stategraph on Cloud Run, ensure you have: - **`gcloud` CLI** installed and authenticated (`gcloud auth login`) - **A Google Cloud project** with billing enabled - **Owner or Editor** on the project (or the equivalent granular roles for Cloud Run, Cloud SQL, Secret Manager, and Artifact Registry) - **Docker** installed locally (only needed to mirror the image into Artifact Registry — see below) - **Access to the Stategraph server image** — it's distributed privately so we can make sure your team has the support it needs; [reach out](https://stategraph.com/contact) and we'll get you set up > **Cloud Run only pulls from Artifact Registry, GCR, or Docker Hub** > > Cloud Run cannot deploy an image directly from an arbitrary registry such as `ghcr.io` — it accepts images only from Artifact Registry (`*-docker.pkg.dev`), Container Registry (`gcr.io`), or Docker Hub (`docker.io`). [Mirror the Stategraph image into Artifact Registry](#1-mirror-the-image-into-artifact-registry) in your project first (a one-time copy), then point Cloud Run at the Artifact Registry path. ## Architecture Overview Stategraph on Cloud Run uses a serverless, GCP-native architecture: ``` Internet (HTTPS) │ ▼ Cloud Run Service ┌──────────────────────────────────┐ │ stategraph (port 8080) │ │ │ 127.0.0.1:5432 │ │ ▼ │ │ cloud-sql-proxy (sidecar) │ └───────────────┬──────────────────┘ ▼ Cloud SQL for PostgreSQL ▲ Secret Manager (DB password) ``` **What you create:** - A Cloud Run service running two containers: the Stategraph server (ingress, port 8080) and the Cloud SQL Auth Proxy sidecar - A Cloud SQL for PostgreSQL instance, database, and user - A Secret Manager secret holding the database password - An Artifact Registry repository holding the Stategraph image - IAM bindings granting the Cloud Run runtime service account access to Cloud SQL, Secret Manager, and Artifact Registry **Why a Cloud SQL Auth Proxy sidecar?** Stategraph connects to PostgreSQL over TCP using `DB_HOST` and `DB_PORT`. The sidecar exposes the database on `127.0.0.1:5432` inside the service, so the server connects with `DB_HOST=127.0.0.1` — no code changes, no public database IP exposed. ## Quick Start The commands below assume these shell variables. Adjust them to your project: ```bash export PROJECT_ID="your-project-id" export REGION="us-central1" export INSTANCE="stategraph-db" export AR_REPO="stategraph" gcloud config set project "$PROJECT_ID" gcloud config set run/region "$REGION" ``` ### 1. Enable APIs ```bash gcloud services enable \ run.googleapis.com \ sqladmin.googleapis.com \ secretmanager.googleapis.com \ artifactregistry.googleapis.com \ compute.googleapis.com ``` ### 2. Mirror the image into Artifact Registry Create an Artifact Registry repository and copy the Stategraph image into it. The image reference (``) is provided when you [get access](https://stategraph.com/contact). ```bash # Create the repository gcloud artifacts repositories create "$AR_REPO" \ --repository-format=docker \ --location="$REGION" \ --description="Stategraph server images" # Authenticate Docker to Artifact Registry gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet # Mirror the image (one-time copy) export IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/stategraph-server:latest" docker pull :latest # image URL provided when you get access: https://stategraph.com/contact docker tag :latest "$IMAGE" docker push "$IMAGE" ``` ### 3. Create the Cloud SQL database ```bash # Create the instance (ENTERPRISE edition allows the smaller shared-core tiers) gcloud sql instances create "$INSTANCE" \ --database-version=POSTGRES_17 \ --edition=ENTERPRISE \ --tier=db-custom-1-3840 \ --region="$REGION" \ --storage-size=10 \ --storage-type=SSD # Create the database and user DB_PASSWORD="$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)" gcloud sql databases create stategraph --instance="$INSTANCE" gcloud sql users create stategraph --instance="$INSTANCE" --password="$DB_PASSWORD" # Note the instance connection name — you'll need it for the sidecar export CONNECTION_NAME="$(gcloud sql instances describe "$INSTANCE" --format='value(connectionName)')" echo "$CONNECTION_NAME" # e.g. your-project-id:us-central1:stategraph-db ``` > **Tier and edition** > > `POSTGRES_17` defaults to the **Enterprise Plus** edition, which rejects the small shared-core tiers. Pass `--edition=ENTERPRISE` to use cost-effective tiers like `db-custom-1-3840` (1 vCPU / 3.75 GB) or the shared-core `db-g1-small` for evaluation. Size up for production. ### 4. Store the database password in Secret Manager ```bash printf '%s' "$DB_PASSWORD" | gcloud secrets create stategraph-db-pass --data-file=- ``` ### 5. Grant the runtime service account access Cloud Run runs as the project's default compute service account unless you specify another. Grant it access to Cloud SQL, the secret, and the Artifact Registry repository: ```bash export PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')" export RUNTIME_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:${RUNTIME_SA}" \ --role="roles/cloudsql.client" --condition=None gcloud secrets add-iam-policy-binding stategraph-db-pass \ --member="serviceAccount:${RUNTIME_SA}" \ --role="roles/secretmanager.secretAccessor" gcloud artifacts repositories add-iam-policy-binding "$AR_REPO" \ --location="$REGION" \ --member="serviceAccount:${RUNTIME_SA}" \ --role="roles/artifactregistry.reader" ``` ### 6. Create the service definition Save the following as `service.yaml`. Replace `IMAGE` and `CONNECTION_NAME` with your values (or substitute the environment variables with `envsubst`): ```yaml apiVersion: serving.knative.dev/v1 kind: Service metadata: name: stategraph labels: cloud.googleapis.com/location: us-central1 spec: template: metadata: annotations: # Keep one warm instance: the price-book loader and cost scheduler are # background jobs that scale-to-zero would interrupt. autoscaling.knative.dev/minScale: "1" autoscaling.knative.dev/maxScale: "3" # CPU is always allocated so background work runs outside request handling. run.googleapis.com/cpu-throttling: "false" run.googleapis.com/startup-cpu-boost: "true" # Start the proxy before the server so the DB is reachable at boot. run.googleapis.com/container-dependencies: '{"server":["cloud-sql-proxy"]}' spec: containers: - name: server image: IMAGE ports: - containerPort: 8080 env: - name: DB_HOST value: "127.0.0.1" - name: DB_PORT value: "5432" - name: DB_USER value: "stategraph" - name: DB_NAME value: "stategraph" - name: DB_PASS valueFrom: secretKeyRef: name: stategraph-db-pass key: "latest" # Set to the service URL after the first deploy (see step 8). - name: STATEGRAPH_UI_BASE value: "https://REPLACE_WITH_SERVICE_URL" # Migrations run on boot; /health/ready returns 502 until they finish. # /health/live is up immediately. Give migrations generous headroom. startupProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 10 failureThreshold: 30 timeoutSeconds: 5 resources: limits: cpu: "1" memory: 1Gi - name: cloud-sql-proxy image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.11.0 args: - "--port=5432" - "--health-check" - "--http-address=0.0.0.0" - "--http-port=9090" - "CONNECTION_NAME" startupProbe: httpGet: path: /startup port: 9090 periodSeconds: 5 failureThreshold: 12 timeoutSeconds: 3 resources: limits: cpu: "1" memory: 512Mi traffic: - percent: 100 latestRevision: true ``` ### 7. Deploy ```bash gcloud run services replace service.yaml --region="$REGION" ``` The deploy completes once the startup probe passes — that is, after database migrations finish and `/health/ready` returns 200. ### 8. Set the public URL and redeploy Cloud Run assigns the service URL on first deploy. Stategraph needs it in `STATEGRAPH_UI_BASE` for correct links, cookies, and OAuth callbacks: ```bash export SERVICE_URL="$(gcloud run services describe stategraph --region="$REGION" --format='value(status.url)')" echo "$SERVICE_URL" # e.g. https://stategraph-xxxxxxxxxx.us-central1.run.app sed -i "s#https://REPLACE_WITH_SERVICE_URL#${SERVICE_URL}#" service.yaml gcloud run services replace service.yaml --region="$REGION" ``` > **Cookie Security** > > Stategraph derives cookie security from `STATEGRAPH_UI_BASE`. The Cloud Run URL is always `https://`, so cookies are set with the `Secure` flag automatically. ### 9. Expose the UI To make the UI reachable in a browser, allow unauthenticated access: ```bash gcloud run services add-iam-policy-binding stategraph \ --region="$REGION" \ --member=allUsers \ --role=roles/run.invoker ``` To keep the service private instead, skip this step and reach it with an identity token (see the next step) or front it with Identity-Aware Proxy. > If your organization enforces **Domain Restricted Sharing** (`iam.allowedPolicyMemberDomains`), the `allUsers` binding is rejected with a "do not belong to a permitted customer" error. Grant the project an `allowAll` override for that constraint, or grant `roles/run.invoker` to specific users/groups instead. ### 10. Verify the deployment ```bash # Liveness — available as soon as nginx is up curl -sS -o /dev/null -w "live: %{http_code}\n" "$SERVICE_URL/health/live" # Readiness — 200 once migrations are done and the backend is connected curl -sS -o /dev/null -w "ready: %{http_code}\n" "$SERVICE_URL/health/ready" ``` If the service requires authentication (you skipped step 9), add an identity token: ```bash TOKEN="$(gcloud auth print-identity-token)" curl -sS -H "Authorization: Bearer $TOKEN" \ -o /dev/null -w "ready: %{http_code}\n" "$SERVICE_URL/health/ready" ``` Then open `$SERVICE_URL` in your browser — the setup wizard prompts you to create an admin account. ## Configuration Stategraph is configured entirely through environment variables on the `server` container. The Quick Start sets the minimum; common additions are below. See the [Environment Variables reference](/docs/reference/environment-variables) for the full list. | Variable | Description | Example | |----------|-------------|---------| | `STATEGRAPH_UI_BASE` | Public URL (the Cloud Run service URL or custom domain) | `https://stategraph-xxxx.us-central1.run.app` | | `DB_HOST` | Database host — `127.0.0.1` via the sidecar proxy | `127.0.0.1` | | `DB_PORT` | Database port the sidecar listens on | `5432` | | `DB_USER` / `DB_NAME` | Database user and name | `stategraph` | | `DB_PASS` | Database password (from Secret Manager) | — | ### Custom domain Map a custom domain to the service, then update `STATEGRAPH_UI_BASE` to match: ```bash gcloud run domain-mappings create \ --service=stategraph \ --domain=stategraph.example.com \ --region="$REGION" ``` After the domain is mapped and DNS propagates, set `STATEGRAPH_UI_BASE=https://stategraph.example.com` and redeploy. ## Authentication Enable OAuth by adding environment variables to the `server` container. The OAuth callback path is `/oauth2/{provider}/callback`, so set the redirect base to your public URL: ```yaml # Add to the server container's env: in service.yaml - name: STATEGRAPH_OAUTH_TYPE value: "google" # or "oidc" - name: STATEGRAPH_OAUTH_CLIENT_ID value: "your-client-id.apps.googleusercontent.com" - name: STATEGRAPH_OAUTH_CLIENT_SECRET valueFrom: secretKeyRef: name: stategraph-oauth-secret key: "latest" - name: STATEGRAPH_OAUTH_REDIRECT_BASE value: "https://stategraph.example.com" # must match STATEGRAPH_UI_BASE - name: STATEGRAPH_OAUTH_EMAIL_DOMAIN value: "yourcompany.com" # optional: restrict to a domain ``` Set the **Authorized redirect URI** in your provider to `https:///oauth2/google/callback` (or `/oauth2/oidc/callback`). Store the client secret in Secret Manager and grant the runtime service account `roles/secretmanager.secretAccessor` on it, exactly as with the database password. For details, see the [Authentication guide](/docs/authentication). ## Enable cost estimation Cost analysis is off by default. The pricing service ships inside the server image; turn it on by adding `STATEGRAPH_COST_ENABLED=true` to the `server` container's `env`. On Cloud Run you must also point the pricing service at Cloud SQL: it builds its own database connection from `PRICING_DB_*` and does **not** inherit `DB_HOST`/`DB_PASS`. Its defaults assume a host named `db` (which does not exist on Cloud Run) and the password `stategraph` (not your generated secret), so with only `STATEGRAPH_COST_ENABLED=true` the pricing service cannot reach the database and crash-loops — cost never works. Set `PRICING_DB_HOST=127.0.0.1` to reach Cloud SQL through the same sidecar, and give it the same password as `DB_PASS` from Secret Manager. Because these live in the service definition, cost survives every new revision and reschedule. The bundled pricing service runs in the same container, so `STATEGRAPH_PRICING_SERVICE_URL` already defaults to `http://localhost:8090` — set it only to point at an external pricing service. ```yaml # Add to the server container's env: in service.yaml - name: STATEGRAPH_COST_ENABLED value: "true" # The bundled pricing service builds its DB connection from PRICING_DB_* and does # not inherit DB_HOST/DB_PASS. Point it at Cloud SQL through the same sidecar and # give it the same password as DB_PASS. PRICING_DB_USER/PRICING_DB_NAME already # default to stategraph/cloud_pricing, so they need no override. - name: PRICING_DB_HOST value: "127.0.0.1" - name: PRICING_DB_PASSWORD valueFrom: secretKeyRef: name: stategraph-db-pass key: "latest" ``` With `PRICING_DB_*` pointed at Cloud SQL, the pricing service loads the price book into the `cloud_pricing` database on first boot in the background, so the server and UI stay available immediately. The `gcloud`-created Cloud SQL user has the `cloudsqlsuperuser` role (which includes `CREATEDB`), so the loader creates the `cloud_pricing` database itself on first boot — no extra grant needed. Because the price book then lives in your **Cloud SQL** instance, it is durable across Cloud Run's ephemeral instances, and the `minScale: "1"` plus always-allocated CPU in this guide keep the background loader and scheduled recomputes running. See [Cost Setup](/docs/cost/setup) for verification, refresh cadence, and air-gapped installs. ## Health Checks Stategraph exposes two health endpoints, both used by the startup probe in this guide: | Endpoint | Purpose | Available | |----------|---------|-----------| | `/health/live` | Liveness — returns 200 as long as nginx is running | Immediately on startup | | `/health/ready` | Readiness — returns 200 when the backend is ready | After database migrations complete | The `server` container's `startupProbe` targets `/health/ready` so Cloud Run does not route traffic until migrations finish. With `failureThreshold: 30` and `periodSeconds: 10`, migrations have up to five minutes — raise the threshold for very large databases. For details, see the [Health Checks reference](/docs/reference/health-checks). ## Scaling Cloud Run autoscales by request concurrency. Control the bounds with annotations on the revision template: ```yaml autoscaling.knative.dev/minScale: "1" # keep one warm instance (recommended) autoscaling.knative.dev/maxScale: "3" # cap concurrent instances ``` **Keep `minScale` at 1 or higher.** Stategraph runs background work — the price-book loader and scheduled cost recomputes — that does not run when the service scales to zero. Always-allocated CPU (`run.googleapis.com/cpu-throttling: "false"`) ensures that work proceeds between requests. To scale vertically, raise the `server` container's CPU and memory limits: ```yaml resources: limits: cpu: "2" memory: 2Gi ``` ## Upgrading ### Update the Stategraph version Mirror the new image tag into Artifact Registry, update the `image:` in `service.yaml`, and redeploy: ```bash docker pull :1.2.0 # image URL provided when you get access: https://stategraph.com/contact docker tag :1.2.0 "${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/stategraph-server:1.2.0" docker push "${REGION}-docker.pkg.dev/${PROJECT_ID}/${AR_REPO}/stategraph-server:1.2.0" # Update image: in service.yaml, then: gcloud run services replace service.yaml --region="$REGION" ``` Cloud Run performs a gradual rollout and shifts traffic to the new revision once its startup probe passes — no downtime. ## Monitoring ### View logs ```bash gcloud run services logs read stategraph --region="$REGION" --limit=100 ``` Or stream them in the Cloud Console under **Cloud Run > stategraph > Logs**. ### Inspect the service ```bash gcloud run services describe stategraph --region="$REGION" ``` Request count, latency, instance count, CPU, and memory are available in the Cloud Console under **Cloud Run > stategraph > Metrics**. ## Troubleshooting ### Deploy rejected: invalid image host **Symptoms**: `gcloud run services replace` fails referencing the image host. **Error message** ``` Expected an image path like [host/]repo-path[:tag and/or @digest], where host is one of [region.]gcr.io, [region-]docker.pkg.dev or docker.io ``` **Solution**: - Cloud Run cannot pull from `ghcr.io` or other arbitrary registries - Mirror the image into Artifact Registry (see [step 2](#2-mirror-the-image-into-artifact-registry)) and reference the `*-docker.pkg.dev` path ### Cloud SQL instance create fails on tier **Symptoms**: `gcloud sql instances create` fails immediately. **Error message** ``` Invalid Tier (db-f1-micro) for (ENTERPRISE_PLUS) Edition ``` **Solution**: - `POSTGRES_17` defaults to the Enterprise Plus edition, which does not allow shared-core tiers - Add `--edition=ENTERPRISE` and choose a supported tier such as `db-custom-1-3840` or `db-g1-small` ### Service stuck starting / readiness never passes **Symptoms**: The deploy times out, or `/health/ready` keeps returning 502. **Solutions**: 1. Confirm the runtime service account has `roles/cloudsql.client` and `roles/secretmanager.secretAccessor` 2. Check the logs for the `cloud-sql-proxy` container — a bad `CONNECTION_NAME` or missing IAM shows up there 3. For a large database, raise the `server` startup probe `failureThreshold` to give migrations more time ## Next Steps - [Set up Velocity](/docs/velocity) — parallel plan and apply for Terraform - [Enable cost estimation](/docs/cost/setup) — surface cost in your terraform plan output - [Set up authentication](/docs/authentication) - [Perform gap analysis on GCP resources](/docs/inventory/gap-analysis) --- # Introduction Source: https://stategraph.com/docs/authentication Configure authentication for Stategraph using OAuth providers. Support for Google OAuth, generic OIDC, and local email/password authentication. Stategraph supports OAuth 2.0 authentication to secure access to the UI and API. ## Overview Without authentication configured, Stategraph is accessible to anyone with network access. For production deployments, you should configure authentication. Stategraph supports three authentication modes: | Mode | Use Case | |------|----------| | **Local Authentication** | Email/password auth with direct user management | | **Google OAuth** | Organizations using Google Workspace | | **Generic OIDC** | Any OIDC-compatible provider (Okta, Auth0, Azure AD, etc.) | ## Authentication Flow ``` ┌──────────┐ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │ User │────▶│ Stategraph │────▶│ OAuth2 Proxy │────▶│ OAuth Provider │ │ Browser │ │ UI │ │ │ │ (Google/OIDC) │ └──────────┘ └─────────────┘ └──────────────┘ └────────────────┘ │ │ │ │ │◀─── Token ──────────│ │◀─── Session Cookie ──────────────────│ │ │ Authenticated requests ▼ ┌─────────────┐ │ Stategraph │ │ API │ └─────────────┘ ``` 1. User accesses Stategraph 2. If not authenticated, redirected to OAuth provider 3. User authenticates with their identity provider 4. Provider returns token to oauth2-proxy 5. Stategraph creates the user (if new) and adds them to the Default tenant 6. Session cookie set in browser 7. Subsequent requests include session cookie ## New User Onboarding When a user logs in via OAuth for the first time: 1. **User account created** - Stategraph creates a user record linked to the OAuth identity 2. **Added to Default tenant** - The user is automatically added to the shared "Default" tenant 3. **Ready to use** - The user can immediately access states and create API keys All users share the Default tenant, making collaboration simple. This means new users can immediately start using Stategraph after their first login. To use the CLI or Terraform: 1. Log in via OAuth in your browser 2. Go to **Settings** and create an API key 3. Use the API key with `STATEGRAPH_API_KEY` environment variable ## Quick Configuration ### Google OAuth ```bash # Required STATEGRAPH_OAUTH_TYPE=google STATEGRAPH_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com STATEGRAPH_OAUTH_CLIENT_SECRET=your-client-secret # Optional STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com # Restrict to domain STATEGRAPH_OAUTH_DISPLAY_NAME="Sign in with Google" ``` [Detailed Google OAuth setup](/docs/authentication/google-oauth) ### Generic OIDC ```bash # Required STATEGRAPH_OAUTH_TYPE=oidc STATEGRAPH_OAUTH_CLIENT_ID=your-client-id STATEGRAPH_OAUTH_CLIENT_SECRET=your-client-secret STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://your-provider.com # Optional STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com STATEGRAPH_OAUTH_DISPLAY_NAME="Sign in with SSO" ``` [Detailed OIDC setup](/docs/authentication/oidc) ## Authentication Components ### UI Authentication Web browser access is authenticated via cookies: 1. User clicks login button 2. Redirected to OAuth provider 3. After authentication, session cookie is set 4. Cookie is sent with each request ### API Authentication The Stategraph API accepts two authentication methods: **API Key (Bearer Token)**: For CLI, Terraform, and programmatic access ```bash curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" http://localhost:8080/api/v1/... ``` **Session Cookie**: For browser-based access (handled automatically by the browser) ## API Keys Stategraph supports two types of API credentials: | Type | Use Case | Created By | |------|----------|------------| | **Personal API Key** | CLI access, personal scripts | Individual user | | **Service Account** | CI/CD pipelines, automation | Team setup | Both types generate tokens that do not expire. ### Personal API Keys Personal API keys are tied to your OAuth user account. Good for CLI access and personal use. **Via UI** (recommended): 1. Log in to Stategraph via OAuth 2. Navigate to **Settings** 3. In the **API Keys** section, click **Create API Key** 4. Copy the generated token (it's only shown once) **Via API** (requires active browser session): ```bash curl -X POST http://localhost:8080/api/v1/user/access-tokens \ -H "Content-Type: application/json" \ -H "Cookie: session=" \ -d '{"name": "my-api-key"}' ``` Response: ```json {"token": ""} ``` **Via CLI** (capability-scoped, uses an existing API key): ```bash stategraph user access-tokens create --name my-api-key ``` Unlike the browser-session `curl` above, the CLI authenticates with an existing API key (`STATEGRAPH_API_KEY` plus `--api-base`), so no browser session is needed. Capability flags scope what the new token may do — `--admin`, `--commit` (with `--commit-tenant`/`--commit-state`), `--preview` (with `--preview-tenant`/`--preview-state`), `--users-manage` (with `--users-manage-tenant`), `--sudo-user`, `--access-token-create`, and `--access-token-refresh`, or `--capabilities-json` for raw JSON. With no capability flags, the token inherits your current session's capabilities. For example, a commit-only token restricted to a single state: ```bash stategraph user access-tokens create \ --name ci-prod-apply \ --commit \ --commit-state '=*' ``` List and revoke tokens with `stategraph user access-tokens list` and `stategraph user access-tokens delete --token-id `. See [Access Tokens & Capabilities](/docs/authentication/access-tokens) for the full capability model. ### Service Accounts Service accounts are dedicated API identities for CI/CD pipelines. Unlike personal API keys, service accounts: - Are separate user identities (type: `api`) - Survive employee turnover - Provide clear audit trails ("ci-production" made this change vs "john@example.com") - Can be managed independently from human users **Via UI**: 1. Log in to Stategraph via OAuth 2. Navigate to **Settings** 3. In the **Service Accounts** section, enter a name (e.g., "ci-production", "github-actions") 4. Click **Create** 5. Copy the generated token (it's only shown once) **Via API**: ```bash curl -X POST http://localhost:8080/api/v1/api-users \ -H "Content-Type: application/json" \ -H "Cookie: session=" \ -d '{"name": "ci-production", "tenant_id": ""}' ``` Response: ```json {"user_id": "", "token": ""} ``` **Recommendation**: Use service accounts for CI/CD pipelines and shared automation. Use personal API keys for individual CLI access. ### Using API Keys **With the CLI**: ```bash export STATEGRAPH_API_KEY="" stategraph tenant list ``` **With the API**: ```bash curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" http://localhost:8080/api/v1/... ``` Confirm which identity a key authenticates as — and the capabilities it carries — with [`stategraph user whoami`](/docs/cli/user). ### Token Properties - Tokens require no prefix (use directly as Bearer token) - Tokens do not expire - Each token creates audit trail entries - Tokens can be listed and revoked at any time: - List your tokens: `GET /api/v1/user/access-tokens` - Revoke a token: `POST /api/v1/user/access-tokens/revoke?token_id=` ### CI/CD Setup For CI/CD pipelines, we recommend using **service accounts** instead of personal API keys: 1. **Create a service account**: Log in to Stategraph UI, go to **Settings > Service Accounts**, and create one (e.g., "github-actions") 2. **Store as secret**: Add the token as a secret in your CI/CD platform (e.g., `STATEGRAPH_API_KEY`) 3. **Configure the pipeline**: Set the environment variable ```yaml # GitHub Actions example env: STATEGRAPH_API_BASE: https://stategraph.example.com STATEGRAPH_API_KEY: ${{ secrets.STATEGRAPH_API_KEY }} # For Terraform env: TF_HTTP_PASSWORD: ${{ secrets.STATEGRAPH_API_KEY }} ``` Using a service account instead of a personal API key means: - The token remains valid even if the employee who created it leaves - Audit logs clearly show "github-actions" made the change, not a person's name - You can create separate service accounts for different pipelines (staging vs production) ## Email Domain Restriction Limit access to specific email domains: ```bash # Single domain STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com # Allow all domains STATEGRAPH_OAUTH_EMAIL_DOMAIN=* ``` Users with email addresses outside the allowed domain will be denied access. ## Redirect URLs The OAuth redirect URL must be configured in your OAuth provider. The URL includes the provider type: **For Google OAuth:** ``` https://stategraph.example.com/oauth2/google/callback ``` **For OIDC providers:** ``` https://stategraph.example.com/oauth2/oidc/callback ``` Set the redirect base explicitly with `STATEGRAPH_OAUTH_REDIRECT_BASE`: ```bash STATEGRAPH_OAUTH_REDIRECT_BASE=https://stategraph.example.com ``` If this variable is not set, the callback base defaults to `http://localhost:`, which only works for local development — always set it for any non-local deployment. The `/oauth2/{provider}/callback` routes only exist when OAuth is configured. In local authentication mode they are not registered, so requesting one (for example `GET /oauth2/google/callback`) returns `404`. Only the `google` and `oidc` providers are supported. ## Troubleshooting ### "Invalid redirect URI" The callback URL in your OAuth provider doesn't match. Verify: - Protocol (http vs https) - Hostname matches `STATEGRAPH_UI_BASE` - Path is `/oauth2/google/callback` (for Google) or `/oauth2/oidc/callback` (for OIDC) ### "Access denied" Check: - Email domain restrictions - User is in allowed group (Google Groups) - OAuth app is approved in organization ### Session not persisting Verify: - Cookies are enabled in browser - `STATEGRAPH_UI_BASE` matches the URL you're accessing - No proxy stripping cookies ## Security Considerations 1. **Use HTTPS** in production 2. **Restrict email domains** to your organization 3. **API keys do not expire** - treat them as long-lived secrets 4. **Audit token usage** via transaction logs 5. **Don't commit API keys** to version control ## Next Steps - [Local Authentication](/docs/authentication/local-authentication) - Email/password authentication with user management - [Google OAuth Setup](/docs/authentication/google-oauth) - For Google Workspace organizations - [OIDC Setup](/docs/authentication/oidc) - For other identity providers - [Environment Variables Reference](/docs/reference/environment-variables) --- # Local Authentication Source: https://stategraph.com/docs/authentication/local-authentication Email and password authentication with user management for Stategraph. Production-ready auth without external OAuth dependencies. Local authentication provides email and password-based user management without external OAuth dependencies. This is production-ready authentication with full control over user accounts and passwords. For organizations using Google Workspace or other identity providers, consider [Google OAuth](/docs/authentication/google-oauth) or [OIDC](/docs/authentication/oidc). ## How It Works ``` ┌──────────────┐ ┌─────────────────┐ ┌────────────┐ │ Browser │────▶│ Stategraph │────▶│ PostgreSQL │ │ │ │ Server │ │ │ └──────────────┘ └─────────────────┘ └────────────┘ │ │ │ 1. Enter email │ │ & password │ │ │ │ 2. POST /login │ │ │ │◀─── Session Cookie ─│ │ │ │ 3. Authenticated │ ▼ requests │ ``` 1. User enters email and password 2. Stategraph validates credentials and creates a session 3. Session token is stored as an HTTP-only cookie 4. Subsequent requests are authenticated via the session cookie ## Initial Setup When you first deploy Stategraph with local authentication (the default), you'll see a setup wizard that guides you through creating the first admin account. ### First-Time Setup Wizard 1. Navigate to your Stategraph URL (e.g., ) 2. You'll see the setup form prompting you to create an admin account 3. Fill in the required fields: - **Email**: Your email address (used for login) - **Password**: Minimum 8 characters, maximum 128 characters - **Name**: Your display name - **Organization**: Your organization name 4. Click **Create Admin Account** 5. You'll be automatically logged in and redirected to the Stategraph UI The first user created is automatically granted admin privileges. ### Example Setup ```bash # Deploy Stategraph (Docker Compose example) docker compose up -d # Open browser to http://localhost:8080 # Fill in the setup form: Email: admin@example.com Password: your-secure-password Name: Admin User Organization: My Organization ``` ## User Management ### Creating Users (Admin Only) Admins can create additional users through the Settings page: 1. Log in as an admin user 2. Navigate to **Settings > Users** 3. Click **Create User** 4. Fill in user details: - **Name**: Display name - **Email**: Email address (used for login) - **Password**: Minimum 8 characters - **Is Admin**: Toggle to grant admin privileges 5. Click **Create** to create the user ### User Roles | Role | Capabilities | |------|--------------| | **Admin** | Full access: create/edit/delete users, manage all states and tenants, view all settings | | **Regular User** | Access assigned tenants, create API keys, manage own states | ### Changing Passwords **Users changing their own password:** 1. Navigate to **Settings > Account** 2. Click **Change Password** 3. Enter current password for verification 4. Enter new password (8-128 characters) 5. Confirm new password **Admins changing any user's password:** 1. Navigate to **Settings > Users** 2. Click on the user you want to modify 3. Click **Change Password** 4. Enter new password (no verification required) 5. Confirm new password ### Managing Admin Access Admins can grant or remove admin privileges from other users: 1. Navigate to **Settings > Users** 2. Click on the user you want to modify 3. Toggle the **Is Admin** switch 4. Confirm the change **Important**: You cannot remove admin privileges from yourself if you're the last admin. This prevents accidental lockout. ### Deleting Users Admins can delete users: 1. Navigate to **Settings > Users** 2. Click on the user you want to delete 3. Click **Delete User** 4. Type the user's email to confirm deletion 5. Click **Delete** to confirm **Note**: Deleted users are soft-deleted (marked as deleted in the database but not removed). Their API keys are immediately invalidated. ## Service Accounts (API Users) Service accounts provide programmatic access for CI/CD pipelines and automation without interactive login. ### Creating Service Accounts Service accounts are created similarly to regular users but with type `api`: 1. Navigate to **Settings > Service Accounts** 2. Enter a name for the service account (e.g., "github-actions", "ci-production") 3. Click **Create** 4. Copy the generated API token (shown only once) Service accounts: - Cannot log in interactively (no password) - Receive an API token on creation - Are tied to a specific tenant - Provide clear audit trails in transaction logs **Via API:** ```bash curl -X POST http://localhost:8080/api/v1/api-users \ -H "Content-Type: application/json" \ -H "Cookie: session=" \ -d '{ "name": "ci-production", "tenant_id": "" }' ``` Response: ```json { "user_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "token": "eyJhbGciOiJIUzI1NiIs..." } ``` ## Authentication Flow ### Login Flow 1. User navigates to Stategraph 2. If not authenticated, redirected to login page 3. User enters email and password 4. POST request to `/api/v1/login/password` 5. Server validates credentials 6. Session token created and stored as HTTP-only cookie 7. User redirected to Stategraph UI ### Login Endpoint ```bash POST /api/v1/login/password Content-Type: application/json { "email": "user@example.com", "password": "your-password" } ``` Response: ```json { "session_token": "eyJhbGciOiJIUzI1NiIs...", "success": true } ``` The `session_token` can be used for Bearer authentication. ### Logout ```bash GET /api/v1/logout ``` Clears the session cookie and logs the user out. ## API Endpoints ### Authentication Endpoints #### Get Login Options ```bash GET /api/v1/login/options ``` Returns available authentication methods (local, OAuth, etc.). Response: ```json { "options": [] } ``` When OAuth is configured, the `options` array contains available providers. #### Login with Password ```bash POST /api/v1/login/password Content-Type: application/json { "email": "user@example.com", "password": "your-password" } ``` #### Check Setup Status ```bash GET /api/v1/setup/status ``` Returns whether initial setup is needed. Response: ```json { "needs_setup": false } ``` #### Create First Admin ```bash POST /api/v1/setup/admin Content-Type: application/json { "email": "admin@example.com", "password": "secure-password", "name": "Admin User", "organization": "My Org" } ``` Only works when no users exist in the system. #### Logout ```bash GET /api/v1/logout ``` ### User Management Endpoints (Admin Only) #### List Users ```bash GET /api/v1/users?type=user&limit=25&offset=0 ``` Query parameters: - `type`: Filter by user type (`user` or `api`) - `search`: Search by name or email - `limit`: Maximum results (default: 25) - `offset`: Number of results to skip (default: 0) This endpoint uses offset-based pagination. To fetch the next page, increment `offset` by `limit`. The response includes `total_count` and `has_more` so you can tell when more pages remain. Response: ```json { "users": [ { "id": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "name": "John Doe", "email": "john@example.com", "is_admin": false, "type": "user", "created_at": "2024-01-15T10:30:00Z" } ], "total_count": 10, "limit": 25, "has_more": false, "next_cursor": null } ``` #### Get User Details ```bash GET /api/v1/users/detail?user_id= ``` Response: ```json { "id": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "name": "John Doe", "email": "john@example.com", "is_admin": false, "type": "user", "created_at": "2024-01-15T10:30:00Z" } ``` **Note**: The response also includes `auth_origin` (how the user authenticates) and `avatar_url` when available. #### Create User ```bash POST /api/v1/users Content-Type: application/json { "name": "Jane Doe", "email": "jane@example.com", "password": "secure-password", "is_admin": false } ``` Response: ```json { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "name": "Jane Doe", "email": "jane@example.com", "is_admin": false } ``` #### Update User ```bash PUT /api/v1/users/update?user_id= Content-Type: application/json { "name": "Jane Smith", "email": "jane.smith@example.com" } ``` #### Delete User ```bash DELETE /api/v1/users/delete?user_id= ``` Response: `204 No Content` #### Change User Password ```bash POST /api/v1/users/change-password?user_id= Content-Type: application/json { "new_password": "new-secure-password", "current_password": "old-password" # Only required when changing own password } ``` #### Toggle Admin Status ```bash POST /api/v1/users/toggle-admin?user_id= Content-Type: application/json { "is_admin": true } ``` ### Service Account Endpoints #### Create Service Account ```bash POST /api/v1/api-users Content-Type: application/json { "name": "ci-production", "tenant_id": "" } ``` Response: ```json { "user_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "token": "eyJhbGciOiJIUzI1NiIs..." } ``` **Important**: Save the token immediately - it's only shown once. ## Creating API Keys Once authenticated, you can create API keys for CLI and Terraform access: 1. Log in to Stategraph 2. Navigate to **Settings > API Keys** 3. Click **Create API Key** 4. Optionally provide a name/description 5. Copy the generated token Use the API key with the CLI: ```bash export STATEGRAPH_API_KEY="" stategraph states list --tenant ``` You can also mint API keys directly from the CLI with `stategraph user access-tokens create`, which issues **capability-scoped** tokens (least-privilege grants such as commit-only on specific states) using an existing API key. Service accounts can be issued the same capability-scoped tokens via the same model. See [Access Tokens & Capabilities](/docs/authentication/access-tokens). ## Security Considerations ### Password Requirements - **Minimum length**: 8 characters - **Maximum length**: 128 characters - No complexity requirements enforced (consider using strong passwords) ### Session Security - Sessions are stored as HTTP-only cookies (not accessible via JavaScript) - Session tokens are JWT tokens signed with a secret key - **Use HTTPS in production** to protect credentials and session tokens in transit ### Best Practices 1. **Use HTTPS**: Always deploy with HTTPS in production 2. **Strong passwords**: Enforce strong password policies at the organizational level 3. **Regular audits**: Review user list and admin access regularly 4. **Service accounts**: Use service accounts for CI/CD instead of personal credentials 5. **Principle of least privilege**: Only grant admin access when necessary 6. **Monitor access**: Review transaction logs for unusual activity ### Admin Responsibilities Admins can: - Create and delete any user - Change any user's password without verification - Grant and revoke admin privileges - View all states and tenants - Cannot accidentally remove the last admin ## Transitioning to OAuth When you're ready to switch to OAuth: 1. Configure OAuth environment variables ([Google OAuth](/docs/authentication/google-oauth) or [OIDC](/docs/authentication/oidc)) 2. Restart Stategraph with OAuth configuration 3. Existing local users remain in the database 4. New OAuth users are created on first login 5. Existing API keys continue to work **Note**: Once OAuth is enabled, the local authentication login page is replaced with OAuth login. Existing local users cannot log in via password unless OAuth is disabled. ## Next Steps - [Configure Google OAuth](/docs/authentication/google-oauth) for SSO - [Configure OIDC](/docs/authentication/oidc) for other providers - [Set up Velocity](/docs/velocity/setup) - [Set up CI/CD with service accounts](/docs/authentication#cicd-setup) --- # Google OAuth Setup Source: https://stategraph.com/docs/authentication/google-oauth Configure Google OAuth for Stategraph authentication. Complete setup guide for Google Workspace integration and user SSO. Configure Google OAuth to authenticate Stategraph users with their Google Workspace accounts. ## Prerequisites - Google Cloud account - Google Cloud project - Admin access to configure OAuth consent screen ## Step 1: Create OAuth Client ### Open Google Cloud Console 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. Select your project (or create a new one) ### Configure OAuth Consent Screen 1. Navigate to **APIs & Services** > **OAuth consent screen** 2. Select user type: - **Internal**: Only users in your Google Workspace organization - **External**: Any Google user (requires app verification for production) 3. Fill in the required fields: - **App name**: Stategraph - **User support email**: Your email - **Developer contact**: Your email 4. Click **Save and Continue** 5. Add scopes (optional, email and profile are included by default) 6. Complete the setup ### Create OAuth Client ID 1. Navigate to **APIs & Services** > **Credentials** 2. Click **Create Credentials** > **OAuth client ID** 3. Select **Web application** 4. Configure: - **Name**: Stategraph - **Authorized redirect URIs**: Add your callback URL ``` https://stategraph.example.com/oauth2/google/callback ``` For local development: ``` http://localhost:8080/oauth2/google/callback ``` 5. Click **Create** 6. Copy the **Client ID** and **Client Secret** ## Step 2: Configure Stategraph ### Required Environment Variables ```bash # Enable Google OAuth STATEGRAPH_OAUTH_TYPE=google # From Google Cloud Console STATEGRAPH_OAUTH_CLIENT_ID=123456789-abc123.apps.googleusercontent.com STATEGRAPH_OAUTH_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxx # Your public URL STATEGRAPH_UI_BASE=https://stategraph.example.com ``` ### Optional Environment Variables ```bash # Restrict to specific domain STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com # Callback URL base (set this for any non-local deployment) STATEGRAPH_OAUTH_REDIRECT_BASE=https://stategraph.example.com ``` > **Callback URL**: The callback URL your Google OAuth client needs is `{STATEGRAPH_OAUTH_REDIRECT_BASE}/oauth2/google/callback`. If `STATEGRAPH_OAUTH_REDIRECT_BASE` is not set, the base defaults to `http://localhost:` (local development only) — so set it for any non-local deployment. This URL is logged at startup for easy copy-paste. ### Docker Compose Example ```yaml services: server: image: : # image URL provided when you get access: https://stategraph.com/contact environment: DB_HOST: "db" DB_PORT: "5432" DB_USER: "stategraph" DB_PASS: "stategraph" DB_NAME: "stategraph" STATEGRAPH_UI_BASE: "https://stategraph.example.com" STATEGRAPH_OAUTH_TYPE: "google" STATEGRAPH_OAUTH_CLIENT_ID: "123456789-abc123.apps.googleusercontent.com" STATEGRAPH_OAUTH_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET}" STATEGRAPH_OAUTH_EMAIL_DOMAIN: "yourcompany.com" STATEGRAPH_OAUTH_REDIRECT_BASE: "https://stategraph.example.com" ``` ## Step 3: Verify Configuration ### Test Authentication 1. Open Stategraph in your browser 2. You should see a login button 3. Click to authenticate with Google 4. After authentication, you should see the Stategraph UI ### Check Logs If authentication fails, check the server logs: ```bash docker compose logs server ``` Look for oauth2-proxy messages indicating the issue. ## Advanced Configuration ### Google Groups Restriction Restrict access to members of specific Google Groups: ```bash # Google Group email STATEGRAPH_OAUTH_GOOGLE_GROUP=stategraph-users@yourcompany.com # Admin email for group lookup STATEGRAPH_OAUTH_GOOGLE_ADMIN_EMAIL=admin@yourcompany.com # Service account JSON for Google Admin API STATEGRAPH_OAUTH_GOOGLE_SERVICE_ACCOUNT_JSON='{...}' ``` #### Setup Service Account 1. Create a service account in Google Cloud Console 2. Enable domain-wide delegation 3. Grant the service account these Admin SDK scopes: - `https://www.googleapis.com/auth/admin.directory.group.readonly` 4. Download the JSON key file 5. Set the JSON content as `STATEGRAPH_OAUTH_GOOGLE_SERVICE_ACCOUNT_JSON` #### Configure Domain-Wide Delegation 1. Go to [Google Admin Console](https://admin.google.com) 2. Navigate to **Security** > **API Controls** > **Domain-wide Delegation** 3. Add the service account client ID 4. Add scope: `https://www.googleapis.com/auth/admin.directory.group.readonly` ### Multiple Domains To allow multiple domains, use a comma-separated list or allow all: ```bash # Allow all domains STATEGRAPH_OAUTH_EMAIL_DOMAIN=* # Note: Multiple specific domains require additional configuration ``` ## Troubleshooting ### "redirect_uri_mismatch" The redirect URI in your Google OAuth configuration doesn't match. **Solution**: 1. Go to Google Cloud Console > APIs & Services > Credentials 2. Edit your OAuth client 3. Add the exact redirect URI: `https://stategraph.example.com/oauth2/google/callback` ### "access_denied" Google denied access to the application. **Causes**: - User's email domain not in allowed list - User not in required Google Group - OAuth consent screen not approved for external users **Solutions**: - Check `STATEGRAPH_OAUTH_EMAIL_DOMAIN` setting - Verify Google Group membership - For external users, complete app verification ### "invalid_client" The client ID or secret is incorrect. **Solution**: - Verify the client ID and secret from Google Cloud Console - Check for extra whitespace or newlines ### Login redirects but nothing happens The session cookie may not be set correctly. **Causes**: - `STATEGRAPH_UI_BASE` doesn't match the URL you're accessing - HTTPS/HTTP mismatch - Proxy stripping cookies **Solutions**: - Verify `STATEGRAPH_UI_BASE` matches your URL exactly - Ensure consistent protocol (https) - Check proxy configuration ### Google Groups not working Service account configuration issues. **Checklist**: - Service account created - Domain-wide delegation enabled - Admin email is a super admin - JSON key is valid - Scopes are delegated in Admin Console ## Complete Example ### Environment File (.env) ```bash # Database DB_HOST=db DB_PORT=5432 DB_USER=stategraph DB_PASS=your-secure-password DB_NAME=stategraph # Stategraph STATEGRAPH_UI_BASE=https://stategraph.example.com # Google OAuth STATEGRAPH_OAUTH_TYPE=google STATEGRAPH_OAUTH_CLIENT_ID=123456789-abc123.apps.googleusercontent.com STATEGRAPH_OAUTH_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxx STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com STATEGRAPH_OAUTH_REDIRECT_BASE=https://stategraph.example.com # Optional: Google Groups # STATEGRAPH_OAUTH_GOOGLE_GROUP=stategraph-users@yourcompany.com # STATEGRAPH_OAUTH_GOOGLE_ADMIN_EMAIL=admin@yourcompany.com # STATEGRAPH_OAUTH_GOOGLE_SERVICE_ACCOUNT_JSON='{...}' ``` ### Docker Compose ```yaml services: db: image: postgres:17-alpine environment: POSTGRES_PASSWORD: "your-secure-password" POSTGRES_USER: "stategraph" POSTGRES_DB: "stategraph" volumes: - db:/var/lib/postgresql/data/ networks: - stategraph server: image: : # image URL provided when you get access: https://stategraph.com/contact env_file: - .env ports: - "8080:8080" depends_on: db: condition: service_healthy networks: - stategraph networks: stategraph: volumes: db: ``` ## Next Steps - [OIDC Configuration](/docs/authentication/oidc) for other providers - [Environment Variables Reference](/docs/reference/environment-variables) - [API Reference](/docs/reference/api) --- # OIDC Configuration Source: https://stategraph.com/docs/authentication/oidc Configure generic OIDC providers for Stategraph authentication. Compatible with Okta, Auth0, Keycloak, and any OpenID Connect provider. Configure Stategraph to authenticate users via any OpenID Connect (OIDC) compatible identity provider. ## Supported Providers Any OIDC-compliant provider works, including: - Okta - Auth0 - Azure Active Directory (Entra ID) - Keycloak - OneLogin - PingIdentity - AWS Cognito - GitLab ## Basic Configuration ### Required Environment Variables ```bash # Enable OIDC STATEGRAPH_OAUTH_TYPE=oidc # Your OIDC provider's issuer URL STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://your-provider.com # OAuth client credentials STATEGRAPH_OAUTH_CLIENT_ID=your-client-id STATEGRAPH_OAUTH_CLIENT_SECRET=your-client-secret # Your Stategraph URL STATEGRAPH_UI_BASE=https://stategraph.example.com ``` ### Optional Environment Variables ```bash # Restrict to email domain STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com # Callback URL base (if different from UI base) STATEGRAPH_OAUTH_REDIRECT_BASE=https://stategraph.example.com ``` > **Callback URL**: The callback URL to register with your OIDC provider is `{STATEGRAPH_OAUTH_REDIRECT_BASE}/oauth2/oidc/callback`. This URL is logged at startup for easy copy-paste. ## Provider-Specific Setup ### Okta #### Create Application 1. Go to Okta Admin Console 2. Navigate to **Applications** > **Create App Integration** 3. Select **OIDC - OpenID Connect** 4. Select **Web Application** 5. Configure: - **App integration name**: Stategraph - **Grant type**: Authorization Code - **Sign-in redirect URIs**: `https://stategraph.example.com/oauth2/oidc/callback` - **Sign-out redirect URIs**: `https://stategraph.example.com` 6. Save and copy Client ID and Client Secret #### Configuration ```bash STATEGRAPH_OAUTH_TYPE=oidc STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://your-org.okta.com STATEGRAPH_OAUTH_CLIENT_ID=0oaxxxxxxxxxxxxxx STATEGRAPH_OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ### Auth0 #### Create Application 1. Go to Auth0 Dashboard 2. Navigate to **Applications** > **Create Application** 3. Select **Regular Web Applications** 4. In Settings: - **Allowed Callback URLs**: `https://stategraph.example.com/oauth2/oidc/callback` - **Allowed Logout URLs**: `https://stategraph.example.com` 5. Copy Domain, Client ID, and Client Secret #### Configuration ```bash STATEGRAPH_OAUTH_TYPE=oidc STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://your-tenant.auth0.com STATEGRAPH_OAUTH_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx STATEGRAPH_OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ### Azure AD (Entra ID) #### Register Application 1. Go to Azure Portal > Azure Active Directory 2. Navigate to **App registrations** > **New registration** 3. Configure: - **Name**: Stategraph - **Supported account types**: Accounts in this organizational directory only - **Redirect URI**: Web - `https://stategraph.example.com/oauth2/oidc/callback` 4. After creation: - Copy **Application (client) ID** - Go to **Certificates & secrets** > **New client secret** - Copy the secret value #### Configuration ```bash STATEGRAPH_OAUTH_TYPE=oidc STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0 STATEGRAPH_OAUTH_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx STATEGRAPH_OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Replace `{tenant-id}` with your Azure AD tenant ID. ### Keycloak #### Create Client 1. Go to Keycloak Admin Console 2. Select your realm 3. Navigate to **Clients** > **Create client** 4. Configure: - **Client ID**: stategraph - **Client Protocol**: openid-connect - **Root URL**: `https://stategraph.example.com` 5. After creation: - Set **Valid Redirect URIs**: `https://stategraph.example.com/oauth2/oidc/callback` - Enable **Client authentication** - Copy the client secret from **Credentials** tab #### Configuration ```bash STATEGRAPH_OAUTH_TYPE=oidc STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://keycloak.example.com/realms/your-realm STATEGRAPH_OAUTH_CLIENT_ID=stategraph STATEGRAPH_OAUTH_CLIENT_SECRET=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ``` ### AWS Cognito #### Create User Pool and App Client 1. Go to AWS Cognito Console 2. Create or select a User Pool 3. Navigate to **App clients** > **Create app client** 4. Configure: - **App client name**: stategraph - Enable **Generate client secret** 5. Under **App integration** > **Domain**, set up a Cognito domain or custom domain 6. Configure callback URL: `https://stategraph.example.com/oauth2/oidc/callback` #### Configuration ```bash STATEGRAPH_OAUTH_TYPE=oidc STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://cognito-idp.{region}.amazonaws.com/{user-pool-id} STATEGRAPH_OAUTH_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx STATEGRAPH_OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ### GitLab #### Create Application 1. Go to GitLab > User Settings > Applications (Or Admin Area > Applications for instance-wide) 2. Configure: - **Name**: Stategraph - **Redirect URI**: `https://stategraph.example.com/oauth2/oidc/callback` - **Scopes**: `openid`, `email`, `profile` 3. Save and copy Application ID and Secret #### Configuration ```bash STATEGRAPH_OAUTH_TYPE=oidc STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://gitlab.com # Or your self-hosted GitLab URL STATEGRAPH_OAUTH_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx STATEGRAPH_OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ## Finding the Issuer URL The issuer URL is the base URL of your OIDC provider. Most providers publish their configuration at: ``` {issuer_url}/.well-known/openid-configuration ``` For example: - Okta: `https://your-org.okta.com/.well-known/openid-configuration` - Auth0: `https://your-tenant.auth0.com/.well-known/openid-configuration` - Azure AD: `https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration` This endpoint returns the OIDC discovery document with authorization endpoints. ## Docker Compose Example ```yaml services: server: image: : # image URL provided when you get access: https://stategraph.com/contact environment: DB_HOST: "db" DB_PORT: "5432" DB_USER: "stategraph" DB_PASS: "stategraph" DB_NAME: "stategraph" STATEGRAPH_UI_BASE: "https://stategraph.example.com" STATEGRAPH_OAUTH_TYPE: "oidc" STATEGRAPH_OAUTH_OIDC_ISSUER_URL: "https://your-provider.com" STATEGRAPH_OAUTH_CLIENT_ID: "your-client-id" STATEGRAPH_OAUTH_CLIENT_SECRET: "${OIDC_CLIENT_SECRET}" STATEGRAPH_OAUTH_EMAIL_DOMAIN: "yourcompany.com" STATEGRAPH_OAUTH_REDIRECT_BASE: "https://stategraph.example.com" ports: - "8080:8080" ``` ## Troubleshooting ### "Invalid issuer" The issuer URL is incorrect or unreachable. **Solutions**: - Verify the issuer URL is correct - Test with: `curl {issuer_url}/.well-known/openid-configuration` - Check network connectivity from Stategraph to the provider ### "Invalid client" The client ID or secret is incorrect. **Solutions**: - Verify credentials from your provider's dashboard - Check for whitespace or newlines in values - Ensure the client hasn't been deleted or disabled ### "Redirect URI mismatch" The callback URL doesn't match what's configured in the provider. **Solutions**: - The exact URL must match: `https://stategraph.example.com/oauth2/oidc/callback` - Check protocol (http vs https) - Check hostname and port - No trailing slash ### "Access denied" after authentication User was authenticated but denied access. **Solutions**: - Check `STATEGRAPH_OAUTH_EMAIL_DOMAIN` setting - Verify user's email is in the allowed domain - Check provider's group/role assignments if applicable ### Session not persisting Cookies not being set correctly. **Solutions**: - Verify `STATEGRAPH_UI_BASE` matches the access URL exactly - Use HTTPS in production - Check for proxy interference ## Verifying Configuration ### Test OIDC Discovery ```bash curl https://your-provider.com/.well-known/openid-configuration | jq ``` Should return a JSON document with endpoints: ```json { "issuer": "https://your-provider.com", "authorization_endpoint": "https://your-provider.com/authorize", "token_endpoint": "https://your-provider.com/oauth/token", "userinfo_endpoint": "https://your-provider.com/userinfo", "jwks_uri": "https://your-provider.com/.well-known/jwks.json" } ``` ### Check Stategraph Logs ```bash docker compose logs server | grep -i oauth ``` Look for errors or successful authentication messages. ## Security Best Practices 1. **Use HTTPS** for both Stategraph and callback URLs 2. **Restrict email domains** to your organization 3. **Use secrets management** for client secrets 4. **Enable MFA** at the identity provider level 5. **Regular audits** of authorized users 6. **Rotate client secrets** periodically ## Next Steps - [Google OAuth Setup](/docs/authentication/google-oauth) for Google-specific features - [Environment Variables Reference](/docs/reference/environment-variables) - [API Reference](/docs/reference/api) --- # Access Tokens & Capabilities Source: https://stategraph.com/docs/authentication/access-tokens Create capability-scoped Stategraph access tokens from the CLI to grant least-privilege admin, commit, preview, and user-management rights. Access tokens authenticate the CLI, Terraform, and API requests as a Bearer token. Every token carries a set of **capabilities** that bound what it can do. The `stategraph user access-tokens` commands let you mint, list, and revoke capability-scoped tokens directly from the CLI using an existing API key — no browser session required. This is the recommended way to issue least-privilege credentials: a token can be limited to, for example, committing changes to a single state while being unable to manage users or create further tokens. ## Capability Model A token's capabilities are a subset of the capabilities held by the session that created it. A token can never exceed its creator. | Capability | Grants | Scope | |------------|--------|-------| | `admin` | Full access to everything. | Global | | `commit` | Apply (commit) changes through transactions. | Per-tenant and per-state | | `preview` | Plan (preview) changes through transactions. | Per-tenant and per-state | | `users-manage` | Create, update, and delete users. | Per-tenant | | `sudo` | Act on behalf of specific users. | Per-user | | `access-token-create` | Issue new access tokens. | Global | | `access-token-refresh` | Refresh existing access tokens. | Global | When no capability flags are given, the token **inherits the creating session's capabilities** — the least-surprising default for "give me a token that can do what I can do." ## stategraph user access-tokens create Create an access token for the current user. ```bash stategraph user access-tokens create --name [capability flags] ``` The `--name` flag is required. The capability flags below select and scope what the token may do: | Flag | Description | |------|-------------| | `--admin` | Grant the admin capability (full access). | | `--commit` | Grant commit; unrestricted unless scoped with `--commit-tenant` or `--commit-state`. | | `--commit-tenant ` | Restrict commit to this tenant (repeatable). | | `--commit-state ` | Restrict commit to resources in a state that match PATTERN (repeatable). | | `--preview` | Grant preview; unrestricted unless scoped with `--preview-tenant` or `--preview-state`. | | `--preview-tenant ` | Restrict preview to this tenant (repeatable). | | `--preview-state ` | Restrict preview to resources in a state that match PATTERN (repeatable). | | `--users-manage` | Grant users-manage; unrestricted unless scoped with `--users-manage-tenant`. | | `--users-manage-tenant ` | Restrict users-manage to this tenant (repeatable). | | `--sudo-user ` | Grant sudo over this user id (repeatable). | | `--access-token-create` | Grant the access-token-create capability. | | `--access-token-refresh` | Grant the access-token-refresh capability. | | `--capabilities-json ` | Raw capabilities JSON object. Mutually exclusive with the individual capability flags above. | State patterns take the form `STATE_ID=PATTERN`. The pattern matches the directly modified resources in that state: use `*` for the whole state (for example `sid=*`), a prefix such as `sid=module.foo.*`, or a leading `!` to deny. ### Least-privilege example Mint a token that can only commit changes to one specific state: ```bash stategraph user access-tokens create \ --name ci-prod-apply \ --commit \ --commit-state 'a1b2c3d4-5678-90ab-cdef-1234567890ab=*' ``` The token value is printed once in the response and cannot be retrieved again — store it securely. ``` field value ----- ------------------------------------ token eyJhbGciOiJIUzI1NiIs... ``` For scripting, use `--format json`: ```json { "token": "eyJhbGciOiJIUzI1NiIs..." } ``` ## stategraph user access-tokens list List the access tokens owned by the current user. ```bash stategraph user access-tokens list ``` By default, output is a table with the columns `id`, `name`, `created_at`, `expiration`, `owner_name`, and `owner_type`. The `expiration` column is blank for tokens that never expire. ``` id name created_at expiration owner_name owner_type ------------------------------------ ------------- -------------------- ---------- ---------------- ---------- f02791c8-aa63-4cdf-acae-a7c968d8a831 ci-prod-apply 2026-06-04T10:30:00Z jane@example.com user ``` For scripting, use `--format json`. Each token object also includes `owner_id`: ```json { "tokens": [ { "id": "f02791c8-aa63-4cdf-acae-a7c968d8a831", "name": "ci-prod-apply", "created_at": "2026-06-04T10:30:00Z", "owner_id": "550e8400-e29b-41d4-a716-446655440000", "owner_name": "jane@example.com", "owner_type": "user" } ] } ``` ## stategraph user access-tokens delete Delete (revoke) an access token by id. The `--token-id` flag is required. ```bash stategraph user access-tokens delete --token-id ``` ``` field value ------- ------------------------------------ id f02791c8-aa63-4cdf-acae-a7c968d8a831 revoked true ``` ## Inspecting your capabilities Run [`stategraph user whoami`](/docs/cli/user) to confirm the current identity and the exact capabilities a token or session holds. The `capabilities` object in its output mirrors the model above, so you can verify a least-privilege token is scoped the way you intended. ## Default capabilities for new users When a new user is created, they are granted a **system-wide default capability**. Out of the box this default is lenient, which is convenient when ACLs are not required. In an ACL-reliant setup you typically tighten it to a minimal baseline and grant additional rights per user or per group (for example, based on OAuth group membership). Two admin commands manage this default: ```bash # Show the capabilities granted to newly created users stategraph capabilities default show # Tighten the default so new users start with plan-only rights stategraph capabilities default set --plan ``` `stategraph capabilities default set` accepts the same style of capability flags as token creation, using `--plan` and `--apply` for plan/apply rights — each restrictable with `--plan-tenant` / `--plan-state` / `--plan-subgraph` and the `--apply-*` equivalents — plus `--admin`, `--users-manage`, `--access-token-create`, `--access-token-refresh`, and `--sudo-user`. Pass `--capabilities-json` to set the raw capabilities object directly instead of using the individual flags. Changing the default applies to users created afterward; it does not retroactively change the capabilities of existing users. ## API The CLI is a thin wrapper over three endpoints. See the [API Reference](/docs/reference/api) for full details. | Method | Endpoint | Purpose | |--------|----------|---------| | `POST` | `/api/v1/user/access-tokens` | Create a token. The request body accepts `name` and an optional `capabilities` object. | | `GET` | `/api/v1/user/access-tokens` | List the current user's tokens. | | `POST` | `/api/v1/user/access-tokens/revoke?token_id=` | Revoke a token. | When `capabilities` is omitted from the create request, the token inherits the creator's full session capabilities; any requested capabilities are capped at the creator's, so a token can never exceed the identity that created it. ## Next Steps - [User Commands](/docs/cli/user) - The `stategraph user` CLI group - [Authentication](/docs/authentication) - API keys and service accounts - [Local Authentication](/docs/authentication/local-authentication) - Email/password user management - [API Reference](/docs/reference/api) - REST API endpoints --- # Introduction Source: https://stategraph.com/docs/cli Command-line interface for Stategraph. Manage states, run queries, execute gap analysis, and automate infrastructure operations from the terminal. The Stategraph CLI (`stategraph`) provides command-line access to Stategraph for automation, scripting, and programmatic workflows. ## Installation For day-to-day use, install the CLI directly using the install script, Homebrew, APT, or a binary download. Docker is also available as a distribution mechanism, but running the CLI inside a container requires extra setup (volume mounts, environment variables) — most users should install the binary directly. See the [releases page](https://github.com/stategraph/releases/releases) for available versions. ### Install Script (macOS & Linux) Install the latest version with a single command: ```bash curl -sSL https://get.stategraph.com/install.sh | sh ``` ### Homebrew (macOS) ```bash brew tap stategraph/stategraph brew install stategraph ``` To upgrade: ```bash brew upgrade stategraph ``` ### APT (Debian & Ubuntu) Add the Stategraph repository and install: ```bash # Add the signing key curl -fsSL https://stategraph.github.io/releases/apt/KEY.gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/stategraph.gpg # Add the repository echo "deb [signed-by=/etc/apt/keyrings/stategraph.gpg] https://stategraph.github.io/releases/apt stable main" \ | sudo tee /etc/apt/sources.list.d/stategraph.list # Install sudo apt update sudo apt install stategraph ``` To upgrade: ```bash sudo apt update sudo apt upgrade stategraph ``` Supported architectures: `amd64` and `arm64`. ### Binary Installation Download the appropriate binary for your platform from the [releases page](https://github.com/stategraph/releases/releases): | Platform | File | |----------|------| | macOS (Apple Silicon) | `stategraph-macos-arm64.tar.gz` | | macOS (Intel) | `stategraph-macos-amd64.tar.gz` | | Linux (amd64) | `stategraph-linux-amd64.tar.gz` | | Linux (arm64) | `stategraph-linux-arm64.tar.gz` | After downloading: ```bash tar xzf stategraph-.tar.gz sudo mv stategraph /usr/local/bin/ ``` Verify the installation: ```bash stategraph --version stategraph --help ``` `stategraph --version` prints the CLI version. `stategraph-server --version` does the same for the server binary. Running `stategraph` with no arguments shows a curated getting-started screen; `stategraph --help` shows the full command list with aliases. ### Docker The CLI is available as a Docker image. Check the [releases page](https://github.com/stategraph/releases/releases) for available version tags. ```bash docker pull ghcr.io/stategraph/stategraph: ``` #### Extract the binary from Docker If you don't want to use Homebrew or curl, you can pull the binary out of the Docker image and use it natively: ```bash docker create --name sg-tmp ghcr.io/stategraph/stategraph: docker cp sg-tmp:/usr/local/bin/stategraph ./stategraph docker rm sg-tmp sudo mv stategraph /usr/local/bin/ ``` #### Running the CLI via Docker For CI/CD pipelines or containerized environments, you can run the CLI directly inside Docker. This requires mounting your working directory and forwarding environment variables: ```bash docker run --rm \ -e STATEGRAPH_API_BASE \ -e STATEGRAPH_API_KEY \ -v $(pwd):/workspace \ -w /workspace \ ghcr.io/stategraph/stategraph: \ states import --tenant --name "networking" terraform.tfstate ``` Or create an alias for convenience: ```bash alias stategraph='docker run --rm -e STATEGRAPH_API_BASE -e STATEGRAPH_API_KEY -v $(pwd):/workspace -w /workspace ghcr.io/stategraph/stategraph:' ``` ## Configuration ### API Base URL Set the Stategraph server URL: ```bash export STATEGRAPH_API_BASE=https://stategraph.example.com ``` Or pass it with each command (the flag goes after the subcommand): ```bash stategraph user whoami --api-base https://stategraph.example.com ``` ### Authentication The CLI authenticates using API keys. Create an API key via the UI or API, then set it as an environment variable: ```bash export STATEGRAPH_API_KEY="" stategraph user tenants list ``` API keys work as Bearer tokens. See [Authentication](/docs/authentication) for details on creating API keys. ## Command Structure ``` stategraph [options] ``` ### Available Commands Every command group has a dedicated reference page: | Command | Description | |---------|-------------| | [`stategraph info`](/docs/cli/utilities) | Show current user, tenants, and server version in one call | | [`stategraph user`](/docs/cli/user) | User identity and tenant membership | | [`stategraph import`](/docs/cli/import) | Onboard a Terraform state and its HCL in one step | | [`stategraph states`](/docs/cli/states) | Manage states: create, list, import, export, query, blast radius | | [`stategraph tf`](/docs/cli/tf) | Plan and apply Terraform changes, including multi-state `mtx` | | [`stategraph refactor`](/docs/cli/refactor) | Detect and apply HCL refactors (renames, module moves) | | [`stategraph tx`](/docs/cli/transactions) | Transaction management: create, list, costs, abort, logs | | [`stategraph sql`](/docs/cli/sql) | SQL query interface and schema discovery | | [`stategraph tenant`](/docs/cli/tenant) | Tenant operations: gap analysis for unmanaged resources | | [`stategraph cost`](/docs/cli/cost) | Cost intelligence: state/tenant cost, attribution, history, and FOCUS billing sources | | [`stategraph hcl`](/docs/cli/hcl) | HCL tools: list addresses, evaluate expressions, convert to/from JSON | | [`stategraph diagnostics`](/docs/cli/diagnostics) | Trace how your HCL evaluates locally (no API calls); great for onboarding | | [`stategraph config`](/docs/cli/config) | Manage `stategraph.json` (attach data-file globs to addresses) | | [`stategraph version`](/docs/cli/utilities) | Print client and server versions | | [`stategraph test`](/docs/cli/hcl) | Debug helpers (`test tf load` parses and prints a module's HCL) | ### Aliases Six top-level aliases shorten the most common commands: | Alias | Expands to | |-------|------------| | `stategraph plan` | [`stategraph tf plan`](/docs/cli/tf) | | `stategraph apply` | [`stategraph tf apply`](/docs/cli/tf) | | `stategraph query` | [`stategraph sql query`](/docs/cli/sql) | | `stategraph whoami` | [`stategraph user whoami`](/docs/cli/user) | | `stategraph gaps` | [`stategraph tenant gaps analyze`](/docs/cli/tenant) | | `stategraph blast-radius` | [`stategraph states instances blast-radius`](/docs/cli/states) | ## Quick Examples ### Orient yourself (great for new users and AI agents) ```bash stategraph info ``` Fetches the current user, server version, and tenant list in a single call. Prints suggested next steps when run in `table` or `simple` format. ### Check current user ```bash stategraph user whoami ``` ### List your tenants ```bash stategraph user tenants list ``` ### List states in a tenant ```bash stategraph states list --tenant ``` ### Import a Terraform state file Run this from the root of your Terraform module. Pass the same `--var-file` / `--var` flags you use with `terraform plan`: ```bash stategraph import tf \ --tenant \ --name "networking" \ terraform.tfstate ``` To replace an existing state with new HCL and state data, pass `--overwrite`: ```bash stategraph import tf --overwrite \ --tenant \ --name "networking" \ terraform.tfstate ``` See [Velocity setup](/docs/velocity/setup) for the full import workflow. ### Run an SQL query ```bash stategraph sql query "SELECT type, count(*) FROM resources GROUP BY type" ``` ### Get blast radius for a resource ```bash stategraph states instances blast-radius --state "aws_vpc.main" ``` ### Run gap analysis ```bash stategraph tenant gaps analyze --tenant --provider aws ``` ## Common Options These options are available on most subcommands (passed after the subcommand, not before): | Option | Environment Variable | Description | |--------|---------------------|-------------| | `--api-base` | `STATEGRAPH_API_BASE` | Base URL for API calls | | - | `STATEGRAPH_API_KEY` | API key for authentication | | `--tenant` | `STATEGRAPH_TENANT_ID` | Tenant ID (required by most commands) | | `--workspace` | `STATEGRAPH_WORKSPACE` | Workspace name (default: `default`) | | `-y` | `STATEGRAPH_NON_INTERACTIVE` | Auto-confirm interactive prompts | | `--silent` | `STATEGRAPH_SILENT` | Suppress prompt output (use with `-y`) | | `--loggers` | `STATEGRAPH_LOGGERS` | Logging configuration | | `-v`, `--verbosity` | `STATEGRAPH_LOG_LEVEL` | Increase log verbosity (e.g. `--verbosity=debug`) | | `--format` | - | Output format: `json`, `table`, or `simple` | | `--version` | - | Print CLI version and exit | ## Managing attached data files When your HCL reads external files (via `file()`, `templatefile()`, and similar), Stategraph needs to know which files to include in a change. `stategraph config files` records these as globs in `stategraph.json`, keyed by workspace and an address glob: ```bash # Attach file globs to an address (takes the union with any existing globs) stategraph config files attach --workspace default --address 'module.app.*' './templates/*.tpl' # Remove specific globs (omit the globs to remove the whole address entry) stategraph config files remove --workspace default --address 'module.app.*' './templates/*.tpl' ``` The address glob uses the same grammar as `--force` / `--force-files`. See [Config Commands](/docs/cli/config) for the full reference. ## Output Format Query and listing commands accept `--format` to choose between three output modes: | Format | Use case | |--------|----------| | `json` | Programmatic consumption, piping to `jq`, scripting | | `table` | Human-readable aligned columns in the terminal | | `simple` | Tab-separated values, easy to consume from shell scripts | ```bash # JSON for scripting stategraph states list --tenant --format json | jq '.results[].name' # Table for humans stategraph states list --tenant --format table # Simple tab-separated for shell pipelines stategraph user tenants list --format simple | awk '{print $1}' ``` `tx` subcommands are the exception: they always emit JSON and do not accept `--format` (passing it returns `unknown option --format`). ## Exit Codes | Code | Meaning | |------|---------| | 0 | Success | | 1 | Runtime error — authentication, connection, or a failed command | | 2 | Query error (SQL/MQL bad request) | | 124 | Command-line parsing error (missing or invalid options) | ## Documentation | Topic | Description | |-------|-------------| | [User](/docs/cli/user) | User identity and tenant membership | | [States](/docs/cli/states) | State management commands | | [Transactions](/docs/cli/transactions) | Transaction commands, including plan-time costs | | [SQL](/docs/cli/sql) | Query commands | | [Tenant](/docs/cli/tenant) | Tenant and gap analysis | | [Terraform](/docs/cli/tf) | `tf plan`, `tf apply`, `tf show`, and multi-state `tf mtx` | | [Refactor](/docs/cli/refactor) | HCL refactor detection and apply | | [Import](/docs/cli/import) | One-step state and HCL onboarding | | [HCL](/docs/cli/hcl) | Address listing, expression evaluation, HCL/JSON conversion | | [Diagnostics](/docs/cli/diagnostics) | Locally trace how your HCL evaluates and share it with us for onboarding | | [Cost](/docs/cli/cost) | Cost intelligence: state/tenant cost, attribution, history, and FOCUS billing sources | | [Config](/docs/cli/config) | `stategraph.json` attached-files management | | [Utilities](/docs/cli/utilities) | `info` and `version` | | [Velocity setup](/docs/velocity/setup) | Importing state and running `stategraph tf` plan/apply | ## Next Steps - [User Commands](/docs/cli/user) - Identity and tenant membership - [States Commands](/docs/cli/states) - Manage Terraform states - [Terraform Commands](/docs/cli/tf) - Plan and apply through Stategraph - [SQL Commands](/docs/cli/sql) - Query your infrastructure - [API Reference](/docs/reference/api) - REST API documentation --- # User Commands Source: https://stategraph.com/docs/cli/user CLI commands for managing user identity and tenant membership. View and manage your Stategraph user profile and organizational access. The `stategraph user` command group provides user identity, access token, and tenant membership operations. ## Commands | Command | Description | |---------|-------------| | `stategraph user whoami` | Display current user information | | `stategraph user tenants list` | List tenants you belong to | | `stategraph user access-tokens create` | Create a capability-scoped access token | | `stategraph user access-tokens list` | List your access tokens | | `stategraph user access-tokens delete` | Delete (revoke) an access token | ## stategraph user whoami Display information about the currently authenticated user. `stategraph whoami` is a top-level alias for the same command. ```bash stategraph user whoami ``` ### Example ```bash stategraph user whoami ``` By default, output is a `field | value` table: ``` field value -------------------- ------------------------------------ id 550e8400-e29b-41d4-a716-446655440000 name Jane Smith email jane@example.com type User avatar_url auth_origin local capabilities admin no access-token-create no access-token-refresh no preview yes tenants all states all commit yes tenants all states all sudo no users-manage no ``` For scripting, use `--format json`: ```bash stategraph user whoami --format json ``` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Jane Smith", "email": "jane@example.com", "type": "user", "capabilities": { "preview": {}, "commit": {} } } ``` The response includes a `capabilities` object describing what the identity is allowed to do (`admin`, `commit`, `preview`, `sudo`, `users-manage`, `access-token-create`, `access-token-refresh`). Admin and role status are expressed through `capabilities.admin` rather than a boolean `is_admin` flag. See [Access Tokens & Capabilities](/docs/authentication/access-tokens) for the full model. This command is useful for: - Verifying your authentication is working - Checking which account you're logged in as - Getting your user ID for API calls ## stategraph user tenants list List all tenants that the current user is a member of. ```bash stategraph user tenants list ``` ### Example ```bash stategraph user tenants list ``` By default, output is an `id | name` table: ``` id name ------------------------------------ ----------- 550e8400-e29b-41d4-a716-446655440000 production 660e8400-e29b-41d4-a716-446655440001 staging 770e8400-e29b-41d4-a716-446655440002 development ``` For scripting, use `--format simple` (tab-separated, no header) or `--format json`: ```bash stategraph user tenants list --format simple ``` ``` 550e8400-e29b-41d4-a716-446655440000 production 660e8400-e29b-41d4-a716-446655440001 staging 770e8400-e29b-41d4-a716-446655440002 development ``` ## stategraph user access-tokens Create, list, and revoke capability-scoped access tokens for the current user. Each token authenticates the CLI, Terraform, and API as a Bearer token and is bounded by a set of capabilities. See [Access Tokens & Capabilities](/docs/authentication/access-tokens) for the full capability model. ### stategraph user access-tokens create Create an access token. `--name` is required; the capability flags scope what the token may do. With no capability flags, the token inherits the creating session's capabilities. ```bash stategraph user access-tokens create --name ci-prod-apply --commit --commit-state 'sid=*' ``` | Flag | Description | |------|-------------| | `--name ` | Name for the token (required). | | `--admin` | Grant admin (full access). | | `--commit` | Grant commit; restrict with `--commit-tenant ` / `--commit-state `. | | `--preview` | Grant preview; restrict with `--preview-tenant ` / `--preview-state `. | | `--users-manage` | Grant users-manage; restrict with `--users-manage-tenant `. | | `--sudo-user ` | Grant sudo over a user (repeatable). | | `--access-token-create` | Grant the access-token-create capability. | | `--access-token-refresh` | Grant the access-token-refresh capability. | | `--capabilities-json ` | Raw capabilities JSON. Mutually exclusive with the individual flags above. | The token value is printed once in the response and cannot be retrieved again: ```json { "token": "eyJhbGciOiJIUzI1NiIs..." } ``` ### stategraph user access-tokens list List the current user's tokens. ```bash stategraph user access-tokens list ``` The default table shows `id`, `name`, `created_at`, `expiration`, `owner_name`, and `owner_type`. Use `--format json` for the full objects (which also include `owner_id`). ### stategraph user access-tokens delete Delete (revoke) a token by id. The `--token-id` flag is required. ```bash stategraph user access-tokens delete --token-id f02791c8-aa63-4cdf-acae-a7c968d8a831 ``` ## Scripting Examples ### Get tenant ID by name ```bash TENANT_ID=$(stategraph user tenants list --format simple | grep "production" | awk '{print $1}') echo "Production tenant: $TENANT_ID" ``` ### Verify authentication in CI ```bash #!/bin/bash if ! stategraph user whoami > /dev/null 2>&1; then echo "Authentication failed. Check STATEGRAPH_API_KEY." exit 1 fi echo "Authenticated successfully" ``` ### List all states across all tenants ```bash stategraph user tenants list --format simple | while read id name; do echo "=== $name ===" stategraph states list --tenant "$id" --format json | jq -r '.results[].name' done ``` ## Next Steps - [States](/docs/cli/states) - Manage Terraform states - [Authentication](/docs/authentication) - Set up API keys - [CLI Overview](/docs/cli) - All CLI commands --- # States Commands Source: https://stategraph.com/docs/cli/states CLI commands for managing Terraform states in Stategraph. List, inspect, and query state files from the command line interface. The `stategraph states` command group manages Terraform states in Stategraph. ## Commands | Command | Description | |---------|-------------| | `stategraph states create` | Create a new state | | `stategraph states delete` | Permanently delete a state | | `stategraph states list` | List states for a tenant | | `stategraph states resolve` | Resolve a state ID by workspace or name | | `stategraph states import` | Import a Terraform state file | | `stategraph states export` | Export a Terraform state file | | `stategraph states summary` | Get state summary statistics | | `stategraph states resources summary` | Get resource type counts | | `stategraph states instances` | Instance operations | | `stategraph states modules list` | List modules in a state | | `stategraph states anonymize` | Anonymize a Terraform state JSON file (local) | ## stategraph states create Create a new empty state. ```bash stategraph states create --tenant --name [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--name` | Yes | Name for the state | | `--group` | No | Group ID to create state in | | `--workspace` | No | Workspace name | | `--no-write-config` | No | Do not write a `stategraph.json` file | | `--silent` | No | Suppress prompt output | By default, `states create` writes a `stategraph.json` in the current directory that binds it to the new state (the CLI uses this to resolve the state on later commands). Commit that file, or pass `--no-write-config` to skip writing it. ### Example ```bash stategraph states create --tenant 550e8400-e29b-41d4-a716-446655440000 --name "networking" ``` Output (JSON): ```json { "id": "...", "name": "networking", "group_id": "...", "workspace": "default", "created_at": "2024-01-15T10:30:00Z" } ``` ## stategraph states delete Permanently delete a state and all of its related data (resources, instances, providers, outputs, raw states, transaction logs, and check entries/results). This **cannot be undone**. By default the command prompts for interactive confirmation before deleting. ```bash stategraph states delete --state ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--state` | No | State ID (UUID) to delete. Auto-discovered from `stategraph.json` if not set. | | `--workspace` | No | Workspace name (default: `"default"`). Used with `stategraph.json` to resolve the state. | | `--auto-approve` | No | Skip the interactive confirmation prompt (useful in scripts and CI). | ### Examples **Delete a state (prompts for confirmation):** ```bash stategraph states delete --state 550e8400-e29b-41d4-a716-446655440000 ``` You are asked to confirm before the state is permanently removed. **Delete without prompting (e.g. in CI):** ```bash stategraph states delete --state 550e8400-e29b-41d4-a716-446655440000 --auto-approve ``` ### Notes - Requires admin privileges. - Deletion is permanent — there is no soft delete or undo. - Returns an error if the state is not found or the user lacks permission. ## stategraph states list List all states for a tenant. ```bash stategraph states list --tenant ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | ### Example ```bash stategraph states list --tenant 550e8400-e29b-41d4-a716-446655440000 ``` Output (JSON): ```json { "results": [ { "created_at": "2024-01-15T10:30:00Z", "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "id": "...", "name": "networking", "workspace": "production" } ] } ``` Soft-deleted states include additional `deleted_at` and `deleted_by` fields. ## stategraph states resolve Resolve a state ID from a workspace or state name. Useful in scripts that need the state ID without hardcoding it. ```bash stategraph states resolve [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--name` | No | Resolve by state name (ignores `stategraph.json` and `--workspace`) | | `--state` | No | State ID or group ID (auto-discovered from `stategraph.json` if not set) | | `--workspace` | No | Workspace name (default: `"default"`) | | `--group` | No | Print the group ID instead of the state ID | ### Examples **Resolve by workspace (using stategraph.json):** ```bash stategraph states resolve --workspace staging ``` **Resolve by name:** ```bash stategraph states resolve --name networking ``` **Get the group ID:** ```bash stategraph states resolve --group ``` ## stategraph states export Export a Terraform state file. ```bash stategraph states export [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--state` | No | State ID (auto-discovered from `stategraph.json` if not set) | | `--workspace` | No | Workspace name (default: `"default"`) | ### Example ```bash stategraph states export --workspace production > terraform.tfstate ``` ## stategraph states import Import an existing Terraform state file. ```bash stategraph states import --tenant --name [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--name` | Yes | Name for the state | | `--group` | No | Group ID to create state in | | `--workspace` | No | Workspace name | | `--tag` | No | Tags (key=value format, repeatable) | | `--tags` | No | Tags as JSON object | ### Example ```bash # Import with tags stategraph states import \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --name "networking" \ --workspace "production" \ --tag environment=prod \ --tag team=platform \ terraform.tfstate ``` ## stategraph states summary Get summary statistics for a state. ```bash stategraph states summary --state ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--state` | Yes | State ID (UUID) | ### Example ```bash stategraph states summary --state 550e8400-e29b-41d4-a716-446655440000 ``` Output (tab-separated): ``` Edges 234 Instances 150 Modules 8 Providers 3 Resources 45 ``` ## stategraph states resources summary Get instance counts by resource type. ```bash stategraph states resources summary --state ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--state` | Yes | State ID (UUID) | ### Example ```bash stategraph states resources summary --state 550e8400-e29b-41d4-a716-446655440000 ``` Output (tab-separated): ``` aws_instance 20 aws_security_group 15 aws_subnet 6 aws_iam_role 12 ``` ## stategraph states instances Instance-related operations. ### stategraph states instances blast-radius Get the blast radius for a specific instance — all resources that depend on it or that it depends on. `stategraph blast-radius` is a top-level alias for the same command. ```bash stategraph states instances blast-radius --state ``` #### Options | Option | Required | Description | |--------|----------|-------------| | `--state` | Yes | State ID (UUID) | #### Example ```bash stategraph states instances blast-radius \ --state 550e8400-e29b-41d4-a716-446655440000 \ "aws_vpc.main" ``` Output (table): ``` resource_address address index distance ---------------------- ---------------------- ----- -------- aws_vpc.main aws_vpc.main 0 aws_instance.web aws_instance.web 1 aws_security_group.web aws_security_group.web 1 aws_subnet.public_a aws_subnet.public_a 1 aws_subnet.public_b aws_subnet.public_b 1 ``` Columns: resource_address, address, distance ### stategraph states instances query Query resource instances using a SQL filter expression. The query is a SQL boolean expression over instance columns (for example, `type = 'aws_instance'` or `module = 'module.app'`), not a full SQL statement and not a `key:value` tag string. ```bash stategraph states instances query --state [-i] ``` #### Options | Option | Required | Description | |--------|----------|-------------| | `--state` | Yes | State ID (UUID) | | `-i` | No | Iterate through all pages | #### Example ```bash stategraph states instances query \ --state 550e8400-e29b-41d4-a716-446655440000 \ "type = 'aws_instance'" ``` ## stategraph states modules list List modules in a state. ```bash stategraph states modules list --state ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--state` | Yes | State ID (UUID) | ### Example ```bash stategraph states modules list --state 550e8400-e29b-41d4-a716-446655440000 ``` Output (tab-separated): ``` module.vpc 10 25 module.eks 8 45 module.rds 4 12 ``` Columns: name, resource_count, instance_count ## stategraph states anonymize Anonymize a Terraform state JSON file locally — useful for sharing a state for debugging or support without exposing real values. It reads a state file and prints an anonymized copy; it does not contact the server. ```bash stategraph states anonymize [--seed ] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `` | Yes | Path to a Terraform state JSON file | | `-s`, `--seed` | No | Seed for deterministic anonymization | ### Example ```bash stategraph states anonymize terraform.tfstate --seed 42 > anonymized.tfstate ``` ## Scripting Examples ### Export all states to JSON ```bash stategraph states list --tenant $TENANT_ID --format json | jq -r '.results[] | .id' | while read state_id; do echo "Exporting $state_id..." stategraph states summary --state "$state_id" done ``` ### Find states with specific resource types ```bash stategraph states list --tenant $TENANT_ID --format json | jq -r '.results[] | .id' | while read state_id; do count=$(stategraph states resources summary --state "$state_id" 2>/dev/null | grep aws_rds_cluster | awk '{print $2}') if [ -n "$count" ] && [ "$count" -gt 0 ]; then echo "State $state_id has $count RDS clusters" fi done ``` ## Next Steps - [Transactions](/docs/cli/transactions) - Manage transactions - [SQL](/docs/cli/sql) - Query infrastructure --- # Transactions Commands Source: https://stategraph.com/docs/cli/transactions CLI commands for managing Stategraph transactions. Track infrastructure changes, view audit trails, and inspect transaction history. The `stategraph tx` command group manages transactions in Stategraph. Transactions track infrastructure changes and provide audit trails. ## Commands | Command | Description | |---------|-------------| | `stategraph tx create` | Create a new transaction | | `stategraph tx create-with-session` | Mint a session token (Terraform HTTP backend password) | | `stategraph tx list` | List transactions for a tenant | | `stategraph tx costs` | Show the cost delta (current vs planned) for a pending transaction | | `stategraph tx abort` | Abort an active transaction | | `stategraph tx logs list` | List logs for a transaction | For commands that take `--tx`, the transaction ID can also be supplied via the `STATEGRAPH_TX_ID` environment variable. ## stategraph tx create Create a new transaction. ```bash stategraph tx create --tenant [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--tag` | No | Tags (key=value format, repeatable) | | `--tags` | No | Tags as JSON object | ### Example ```bash stategraph tx create \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --tag pipeline=github-actions \ --tag commit=abc123 ``` Output (JSON): ```json { "id": "...", "created_at": "2024-01-15T10:30:00Z", "created_by": "7cc3cdab-c634-4b7b-9523-ef7fd7013d87", "state": "open", "params": {}, "tags": { "pipeline": "github-actions", "commit": "abc123" } } ``` The `completed_at` and `completed_by` fields appear only after the transaction is committed or aborted. ## stategraph tx create-with-session Create a transaction and generate an API key for it. Useful for CI/CD pipelines where you want to scope Terraform operations to a single transaction. ```bash stategraph tx create-with-session --tenant [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--tag` | No | Tags (key=value format, repeatable) | | `--tags` | No | Tags as JSON object | ### Example ```bash stategraph tx create-with-session \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --tag pipeline=terraform-ci ``` Output (API key): ``` eyJhbGciOiJIUzI1NiIs... ``` Use this API key for Terraform backend authentication: ```bash export TF_HTTP_PASSWORD=$(stategraph tx create-with-session --tenant $TENANT_ID) terraform apply ``` The returned API key can be used with: - Direct API calls (as a Bearer token) - The CLI (via `STATEGRAPH_API_KEY`) ## stategraph tx list List transactions for a tenant. ```bash stategraph tx list --tenant ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | ### Example ```bash stategraph tx list --tenant 550e8400-e29b-41d4-a716-446655440000 ``` Output (JSON): ```json { "results": [ { "id": "...", "created_at": "2024-01-15T10:30:00Z", "created_by": "7cc3cdab-c634-4b7b-9523-ef7fd7013d87", "completed_at": "2024-01-15T10:30:05Z", "completed_by": "7cc3cdab-c634-4b7b-9523-ef7fd7013d87", "state": "committed", "params": {}, "tags": { "pipeline": "github-actions" } } ] } ``` ## stategraph tx costs Show the cost delta (current vs planned) for a pending transaction. Only transactions opened by `stategraph tf plan` or `stategraph tf mtx` have a cost preview; a transaction created by hand has none. ```bash stategraph tx costs --tx ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tx` | Yes | Transaction ID (UUID), or set `STATEGRAPH_TX_ID` | ### Example ```bash stategraph tx costs --tx 550e8400-e29b-41d4-a716-446655440000 ``` Output: ``` Costs: Totals: Monthly: 130.82 USD → 140.16 USD (+9.34 USD) Hourly: 0.18 USD → 0.19 USD (+0.01 USD) Resources: 12 → 13 (+1) Coverage: 100.0% (unchanged) Per state: networking: +9.34/mo (resources +1) + aws_nat_gateway.main +9.34 ``` This is the same `Costs:` block that `stategraph tf plan` prints inline. Use `tx costs` to re-check a plan whose preview was not ready yet, or to inspect a plan's cost impact later. See [Plan-Time Cost](/docs/cost/plan-time) for how to read the delta. ## stategraph tx abort Abort an active transaction. ```bash stategraph tx abort --tx ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tx` | Yes | Transaction ID (UUID) | ### Example ```bash stategraph tx abort --tx 550e8400-e29b-41d4-a716-446655440000 ``` ## stategraph tx logs list List logs for a transaction. ```bash stategraph tx logs list --tx ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tx` | Yes | Transaction ID (UUID) | ### Example ```bash stategraph tx logs list --tx 550e8400-e29b-41d4-a716-446655440000 ``` Output (JSON): ```json { "results": [ { "id": "...", "action": "state_set", "object_type": "instance", "created_at": "2024-01-15T10:30:00Z", "state_id": "...", "user_id": "...", "data": { ... } } ] } ``` ## CI/CD Integration These examples run `terraform apply` directly, which requires your configuration to use the Stategraph Terraform HTTP backend. See [Using Stategraph as a Terraform HTTP backend](/docs/velocity/setup#using-stategraph-as-a-terraform-http-backend) for the `backend "http"` block — `TF_HTTP_PASSWORD` below is that backend's password. ### GitHub Actions Example ```yaml jobs: terraform: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Create transaction run: | TOKEN=$(docker run --rm \ -e STATEGRAPH_API_BASE=${{ secrets.STATEGRAPH_URL }} \ ghcr.io/stategraph/stategraph: stategraph tx create-with-session \ --tenant ${{ secrets.TENANT_ID }} \ --tag pipeline=github-actions \ --tag commit=${{ github.sha }}) echo "TF_HTTP_PASSWORD=$TOKEN" >> $GITHUB_ENV - name: Terraform Apply run: terraform apply -auto-approve ``` ### GitLab CI Example ```yaml terraform: script: - export TF_HTTP_PASSWORD=$(stategraph tx create-with-session --tenant $TENANT_ID --tag pipeline=gitlab-ci) - terraform apply -auto-approve ``` ## Transaction States | State | Description | |-------|-------------| | `open` | Transaction created, changes recorded | | `previewing` | Plan is being generated | | `committing` | Apply is in progress | | `committed` | Apply succeeded, state updated | | `failed` | Error occurred during preview or commit | | `aborted` | Cancelled by user | ## Next Steps - [States](/docs/cli/states) - State management - [Timeline](/docs/insights/timeline) - View transactions in UI --- # SQL Commands Source: https://stategraph.com/docs/cli/sql CLI commands for querying infrastructure with SQL. Execute Structured Query Language queries against your Terraform resources from the terminal. The `stategraph sql` command group provides access to SQL (Structured Query Language) for querying your infrastructure from the command line. ## Commands | Command | Description | |---------|-------------| | `stategraph sql query` | Execute a SQL query | | `stategraph sql schema` | Get the SQL schema | ## stategraph sql query Execute a SQL query against your infrastructure. `stategraph query` is a top-level alias for the same command. ```bash stategraph sql query ``` ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `` | Yes | SQL query string | ### Options | Option | Required | Description | |--------|----------|-------------| | `--state` | No | Scope the query to a single state (UUID). Without it, queries run across all states in the tenant. | | `--format` | No | Output format: `table` (default), `json`, or `simple` | By default, results print as a human-readable **table**. Pass `--format json` for JSON (useful for piping to `jq`), or `--format simple` for one value per line with no headers. ### Example ```bash stategraph sql query "SELECT type, count(*) AS total FROM resources GROUP BY type" --format json ``` Output (with `--format json`): ```json [ { "type": "aws_instance", "total": 20 }, { "type": "aws_security_group", "total": 15 }, { "type": "aws_subnet", "total": 6 } ] ``` ### More Queries ```bash # Count total instances stategraph sql query "SELECT count(*) FROM instances" # List all resource types with counts (count is reserved; alias as total and order by the expression) stategraph sql query "SELECT type, count(*) as total FROM resources GROUP BY type ORDER BY count(*) DESC" # Find resources by type (type lives on the resources table) stategraph sql query "SELECT address FROM resources WHERE type = 'aws_instance'" # Scope a query to one state stategraph sql query "SELECT count(*) AS total FROM resources" --state ``` ### SQL Syntax Notes Stategraph SQL supports a subset of standard SQL: - **JOINs work** — `INNER`, `LEFT`, and `RIGHT` joins with `ON` conditions - **Table aliases work** — `FROM resources AS r` lets you write `r.type` - **Column aliases work** — `count(*) as total` is supported - **CTEs work** — `WITH name AS (...)` clauses, including `UNION` - **ORDER BY works** — `ORDER BY type ASC` is supported - **LIKE / ILIKE work** — pattern matching with `%` and `_` wildcards, plus `NOT LIKE` / `NOT ILIKE` - **No comments** — `--` and `/* */` are not supported in queries ## stategraph sql schema Get the SQL schema for understanding available tables and columns. ```bash stategraph sql schema --format json ``` Like `sql query`, `sql schema` defaults to a table; pass `--format json` for the structure below. It shows all tables with column names and their PostgreSQL types: ```json { "tables": { "instances": { "columns": { "address": { "type": "text" }, "attributes": { "type": "jsonb" }, "dependencies": { "type": "text[]" }, "resource_address": { "type": "text" }, "state_id": { "type": "uuid" } } }, "resources": { "columns": { "address": { "type": "text" }, "module": { "type": "text" }, "state_id": { "type": "uuid" }, "type": { "type": "text" } } }, "states": { "columns": { "id": { "type": "uuid" }, "name": { "type": "text" }, "group_id": { "type": "uuid" }, "workspace": { "type": "text" } } } } } ``` Run `stategraph sql schema` to see the full list of tables and columns — there are additional tables beyond what's shown here. ## Scripting Examples ### Export query results to CSV ```bash stategraph sql query "SELECT address, type FROM resources" --format json | \ jq -r '.[] | [.address, .type] | @csv' > resources.csv ``` ### Count resources and format output ```bash stategraph sql query "SELECT type, count(*) AS total FROM resources GROUP BY type" --format json | \ jq -r '.[] | "\(.type): \(.total)"' ``` ### Generate report ```bash #!/bin/bash echo "Infrastructure Report" echo "====================" echo "" echo "Total resources:" stategraph sql query "SELECT count(*) AS total FROM instances" --format json | jq -r '.[0].total' echo "" echo "By type:" stategraph sql query "SELECT type, count(*) AS total FROM resources GROUP BY type" --format json | \ jq -r '.[] | " \(.type): \(.total)"' ``` ## Error Handling ### Syntax Errors ```bash stategraph sql query "SELEC * FROM instances" # Exit code: 2 # Error: could not parse the query (unexpected token near the start of the statement). ``` ### Unknown Columns ```bash stategraph sql query "SELECT foo FROM instances" # Exit code: 2 # Error: Unknown column 'foo'. # # To fix: run 'stategraph sql schema' to see available columns for each table. ``` ## Tips 1. **Quote queries** — Always wrap queries in quotes to prevent shell interpretation 2. **Use jq** — Pipe output to jq for formatting and filtering 3. **Test incrementally** — Build complex queries step by step 4. **Check schema** — Use `stategraph sql schema` to see available columns 5. **Qualify columns when joining** — Use a table name or alias (`r.type`) when querying multiple tables ## Next Steps - [Query Language](/docs/inventory/query) — Full SQL documentation - [SQL Syntax Reference](/docs/reference/sql-syntax) — Complete grammar - [Dashboards](/docs/inventory/dashboards) — Save queries in UI --- # Tenant Commands Source: https://stategraph.com/docs/cli/tenant CLI commands for tenant management and gap analysis. Find unmanaged cloud resources and identify infrastructure drift from the command line. The `stategraph tenant` command group provides tenant-level operations, including gap analysis for finding unmanaged cloud resources. ## Commands | Command | Description | |---------|-------------| | `stategraph tenant gaps analyze` | Run gap analysis | | `stategraph tenant gaps config` | Get gap analysis configuration | | `stategraph tenant gaps import` | Generate Terraform import blocks for unmanaged resources | ## stategraph tenant gaps analyze Run gap analysis to find cloud resources not managed by Terraform. `stategraph gaps` is a top-level alias for the same command. ```bash stategraph tenant gaps analyze --tenant --provider [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--provider` | Yes | Cloud provider (e.g., `aws`) | | `--source` | No | Data source: `cache` (default) or `no-cache` | ### Example ```bash stategraph tenant gaps analyze \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --provider aws ``` Gap analysis runs asynchronously: the first call for a provider starts a background scan and returns `{"status": "running", "started_at": }`. Re-run the command to retrieve the result once the scan finishes (subsequent calls are served from cache). See [Gap Analysis](/docs/inventory/gap-analysis) for the full flow. Output (JSON): ```json { "summary": { "total_aws_resources": 1500, "managed_by_stategraph": 1200, "unmanaged": 300, "phantom_filtered": 8 }, "unmanaged_resources": [ { "arn": "arn:aws:s3:::orphaned-bucket", "service": "s3", "resource_type": "s3:bucket", "region": "us-east-1", "owning_account_id": "123456789012" } ], "fetched_at": 1705312800 } ``` ### Force Fresh Scan ```bash stategraph tenant gaps analyze \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --provider aws \ --source no-cache ``` ## stategraph tenant gaps config Get gap analysis configuration status. ```bash stategraph tenant gaps config --tenant --provider ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--provider` | Yes | Cloud provider (e.g., `aws`) | ### Example ```bash stategraph tenant gaps config \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --provider aws ``` Output (JSON): ```json { "status": "ready", "ready_for_gap_analysis": true, "ready_for_terraform_import": true, "has_aggregator": true, "aggregator_region": "us-east-1", "indexed_regions": ["us-east-1", "us-west-2", "eu-west-1"], "index_count": 3, "warnings": [] } ``` ## stategraph tenant gaps import Generate Terraform `import` blocks and resource configuration for unmanaged resources. Pass a JSON file of resources (the `unmanaged_resources` array from `gaps analyze`) and Stategraph returns ready-to-use `import` blocks plus generated HCL. ```bash stategraph tenant gaps import --tenant [--provider ] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--provider` | No | Cloud provider (default: `aws`) | ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `` | Yes | JSON file containing an array of resources to import (the `unmanaged_resources` from `gaps analyze`) | ### Example First, save unmanaged resources to a file: ```bash stategraph tenant gaps analyze --tenant $TENANT_ID --provider aws | \ jq '.unmanaged_resources' > unmanaged.json ``` Then generate import blocks and configuration: ```bash stategraph tenant gaps import --tenant $TENANT_ID unmanaged.json ``` Output (JSON): ```json { "import_blocks": "import {\n provider = aws.us_east_1\n to = aws_s3_bucket.imported_orphaned_bucket\n id = \"orphaned-bucket\"\n}", "provider_hcl": "terraform {\n required_providers {\n aws = {\n source = \"hashicorp/aws\"\n version = \"~> 5.0\"\n }\n }\n}\n", "generated_hcl": "resource \"aws_s3_bucket\" \"imported_orphaned_bucket\" {\n bucket = \"orphaned-bucket\"\n # ... generated attributes\n}\n", "supported_count": 1, "unsupported_count": 0, "unsupported_resources": [] } ``` ## Scripting Examples ### Weekly Gap Analysis Report ```bash #!/bin/bash TENANT_ID="550e8400-e29b-41d4-a716-446655440000" echo "Gap Analysis Report - $(date)" echo "================================" echo "" result=$(stategraph tenant gaps analyze --tenant $TENANT_ID --provider aws --source no-cache) total=$(echo $result | jq '.summary.total_aws_resources') managed=$(echo $result | jq '.summary.managed_by_stategraph') unmanaged=$(echo $result | jq '.summary.unmanaged') echo "Total AWS resources: $total" echo "Managed by Terraform: $managed" echo "Unmanaged: $unmanaged" echo "" echo "Coverage: $(echo "scale=2; $managed * 100 / $total" | bc)%" echo "" echo "Top unmanaged resource types:" echo $result | jq -r '.unmanaged_resources | group_by(.service) | map({service: .[0].service, count: length}) | sort_by(-.count) | .[:5][] | " \(.service): \(.count)"' ``` ### Find and Import S3 Buckets ```bash #!/bin/bash TENANT_ID="550e8400-e29b-41d4-a716-446655440000" # Get unmanaged S3 buckets stategraph tenant gaps analyze --tenant $TENANT_ID --provider aws | \ jq '[.unmanaged_resources[] | select(.service == "s3")]' > s3_unmanaged.json # Generate import blocks stategraph tenant gaps import --tenant $TENANT_ID s3_unmanaged.json | \ jq -r '.import_blocks' > import.tf echo "Generated import.tf with $(jq length s3_unmanaged.json) S3 bucket imports" ``` ### Monitor Gap Trend ```bash #!/bin/bash TENANT_ID="550e8400-e29b-41d4-a716-446655440000" LOG_FILE="gap_history.csv" # Add header if file doesn't exist if [ ! -f $LOG_FILE ]; then echo "date,total,managed,unmanaged,coverage" > $LOG_FILE fi result=$(stategraph tenant gaps analyze --tenant $TENANT_ID --provider aws) total=$(echo $result | jq '.summary.total_aws_resources') managed=$(echo $result | jq '.summary.managed_by_stategraph') unmanaged=$(echo $result | jq '.summary.unmanaged') coverage=$(echo "scale=4; $managed / $total" | bc) echo "$(date -I),$total,$managed,$unmanaged,$coverage" >> $LOG_FILE ``` ## Next Steps - [Gap Analysis](/docs/inventory/gap-analysis) - Full gap analysis documentation - [States](/docs/cli/states) - State management commands - [Troubleshooting](/docs/reference/troubleshooting) - Common issues --- # Terraform Commands Source: https://stategraph.com/docs/cli/tf CLI reference for stategraph tf plan, apply, show, and mtx. Plan and apply Terraform changes through Stategraph with plan files and cost previews. The `stategraph tf` command group plans and applies Terraform changes through Stategraph. These commands replace `terraform plan` and `terraform apply` — Stategraph computes the plan scope from your change, runs the underlying Terraform binary, and records everything in a transaction. Two top-level aliases save typing: `stategraph plan` is `stategraph tf plan`, and `stategraph apply` is `stategraph tf apply`. ## Commands | Command | Description | |---------|-------------| | `stategraph tf plan` | Plan changes by comparing local HCL against the current state | | `stategraph tf apply` | Apply a saved plan file, or plan and apply in one step | | `stategraph tf show` | Re-display a plan file, optionally as JSON | | `stategraph tf mtx` | Plan across multiple states in a single atomic transaction | The classic workflow is plan-then-apply: `plan --out` writes a plan file, you review it, then `apply` commits it. `--out` is optional — without it, `plan` just shows a read-only preview. You can also skip the saved plan entirely and run `stategraph tf apply` with no plan file to plan, review the diff, and apply in one step. ```bash # From your root module directory stategraph tf plan --tenant --out plan.json stategraph tf apply plan.json ``` See [Velocity Setup](/docs/velocity/setup) for the end-to-end onboarding flow, including importing your existing state first. ## stategraph tf plan Plan Terraform changes by comparing local HCL against the current state in Stategraph. Outputs a plan file for review before applying. Run it from your root module directory — the state is auto-discovered from `stategraph.json` if `--state` is not passed. ```bash stategraph tf plan --tenant [--out ] [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID), or set `STATEGRAPH_TENANT_ID` | | `--out` | No | File to save the plan to for a later `stategraph tf apply`. When omitted, the plan is shown as a read-only preview and no plan file is written. | | `--state` | No | State ID (auto-discovered from `stategraph.json` if not set) | | `--workspace` | No | Workspace name (default: `"default"`) | | `--var` | No | Set a variable (`key=value`, repeatable) | | `--var-file` | No | Path to a variable file | | `--force` | No | Force a node address into the plan; supports globs (e.g., `data.*`). Repeatable. | | `--force-files` | No | Attach all files matching a path glob (e.g., `'**/*.yml'`) to every `file()` / `templatefile()` / `fileset()` call — an escape hatch for paths Stategraph cannot resolve statically. Repeatable. | | `--skip-refresh` | No | Skip the state refresh (passes `-refresh=false` to the underlying `terraform plan`) | | `--skip-costs` | No | Skip fetching the cost-delta preview after the plan diff | | `--costs-wait` | No | Maximum seconds to wait for the cost delta (default: `3`). Cannot be combined with `--skip-costs`. | | `--batch-size` | No | Batch size for transaction log appends (performance tuning) | | `--parallel-batches` | No | Number of parallel batches for transaction log appends (performance tuning) | | `--detailed-exitcode` | No | Return tofu/terraform-compatible exit codes (for CI scripting) | Pass the same `--var` / `--var-file` flags you use with `terraform plan`. ### Example ```bash stategraph tf plan --tenant 550e8400-e29b-41d4-a716-446655440000 --out plan.json ``` The plan output ends with the familiar summary line, followed by a cost preview when [cost analysis](/docs/cost) is configured: ``` Plan: 1 to add, 0 to change, 0 to destroy. Costs: Totals: Monthly: 130.82 USD → 140.16 USD (+9.34 USD) ... ``` If the cost preview is not ready within `--costs-wait` seconds, the plan prints a hint pointing at `stategraph tx costs` instead — see [Plan-Time Cost](/docs/cost/plan-time). ### Forcing resources into a plan By default, Stategraph plans only what your change touches. `--force` widens the plan to additional addresses: ```bash # Re-plan every data source stategraph tf plan --tenant --out plan.json --force 'data.*' # Force one module into the plan stategraph tf plan --tenant --out plan.json --force 'module.networking.*' ``` ## stategraph tf apply Apply Terraform changes. With a saved plan file, `apply` commits a plan generated earlier by `stategraph tf plan --out`. With no plan file, it runs a fresh plan, shows the diff, and prompts for approval before committing — a one-step alternative to the plan-then-apply flow. ```bash stategraph tf apply [options] [] ``` ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `` | No | Plan file produced by `stategraph tf plan` or `stategraph tf mtx`. When omitted, `apply` runs a fresh plan, shows the diff, and prompts for approval before committing. | ### Options | Option | Required | Description | |--------|----------|-------------| | `--skip-revision-check` | No | Skip the revision hash check between plan and apply | The plan file records the state revision it was computed against. If the state changed between plan and apply, `apply` refuses to run so you never apply a stale plan — re-plan instead. `--skip-revision-check` overrides this guard. ### Applying without a plan file When you omit the `` argument, `stategraph tf apply` runs a fresh plan, shows the diff, and prompts for approval before committing — the same one-step flow as `terraform apply`. Pass `--auto-approve` to skip the prompt: ```bash stategraph tf apply --tenant --auto-approve ``` The approval prompt requires an interactive terminal. With a non-tty stdin or `--silent` (`STATEGRAPH_SILENT`), the apply fails unless `--auto-approve` is also given — combine `--silent` with `--auto-approve` to apply non-interactively in CI. In this plan-less form, `apply` accepts the same flags as `stategraph tf plan` to configure the fresh plan it runs: `--tenant`, `--state`, `--workspace`, `--var`, `--var-file`, `--force`, `--force-files`, `--skip-refresh`, `--skip-costs`, `--costs-wait`, `--batch-size`, and `--parallel-batches`. When a saved plan file is supplied these flags are unnecessary — the plan already captured those choices — and several are rejected as a usage error. ### Example ```bash stategraph tf apply plan.json ``` ## stategraph tf show Show a previously generated plan file. By default it re-displays the plan in human-readable form; with `--json` it emits the plan as JSON with all names unmangled, which is useful for policy checks and tooling. ```bash stategraph tf show [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--json` | No | Emit the plan as JSON | ### Example ```bash # Review a plan again before applying stategraph tf show plan.json # Feed the plan into tooling stategraph tf show --json plan.json | jq '.' ``` ## stategraph tf mtx Plan Terraform changes across all workspaces in each given directory's group, in a single atomic transaction. Either every state applies or none do. Each directory must contain a `stategraph.json`. ```bash stategraph tf mtx --tenant --out [options] ... ``` ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `...` | Yes | One or more directories containing `stategraph.json` | ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID), or set `STATEGRAPH_TENANT_ID` | | `--out` | Yes | File to write the plan to | | `--force` | No | Force a node address into the plan; supports globs. Repeatable. | | `--skip-refresh` | No | Skip the state refresh during plan | | `--skip-costs` | No | Skip fetching the cost-delta preview | | `--costs-wait` | No | Maximum seconds to wait for the cost delta (default: `3`) | | `--batch-size` | No | Batch size for transaction log appends (performance tuning) | | `--parallel-batches` | No | Number of parallel batches for transaction log appends (performance tuning) | | `--detailed-exitcode` | No | Return tofu/terraform-compatible exit codes (for CI scripting) | ### Example ```bash stategraph tf mtx --tenant --out plan.json ./networking ./compute ./application stategraph tf apply plan.json ``` See [Multi-State Transactions](/docs/velocity/multi-state) for cross-state dependency semantics and guarantees. ## Configuration By default, the plan and apply stages execute `tofu` (OpenTofu). Set the `TF_CMD` environment variable to use a different binary, such as `terraform`. | Environment Variable | Description | |---------------------|-------------| | `TF_CMD` | Path to the Terraform/OpenTofu binary (default: `tofu`) | | `STATEGRAPH_TENANT_ID` | Tenant ID (alternative to `--tenant`) | | `STATEGRAPH_WORKSPACE` | Workspace name (alternative to `--workspace`) | | `STATEGRAPH_COSTS_WAIT_SECONDS` | Deployment-level default for `--costs-wait` | | `STATEGRAPH_TX_BATCH_SIZE` | Default for `--batch-size` | | `STATEGRAPH_TX_PARALLEL_BATCHES` | Default for `--parallel-batches` | ## Next Steps - [Velocity Setup](/docs/velocity/setup) - Import your state and run the first plan - [Multi-State Transactions](/docs/velocity/multi-state) - The `tf mtx` workflow in depth - [Plan-Time Cost](/docs/cost/plan-time) - Reading the `Costs:` block - [Refactor](/docs/cli/refactor) - Generate `moved` blocks during HCL restructuring - [Transactions](/docs/cli/transactions) - Inspect the transactions plans create --- # Refactor Source: https://stategraph.com/docs/cli/refactor Detect HCL refactors — resource renames and module moves — across plan steps and emit Terraform moved blocks, via a session-based CLI workflow. `stategraph refactor` is a session-based workflow that tracks HCL changes across successive plans and emits Terraform `moved` blocks when you're done. Use it when you're renaming resources, restructuring modules, or moving things between files and you want Stategraph to keep the state mapping in sync without hand-writing every `moved` block. ## When to use refactor mode Use `stategraph refactor` when you're making structural HCL changes like: - Renaming a resource (`aws_instance.web` → `aws_instance.frontend`) - Moving a resource into or out of a module - Renaming a module - Restructuring files without changing the actual infrastructure Refactor mode detects these changes at each step and maps old addresses to new ones. When you complete the session, it writes the mappings to a `moved` blocks file that Terraform picks up automatically. ## Workflow A refactor session has four phases: **start**, one or more **step** calls as you make edits, then **complete** or **abort**. ```bash # 1. Start a session from your root module stategraph refactor start # 2. Make HCL edits, then run step to detect changes stategraph refactor step # 3. Continue editing and stepping until you're done stategraph refactor step # 4. Complete the session to write moved blocks stategraph refactor complete ``` If you want to throw the session away: ```bash stategraph refactor abort ``` Session state lives in the `.stategraph` directory alongside your root module. ## Commands ### `stategraph refactor start` Starts a refactoring session. Snapshots the current HCL and state so later `step` calls have something to diff against. Run this from the root of your Terraform module. ```bash stategraph refactor start ``` ### `stategraph refactor step [OLD=NEW ...]` Detects what changed since the last `start` or `step` and maps removed addresses to added addresses. Prints each detected mapping as `OLD -> NEW`. If the automatic detection can't figure out where something moved (for example, when a resource was renamed and simultaneously had its attributes changed), `step` exits non-zero and lists the unmatched `added` and `removed` addresses. Pass explicit mappings to resolve them: ```bash stategraph refactor step aws_instance.web=aws_instance.frontend ``` You can pass multiple mappings, one per argument. Run `step` as many times as you need during a refactor. ### `stategraph refactor complete` Finalizes the session and writes the accumulated mappings to a `moved` blocks file in your module (printed at the end of the command). Terraform and OpenTofu automatically apply `moved` blocks on the next plan, so your state follows the refactor without manual `terraform state mv` calls. ```bash stategraph refactor complete ``` ### `stategraph refactor abort` Discards the current session and removes its state. Use this if you decide to back out of the refactor, or if the session got into a bad state and you want to start over. ```bash stategraph refactor abort ``` ## Notes - Refactor mode is scoped to the module you ran `start` in. Run it from the same directory you run `stategraph tf plan` in. - The session is persisted in `.stategraph/` — don't commit that directory mid-refactor. - Refactor mode currently handles HCL-visible moves. Changes that happen entirely inside provider behavior (e.g., an upstream provider renaming an attribute) aren't refactor-mode territory. ## How plan and apply drive a session Once a session is active (a `.stategraph` directory is present), the `stategraph tf` plan/apply workflow drives `step` and `complete` for you — you rarely call them by hand: - `stategraph tf plan` auto-runs a `step`, prints `WARNING: Running in refactor mode.`, and includes the detected `moved` blocks in the plan it produces. If a move is ambiguous (a resource was renamed and its attributes changed at the same time), the plan stops, lists the unmatched added and removed addresses, and tells you to `Run 'stategraph refactor step' to resolve.` — pass the explicit `OLD=NEW` mapping to `stategraph refactor step` (documented above) and re-plan. - `stategraph tf apply` also auto-runs a step, and after the commit succeeds it prints `WARNING: Running in refactor mode. Finalizing refactor session.` and runs `complete`, writing the `moved` blocks file and ending the session. `stategraph tf mtx` does not drive a refactor session — run a refactor through the single-state [`tf plan`](/docs/cli/tf) / `tf apply` commands. ## Next Steps - [Velocity Setup](/docs/velocity/setup) — the `stategraph tf` plan/apply workflow this integrates with - [CLI Overview](/docs/cli) — full command reference --- # Import Commands Source: https://stategraph.com/docs/cli/import CLI reference for stategraph import tf. Onboard an existing Terraform state file and its HCL configuration into Stategraph in one step. The `stategraph import` command group onboards existing Terraform projects into Stategraph. ## Commands | Command | Description | |---------|-------------| | `stategraph import tf` | Create a state and import a Terraform state file plus its HCL in one step | `import tf` is the recommended onboarding command: it creates the state, uploads your `terraform.tfstate`, and imports the HCL configuration from the current directory in a single call. The lower-level pieces are also available separately — [`stategraph states import`](/docs/cli/states) uploads only the state file, and [`stategraph hcl import`](/docs/cli/hcl) imports only the HCL. ## stategraph import tf Create a new state and import both a Terraform state file and HCL configuration in one step. Run it from the root of your Terraform module so the HCL can be discovered. ```bash stategraph import tf --tenant --name [options] ``` ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `` | Yes | Path to the Terraform state file | ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID), or set `STATEGRAPH_TENANT_ID` | | `--name` | No* | Name of the state to create | | `--workspace` | No | Workspace for the state (default: `"default"`) | | `--group` | No | Group ID for the state | | `--overwrite` | No | Overwrite an existing state (uses the state ID from `stategraph.json`) | | `--no-write-config` | No | Do not write a `stategraph.json` config file | | `--var` | No | Set a variable (`key=value`, repeatable) | | `--var-file` | No | Path to a variable file | | `--attach-files` | No | Attach files to resources matching an address glob, format `ADDRESS_GLOB=FILE_GLOB` (e.g., `'module.foo.*=**/*.yml'`). Repeatable. | | `--force-files` | No | Attach all files matching a path glob to every `file()` / `templatefile()` / `fileset()` call — an escape hatch for paths Stategraph cannot resolve statically. Repeatable. | | `--batch-size` | No | Batch size for transaction log appends (performance tuning) | | `--parallel-batches` | No | Number of parallel batches for transaction log appends (performance tuning) | | `-y` | No | Auto-confirm interactive prompts | | `--silent` | No | Suppress prompt output (only effective with `-y`) | *`--name` is required when creating a new state; with `--overwrite` the state is resolved from `stategraph.json` instead. If both `--overwrite` and `--name` are given, a new state is created when `stategraph.json` is absent. Pass the same `--var` / `--var-file` flags you use with `terraform plan` so the HCL evaluates the way Terraform would evaluate it. ### Example Pull your current state and import it: ```bash # From the root of your Terraform module terraform state pull > terraform.tfstate stategraph import tf \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --name "networking" \ terraform.tfstate ``` After the import, a `stategraph.json` is written to the current directory binding it to the new state. Commit that file — later commands like `stategraph tf plan` use it to resolve the state. Verify the import: ```bash stategraph states summary --state ``` ### Replacing an existing state To replace a previously imported state with fresh HCL and state data, pass `--overwrite`: ```bash stategraph import tf --overwrite \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --name "networking" \ terraform.tfstate ``` ### Attaching external files If your HCL reads external files via `file()`, `templatefile()`, or similar, tell Stategraph which files belong to which resources so they are included in change detection: ```bash stategraph import tf \ --tenant \ --name "app" \ --attach-files 'module.app.*=templates/*.tpl' \ terraform.tfstate ``` After import, the same mappings are managed with [`stategraph config files`](/docs/cli/config). ## Next Steps - [Velocity Setup](/docs/velocity/setup) - The full onboarding workflow - [Terraform Commands](/docs/cli/tf) - Plan and apply after importing - [States Commands](/docs/cli/states) - Inspect the imported state - [Config Commands](/docs/cli/config) - Manage attached file globs --- # HCL Commands Source: https://stategraph.com/docs/cli/hcl CLI commands for working with HCL. List resource addresses, evaluate expressions, convert between HCL and JSON, and import HCL into a state. The `stategraph hcl` command group works with Terraform HCL: listing addresses, evaluating expressions, converting between HCL and JSON, and importing HCL configuration into an existing state. Apart from `hcl import`, these commands run entirely locally and do not contact the server. ## Commands | Command | Description | |---------|-------------| | `stategraph hcl addresses` | List Terraform addresses from the HCL in the current directory | | `stategraph hcl eval` | Evaluate an HCL expression | | `stategraph hcl json` | Convert between HCL and JSON | | `stategraph hcl import` | Import HCL from the current directory into an existing state | ## stategraph hcl addresses List the Terraform addresses declared in the HCL of the current directory, including resources inside local modules, module calls, and variables. Run it from your root module. ```bash stategraph hcl addresses ``` ### Example ```bash cd my-terraform-module stategraph hcl addresses ``` Output (one address per line): ``` module.app.aws_instance.web module.app aws_subnet.public aws_vpc.main var.region ``` This is useful for building address globs for [`--force`](/docs/cli/tf), [`--attach-files`](/docs/cli/import), or [`stategraph config files`](/docs/cli/config), and for quick scripting over a module's contents. ## stategraph hcl eval Evaluate an HCL expression and print the result. Reads the expression from the argument, or from stdin when the argument is omitted. ```bash stategraph hcl eval [-e KEY=EXPR] [] ``` ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `` | No | HCL expression to evaluate. If omitted, reads from stdin. | ### Options | Option | Required | Description | |--------|----------|-------------| | `-e KEY=EXPR` | No | Bind a variable in the environment. `KEY` is a dot-separated path (e.g., `var.name`); `EXPR` is an HCL expression evaluated using the environment built so far. Repeatable. | ### Examples ```bash # Simple arithmetic stategraph hcl eval '1 + 2' # 3 # Terraform functions work stategraph hcl eval 'upper("hello")' # HELLO stategraph hcl eval 'join("-", ["a", "b", "c"])' # a-b-c # Bind variables with -e, then reference them stategraph hcl eval -e 'var.region="us-east-1"' '"bucket-${var.region}"' # bucket-us-east-1 ``` Use it to test interpolations and function calls without running a plan. ## stategraph hcl json Convert between HCL and JSON. Reads from stdin and writes to stdout. By default converts HCL to JSON in Terraform's [JSON configuration syntax](https://developer.hashicorp.com/terraform/language/syntax/json); `--to-hcl` converts the other way. ```bash stategraph hcl json [--safe] [--to-hcl] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--to-hcl` | No | Convert JSON to HCL instead of HCL to JSON | | `--safe` | No | Use a schema-free encoding that preserves block structure instead of the Terraform JSON syntax | ### Examples HCL to JSON: ```bash stategraph hcl json < main.tf ``` ```json { "resource": { "aws_s3_bucket": { "data": [ { "bucket": "my-data", "tags": { "env": "prod" } } ] } } } ``` JSON back to HCL: ```bash stategraph hcl json --to-hcl < main.tf.json ``` ``` resource "aws_s3_bucket" "data" { bucket = "my-data" } ``` With `--safe`, the output is a flat list of blocks with explicit `type`, `labels`, and `attrs` — useful when tooling needs the block structure without Terraform's schema conventions: ```bash stategraph hcl json --safe < main.tf ``` ```json [ { "type": "resource", "labels": [ "aws_s3_bucket", "data" ], "attrs": [ { "bucket": "my-data" } ] } ] ``` ## stategraph hcl import Import the HCL configuration files from the current directory into an existing state. Use this when the state already exists in Stategraph and you only need to (re)upload the configuration — for the combined state-plus-HCL onboarding, use [`stategraph import tf`](/docs/cli/import) instead. ```bash stategraph hcl import --tenant --state [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID), or set `STATEGRAPH_TENANT_ID` | | `--state` | Yes | State ID (UUID) to import the HCL into | | `--workspace` | No | Workspace for the state (default: `"default"`) | | `-y` | No | Auto-confirm interactive prompts | | `--silent` | No | Suppress prompt output (only effective with `-y`) | ### Example ```bash cd my-terraform-module stategraph hcl import \ --tenant 550e8400-e29b-41d4-a716-446655440000 \ --state 7c9e6679-7425-40de-944b-e07fc1f90ae7 ``` ## Debugging: stategraph test tf load The `stategraph test` group holds debug helpers. `test tf load` parses a Terraform module directory the same way the importer does and prints the parsed HCL — local modules are inlined with a `# module
` comment. Use it to check how Stategraph reads your configuration when an import or plan does not pick up what you expect. ```bash stategraph test tf load [--out ] ``` | Option | Required | Description | |--------|----------|-------------| | `` | Yes | Path to a Terraform module directory | | `--out` | No | Output file (defaults to stdout) | ```bash stategraph test tf load . ``` ## Next Steps - [Import Commands](/docs/cli/import) - One-step state and HCL onboarding - [Config Commands](/docs/cli/config) - Attach data files to addresses - [Refactor](/docs/cli/refactor) - Track HCL refactors with moved blocks --- # Cost Commands Source: https://stategraph.com/docs/cli/cost CLI commands for Stategraph cost intelligence: manage FOCUS billing sources and query state, tenant, attribution, history, and coverage data. The `stategraph cost` command group manages cost intelligence. It has two halves: managing [FOCUS](https://focus.finops.org/) billing sources — connections that ingest your actual cloud spend from AWS, GCP, or Azure — and read-only [cost queries](#cost-queries) over your computed estimates and billed spend (state and tenant cost, attribution, history, and coverage gaps). Billing sources are admin-only and managed per tenant. For the full ingestion workflow, including how to create the FOCUS export in each cloud provider, see [Cloud Billing (FOCUS)](/docs/cost/billing-sources). The plan-time cost preview is a separate feature — see [Cost Analysis](/docs/cost) and [Plan-Time Cost](/docs/cost/plan-time). ## Commands ### Billing sources | Command | Description | |---------|-------------| | `stategraph cost billing-source add` | Add a FOCUS billing source to a tenant | | `stategraph cost billing-source list` | List billing sources in a tenant | | `stategraph cost billing-source update` | Update a billing source (omitted fields are unchanged) | | `stategraph cost billing-source enable` | Resume scheduled sync for a source | | `stategraph cost billing-source disable` | Pause scheduled sync for a source | | `stategraph cost billing-source sync` | Trigger a sync of a source now | | `stategraph cost billing-source remove` | Remove a source and its loaded billing rows | All billing-source subcommands take `--tenant` (or `STATEGRAPH_TENANT_ID`). Apart from `add` and `list`, they take the source ID as a positional argument. ### Cost query commands | Command | Description | |---------|-------------| | `stategraph cost state` | Cost of a single state: totals, coverage, and per-resource breakdown | | `stategraph cost calculate` | Trigger a fresh cost calculation for a state (enqueues a recompute) | | `stategraph cost unsupported` | Coverage gaps for a state: resources the pricing engine cannot price | | `stategraph cost tenant` | Tenant cost rollup: totals, coverage, and per-state breakdown | | `stategraph cost attribution` | Attribution of actual (FOCUS) spend to managed resources, by provider | | `stategraph cost history` | Tenant cost over time, one point per day | | `stategraph cost tag-keys` | Tag keys available for cost attribution in a tenant | | `stategraph cost unmanaged` | Billed resources the tenant's states do not manage | ## stategraph cost billing-source add Add a FOCUS billing source. Point `--source-uri` at the exported files (Parquet or CSV; globs allowed). Credentials resolve ambiently — instance role, workload identity, `az login`, or the standard provider environment variables — so no secrets are handed to Stategraph. ```bash stategraph cost billing-source add \ --tenant \ --provider aws \ --source-uri "s3://my-bucket/focus/data/**/*.parquet" ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--provider` | Yes | Cloud provider: `aws`, `gcp`, or `azure` | | `--source-uri` | Yes | FOCUS export location, e.g. `s3://bucket/prefix/data/**/*.parquet` | | `--region` | No | Bucket region (AWS). Omit to resolve ambiently. | | `--window-months` | No | Trailing reload window in months (default: `2`, current plus previous) | | `--disabled` | No | Create the source disabled (no scheduled sync until enabled) | Source URI shapes per provider: | Provider | `--source-uri` | |----------|----------------| | AWS | `s3://bucket/prefix//data/**/*.parquet` | | GCP | `gs://bucket/prefix/**/*.parquet` | | Azure | `abfss://container@account.dfs.core.windows.net/path/**/*.parquet` | ### Example Output (JSON): ```json { "id": "2d5013c1-57c6-47d2-8e99-c988d6ecb1c8", "tenant_id": "550e8400-e29b-41d4-a716-446655440000", "provider": "aws", "source_uri": "s3://my-bucket/focus/data/**/*.parquet", "region": "us-east-1", "window_months": 2, "enabled": true, "created_at": "2026-06-11T12:47:32Z", "updated_at": "2026-06-11T12:47:32Z" } ``` ## stategraph cost billing-source list List the billing sources in a tenant, including each source's sync health. ```bash stategraph cost billing-source list --tenant ``` Output (table): ``` id provider source_uri region window_months enabled last_status last_synced_at last_row_count ------------ -------- ------------------------------------ --------- ------------- ------- ----------- -------------------- -------------- 2d5013c1-... aws s3://my-bucket/focus/data/**/*.par... us-east-1 2 true ok 2026-06-11T03:10:02Z 184022 ``` `last_status`, `last_synced_at`, and `last_row_count` confirm syncs are landing. Pass `--format json` for the structured form. ## stategraph cost billing-source update Update a billing source. Omitted fields are unchanged. ```bash stategraph cost billing-source update --tenant [options] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--source-uri` | No | New FOCUS export location | | `--region` | No | New bucket region | | `--window-months` | No | New trailing reload window in months | | `--enabled` | No | Enable (`true`) or disable (`false`) the source | ### Example ```bash stategraph cost billing-source update --tenant --window-months 3 ``` ## stategraph cost billing-source enable / disable Pause or resume a source's scheduled sync. Both take the source ID as the positional argument and print the updated source as JSON. ```bash stategraph cost billing-source disable --tenant stategraph cost billing-source enable --tenant ``` ## stategraph cost billing-source sync Trigger a sync of a billing source now instead of waiting for the schedule. ```bash stategraph cost billing-source sync --tenant [--from ] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--tenant` | Yes | Tenant ID (UUID) | | `--from` | No | Backfill from this date (`YYYY-MM-DD`) instead of the trailing window | ### Example ```bash # Backfill the year to date stategraph cost billing-source sync --tenant --from 2026-01-01 ``` ## stategraph cost billing-source remove Remove a billing source and the billing rows it loaded. Prompts for confirmation; pass `--auto-approve` to skip the prompt in scripts. ```bash stategraph cost billing-source remove --tenant [--auto-approve] ``` Output: ``` Billing source 2d5013c1-57c6-47d2-8e99-c988d6ecb1c8 deleted ``` ## Cost queries Beyond billing sources, the `cost` group reads computed cost data. Every cost query takes `--api-base` (or `STATEGRAPH_API_BASE`), and all but `calculate` accept `--format=table|json|simple` (default `table`). Tenant-scoped queries (`attribution`, `history`, `tag-keys`, `tenant`, `unmanaged`) take `--tenant` (or `STATEGRAPH_TENANT_ID`). State-scoped queries (`state`, `calculate`, `unsupported`) take `--state` (a UUID; required, with no env-var equivalent) and do not accept `--tenant`. ## stategraph cost state Price a single state: the latest snapshot's totals, coverage, and per-resource breakdown. See [State & Resource Cost](/docs/cost/state-cost) for the fields. ```bash stategraph cost state --state ``` Takes `--state` (required) and `--format`. ## stategraph cost calculate Enqueue a fresh cost recompute for a state. It returns a task and runs in the background; a new snapshot lands when it finishes. Takes `--state` only — it does not accept `--format`. ```bash stategraph cost calculate --state ``` ## stategraph cost unsupported List a state's coverage gaps: resources the pricing engine cannot price. Takes `--state` (required) and `--format`. ```bash stategraph cost unsupported --state ``` ## stategraph cost tenant Tenant cost rollup: totals, coverage, and a per-state breakdown. With `--tag-key`, it breaks the rollup down by that tag's values instead. Takes `--tenant` (required), `--format`, and `--tag-key`. ```bash stategraph cost tenant --tenant [--tag-key Team] ``` ## stategraph cost attribution Attribute actual (FOCUS) billed spend to your managed resources, by provider. Requires a connected [billing source](/docs/cost/billing-sources). Takes `--tenant` (required) and `--format`. ```bash stategraph cost attribution --tenant ``` ## stategraph cost history Tenant cost over time, one point per day, oldest first. Takes `--tenant` (required) and `--format`, plus the window and grouping options below. | Option | Required | Description | |--------|----------|-------------| | `--from` | No | History window start (ISO 8601). Defaults to 30 days ago | | `--to` | No | History window end (ISO 8601). Defaults to now | | `--group-by` | No | Group each point by `provider`, `type`, or `tag` (with `--tag-key`) | | `--tag-key` | No | Tag key to break each point down by when `--group-by tag` | ```bash stategraph cost history --tenant --group-by provider ``` ## stategraph cost tag-keys List the Terraform tag keys available for cost attribution in a tenant. Takes `--tenant` (required) and `--format`. ```bash stategraph cost tag-keys --tenant ``` ## stategraph cost unmanaged List billed resources the tenant's states do not manage. Takes `--tenant` (required), `--format`, and `--limit` (the maximum number of entries to return). ```bash stategraph cost unmanaged --tenant [--limit 20] ``` ## Next Steps - [Cloud Billing (FOCUS)](/docs/cost/billing-sources) - Provider export setup and the full workflow - [Cost Analysis](/docs/cost) - The estimate-based cost surface - [Plan-Time Cost](/docs/cost/plan-time) - Cost deltas in `stategraph tf plan` --- # Config Commands Source: https://stategraph.com/docs/cli/config CLI commands for managing stategraph.json. Attach external data file globs to resource addresses per workspace for change detection. The `stategraph config` command group manages the client-side `stategraph.json` file in your module directory. It edits the file locally and does not contact the server. ## Why attach files When your HCL reads external files — via `file()`, `templatefile()`, `fileset()`, `filebase64()`, or `data.local_file` — Stategraph needs to know which files belong to a change. `stategraph config files` records these as globs in `stategraph.json`, keyed by workspace and an address glob. When any attached file changes, the matching resources are included in the next plan. ## Commands | Command | Description | |---------|-------------| | `stategraph config files attach` | Attach file globs to an address glob under a workspace | | `stategraph config files remove` | Remove file globs, or an entire address entry | ## stategraph config files attach Attach file globs to an address glob under a workspace. Takes the union with any globs already attached at that address. ```bash stategraph config files attach --workspace --address ... ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--address` | Yes | Address glob identifying which blocks get the attached files (e.g., `'module.foo.*'`). Uses the same glob grammar as `--force`. | | `--workspace` | Yes | Workspace whose config entry to modify | ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `...` | Yes | One or more file globs to attach at the given address | ### Example ```bash stategraph config files attach --workspace default --address 'module.app.*' './templates/*.tpl' ``` ``` Attached 1 glob(s) at module.app.* for workspace default ``` The resulting `stategraph.json`: ```json { "workspaces": { "default": { "attached_files": { "module.app.*": { "globs": [ "./templates/*.tpl" ] } } } } } ``` Commit `stategraph.json` so the mappings travel with the module. ## stategraph config files remove Remove file globs from an address glob under a workspace. With no globs, removes the entire address entry. ```bash stategraph config files remove --workspace --address [...] ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--address` | Yes | Address glob to modify | | `--workspace` | Yes | Workspace whose config entry to modify | ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `...` | No | File globs to remove. When omitted, the entire address entry is removed. | ### Examples ```bash # Remove one glob stategraph config files remove --workspace default --address 'module.app.*' './templates/*.tpl' # Remove the whole address entry stategraph config files remove --workspace default --address 'module.app.*' ``` ## Related options The same mappings can be set at import time with [`stategraph import tf --attach-files`](/docs/cli/import). For paths Stategraph cannot resolve statically, `--force-files` on [`stategraph tf plan`](/docs/cli/tf) and `stategraph import tf` is the blunt-instrument alternative — it attaches every matching file to every file-reading call instead of specific addresses. ## Next Steps - [Import Commands](/docs/cli/import) - Attach files at import time - [Terraform Commands](/docs/cli/tf) - How attached files affect plans - [HCL Commands](/docs/cli/hcl) - List addresses to build globs against --- # Utility Commands Source: https://stategraph.com/docs/cli/utilities CLI utility commands for Stategraph. Get oriented with stategraph info and check client and server versions from the command line. Small commands for orientation and diagnostics. ## Commands | Command | Description | |---------|-------------| | `stategraph info` | Show current user, tenants, server version, and suggested next steps | | `stategraph version client` | Print the client version | | `stategraph version server` | Print the server version | ## stategraph info Show orientation info in a single call: the current user, client and server versions, and the tenants you belong to. In `table` or `simple` format it also prints suggested next steps — useful for new users and AI agents getting their bearings. ```bash stategraph info ``` ### Options | Option | Required | Description | |--------|----------|-------------| | `--format` | No | Output format: `table` (default), `json`, or `simple` | ### Example ```bash stategraph info ``` ``` key value -------- ---------------------------------------------- User Jane Smith (jane@example.com) [admin] Server 2.3.2 Client 2.3.2 Tenants 1 Tenant production (550e8400-e29b-41d4-a716-446655440000) --- What you can do next --- stategraph sql schema # Discover queryable tables stategraph query "SELECT * FROM resources" # Browse your infrastructure stategraph states list --tenant ID # List Terraform states stategraph gaps --tenant ID --provider aws # Find unmanaged resources stategraph blast-radius ADDR --state ID # Check impact before changes ``` For scripting, use `--format json` to get the same data as a single JSON object. ## stategraph version Print version information for the client and the server. `version client` is fully offline; `version server` queries the configured server. ```bash stategraph version client stategraph version server ``` ### Example ```bash stategraph version client # 2.3.2 stategraph version server # 2.3.2 ``` `stategraph --version` is equivalent to `version client`. Use these in CI to assert client/server compatibility before running operations. ## Next Steps - [CLI Overview](/docs/cli) - All commands and global options - [User Commands](/docs/cli/user) - Identity and tenant membership --- # Diagnostics Source: https://stategraph.com/docs/cli/diagnostics Run stategraph diagnostics to trace how Stategraph evaluates your Terraform HCL locally, then send us the output so we can understand your setup. The `stategraph diagnostics run` command evaluates your root module's HCL locally and writes a detailed trace of how Stategraph reads it. During onboarding we ask new users to run this command and send us the output. The trace tells us exactly how your modules, variables, and file references evaluate on your machine, which lets us understand your setup and help you import cleanly on the first try. This command makes no API calls. It runs only the local HCL evaluation, so you can run it before you have a server configured, an API key, or a single state imported. Nothing is uploaded. You run it, you get a file, and you decide what to send us. ## Why we ask new users to run this Every Terraform codebase is a little different: nested modules, `for_each` over computed values, `file()` and `templatefile()` reading data off disk, variables layered across `.tfvars` files. Before you import, we want to see how your specific HCL evaluates. The diagnostics trace gives us that picture without us needing access to your infrastructure, your cloud accounts, or your state. Sending us this trace early means: - We can spot HCL patterns that need extra configuration (for example, files that need to be attached to an address) before they trip you up. - We can confirm your modules resolve the way you expect. - Support conversations start with data instead of back-and-forth screenshots. ## What the trace contains (and what it does not) The trace is a stream of greppable `SG_TFEVAL_*` lines describing the evaluation, followed by a one-line rollup summary. It records **structure, not secrets**: types, reasons, origins, reference addresses, and expressions rendered back to HCL source. It never records the contents of an evaluated value. Data-map keys are hidden by default. That default keeps a run safe to share even when it is pointed at a production module. (Setting `SG_DIAG_SHOW_KEYS=1` reveals those keys, but you do not need it for onboarding and should leave it off unless we ask.) In short: the trace shows us how your HCL is shaped and how it evaluates, not the values it evaluates to. ## Quickstart Run it from your root module directory and write the trace to a file: ```bash # From your Terraform root module stategraph diagnostics run --out diagnostics.txt ``` Then send us `diagnostics.txt`. That is the whole onboarding step. If your module needs variables to evaluate (most do), pass the same `--var` and `--var-file` flags you use with `terraform plan`: ```bash stategraph diagnostics run \ --out diagnostics.txt \ --var-file prod.tfvars \ --var region=us-east-1 ``` To trace a module in another directory, pass it as the argument: ```bash stategraph diagnostics run ./infra/networking --out networking-diagnostics.txt ``` Without `--out`, the trace streams to stderr instead, which is handy for a quick look: ```bash stategraph diagnostics run 2>&1 | less ``` ## Options ```bash stategraph diagnostics run [options] [DIR] ``` ### Arguments | Argument | Required | Description | |----------|----------|-------------| | `DIR` | No | Root module directory to evaluate. Defaults to the current directory. | ### Options | Option | Required | Description | |--------|----------|-------------| | `--out` | No | File to write the trace to. When omitted, the trace streams to stderr. For onboarding, always use `--out` so you have a file to send us. | | `--workspace` | No | Workspace to evaluate (default: `default`, or set `STATEGRAPH_WORKSPACE`). | | `--var` | No | Set a variable (`key=value`, repeatable). | | `--var-file` | No | Path to a variable file (repeatable). | | `--attach-files` | No | Attach files to resources matching an address glob (`ADDRESS_GLOB=FILE_GLOB`, repeatable). Only needed if we ask you to reproduce a file-attachment case. | | `--attach-dest` | No | Destination directory for attached files (`ADDRESS_GLOB=DEST`, repeatable). | | `--mode` | No | Detail level: `rollup`, `on`, `detail`, `verbose` (default), or `perf`. `verbose` is the full step-by-step trace; `perf` stays silent during evaluation and prints only the `SG_TFEVAL_PERF*` timing breakdown for performance analysis. | For onboarding you do not need to touch `--mode`: it defaults to `verbose`, which is exactly the level we want. Verbose tracing emits one line per evaluated expression, so for a very large module the file can get big. That is expected, and the file compresses well if you want to zip it before sending. (If we are chasing a slow evaluation rather than a correctness issue, we may ask you to run `--mode perf`, which skips the per-expression trace and prints only a timing breakdown.) ## Reading the output (optional) You do not need to interpret the trace yourself, but here is what you are looking at if you are curious. Each line is a `key=value` record you can grep: | Line | What it reports | |------|-----------------| | `SG_TFEVAL_ROOT` | The root module directory and workspace the run started from. | | `SG_TFEVAL_NODE` | A resolved node (variable, local, output, module input) and its outcome. | | `SG_TFEVAL_ATTR` | A resource attribute and whether it resolved, was unresolved, or was dropped. | | `SG_TFEVAL_REF` | A reference between values and how its address resolved. | | `SG_TFEVAL_FILE` | A `file()` / `templatefile()` / `fileset()` reference and any failure or warning. | | `SG_TFEVAL_STEP` | One line per evaluated expression node (verbose detail). | | `SG_TFEVAL_ROLLUP` | The end-of-run summary: counts of nodes, unresolved and dropped values, file failures, missing variables, coercions, and more. | A quick way to see the shape of a run is to read the last line: ```bash grep SG_TFEVAL_ROLLUP diagnostics.txt ``` Or find anything that did not resolve cleanly: ```bash grep 'outcome=dropped' diagnostics.txt grep 'resolution=undefined' diagnostics.txt ``` The rollup alone often tells us most of what we need. If you want to sanity-check your own module before sending, non-zero `node_dropped`, `file_failures`, or `vars_missing` counts are the interesting ones. ## What to send us For onboarding, send the trace file: ```bash stategraph diagnostics run --out diagnostics.txt --var-file prod.tfvars # then attach diagnostics.txt to your onboarding thread ``` If you ran it against several root modules, send one file per module and name them so we can tell them apart. If a file is large, zip it first. Because the trace records structure rather than values, it is safe to share as-is, but you are always welcome to skim it first. ## Next Steps - [Import](/docs/cli/import) - Onboard a Terraform state and its HCL once diagnostics look clean - [Velocity Setup](/docs/velocity/setup) - The end-to-end onboarding flow - [HCL Commands](/docs/cli/hcl) - List addresses and evaluate expressions interactively - [Troubleshooting](/docs/reference/troubleshooting) - Common evaluation issues and fixes --- # Introduction Source: https://stategraph.com/docs/reference Complete reference documentation for Stategraph configuration, environment variables, API endpoints, SQL syntax, and health checks. Complete reference documentation for Stategraph configuration and APIs. ## Reference Documentation | Document | Description | |----------|-------------| | [Architecture](/docs/reference/architecture) | Component overview: the local CLI, the Server, and your cloud | | [API Reference](/docs/reference/api) | REST API endpoints and usage | | [Environment Variables](/docs/reference/environment-variables) | Configuration options | | [SQL Syntax](/docs/reference/sql-syntax) | Complete query language reference | | [Releases & Downloads](/docs/reference/releases) | Docker images and binary downloads | | [Terraform CLI Compatibility](/docs/reference/terraform-compatibility) | Mapping of Terraform CLI commands to Stategraph equivalents | | [Glossary](/docs/reference/glossary) | Definitions of key Stategraph terms and their Terraform mappings | | [Health Checks](/docs/reference/health-checks) | Liveness and readiness endpoints for monitoring and orchestration | | [Troubleshooting](/docs/reference/troubleshooting) | Common issues and solutions for deployment, authentication, and performance | ## Quick Links ### API Endpoints - `GET /api/v1/whoami` - Current user information - `GET /api/v1/mql` - Execute SQL query - `GET /api/v1/tenants/{id}/states` - List states - `POST /api/v1/states/backend/{group_id}` - Store state (Terraform) - `GET /api/v1/states/{id}/instances` - List resource instances - `GET /api/v1/states/{id}/costs` - Cost snapshot for a state - `GET /api/v1/openapi` - OpenAPI schema [Full API Reference](/docs/reference/api) ### Common Environment Variables ```bash # Required STATEGRAPH_UI_BASE=https://stategraph.example.com DB_HOST=postgres DB_USER=stategraph DB_PASS=password DB_NAME=stategraph # Optional STATEGRAPH_PORT=8180 STATEGRAPH_OAUTH_TYPE=google ``` [Full Environment Variables Reference](/docs/reference/environment-variables) ### SQL Quick Reference ```sql -- Basic query SELECT * FROM resources WHERE type = 'aws_instance' -- Aggregation SELECT type, count(*) FROM resources GROUP BY type ORDER BY type -- JSON access SELECT attributes->>'instance_type' FROM instances ``` [Full SQL Reference](/docs/reference/sql-syntax) --- # API Reference Source: https://stategraph.com/docs/reference/api Complete REST API reference for Stategraph. Programmatic access to states, queries, transactions, and infrastructure management operations. Stategraph provides a REST API for programmatic access to states, queries, and management operations. ## Authentication Stategraph supports two authentication methods: ### Bearer Token (API Key) For CLI, Terraform, and programmatic access, use Bearer authentication with API keys: ```bash curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" http://localhost:8080/api/v1/... ``` API keys are created via the Settings page in the UI. ### Session Cookie For browser-based access, authentication uses session cookies set after login (local authentication or OAuth). This is handled automatically by the browser. ### Authentication Modes Stategraph supports three authentication modes: - **Local Authentication** - Email/password with user management (default) - **Google OAuth** - SSO via Google Workspace - **Generic OIDC** - Any OIDC-compatible provider ## Base URL ``` http://localhost:8080/api/v1 ``` Replace `localhost:8080` with your Stategraph server address if different. --- ## User Endpoints ### Get Current User ``` GET /api/v1/whoami ``` Returns information about the authenticated user. **Response** ```json { "id": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "name": "User Name", "email": "user@example.com", "type": "user", "capabilities": { "admin": true } } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique user ID (UUID) | | `name` | string | Display name | | `email` | string | User email address | | `type` | enum | `user`, `api`, or `system` | | `capabilities` | object | The session's capability grants (admin / commit / preview / sudo / users-manage / access-token-create / access-token-refresh). See [Access Tokens & Capabilities](/docs/authentication/access-tokens). | | `auth_origin` | string | How the user authenticates (optional). | | `avatar_url` | string | Avatar image URL (optional). | Admin status is conveyed through `capabilities.admin` rather than a boolean `is_admin` field. This per-user `capabilities` object describes what the identity may do; for server-wide feature flags, see [Get Server Capabilities](#get-server-capabilities). ### List User Tenants ``` GET /api/v1/user/tenants ``` Returns tenants the user has access to. **Response** ```json { "results": [ { "id": "tenant-123", "name": "My Organization" } ] } ``` ### Get Server Capabilities ``` GET /api/v1/capabilities ``` Reports the server's feature flags so clients (including the UI) decide which features to offer. This is server-wide configuration, distinct from the per-user `capabilities` returned by `whoami`. **Response** ```json { "costs": { "enabled": true } } ``` | Field | Type | Description | |-------|------|-------------| | `costs.enabled` | boolean | Whether the server can answer cost queries. | --- ## Authentication Endpoints ### Get Login Options ``` GET /api/v1/login/options ``` Returns available authentication methods. **Response** When local authentication is the only method configured: ```json { "options": [] } ``` When OAuth is configured: ```json { "options": [ { "type": "google", "display_name": "Sign in with Google" } ] } ``` ### Login with Password ``` POST /api/v1/login/password ``` Authenticates user with email and password (local authentication only). **Request Body** ```json { "email": "user@example.com", "password": "your-password" } ``` **Response** ```json { "session_token": "eyJhbGciOiJIUzI1NiIs...", "success": true } ``` The `session_token` can be used for Bearer authentication. A session cookie is also set for browser-based access. ### Check Setup Status ``` GET /api/v1/setup/status ``` Returns whether initial setup is needed (no users exist). **Response** ```json { "needs_setup": false } ``` ### Create First Admin User ``` POST /api/v1/setup/admin ``` Creates the first admin user during initial setup. Only works when no users exist. **Request Body** ```json { "email": "admin@example.com", "password": "secure-password", "name": "Admin User", "organization": "My Organization" } ``` **Response** ```json { "session_token": "eyJhbGciOiJIUzI1NiIs...", "user_id": "f30ed1f9-be44-46a3-9050-03e9561e94f0" } ``` The `session_token` can be used for Bearer authentication. ### Complete Setup ``` POST /api/v1/setup/complete ``` Marks the initial setup as complete. Called after creating the first admin user. **Response**: `200 OK` ### Logout ``` GET /api/v1/logout ``` Clears session cookie and logs out the user. **Response**: `200 OK` with redirect --- ## User Management Endpoints All user management endpoints require admin authentication. ### List Users ``` GET /api/v1/users ``` Returns list of users with cursor-based pagination and filtering. **Parameters** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `type` | query | No | Filter by user type: `user` or `api` | | `limit` | query | No | Max results (default: 25) | | `cursor` | query | No | Pagination cursor from previous response | | `search` | query | No | Search by name or email | **Response** ```json { "users": [ { "id": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "name": "John Doe", "email": "john@example.com", "is_admin": false, "type": "user", "auth_origin": "local", "created_at": "2024-01-15T10:30:00Z" } ], "total_count": 10, "limit": 25, "has_more": false, "next_cursor": null } ``` | Field | Type | Description | |-------|------|-------------| | `users` | array | List of user objects | | `total_count` | integer | Total number of matching users | | `limit` | integer | Limit used for this request | | `has_more` | boolean | Whether more results are available | | `next_cursor` | string | Cursor for next page (null if no more) | | `auth_origin` | string | Authentication origin (e.g., `local`, `google`, `oidc`) | ### Get User Details ``` GET /api/v1/users/detail?user_id={user_id} ``` Returns detailed information about a specific user. **Response** ```json { "id": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "name": "John Doe", "email": "john@example.com", "is_admin": false, "type": "user", "created_at": "2024-01-15T10:30:00Z" } ``` **Note**: The response also includes `auth_origin` (how the user authenticates) and `avatar_url` when those values are set. ### Create User ``` POST /api/v1/users ``` Creates a new user account. **Request Body** ```json { "name": "Jane Doe", "email": "jane@example.com", "password": "secure-password", "is_admin": false } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Display name | | `email` | string | Yes | Email address (used for login) | | `password` | string | Yes | Password (8-128 characters) | | `is_admin` | boolean | No | Grant admin privileges (default: false) | **Response** ```json { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "name": "Jane Doe", "email": "jane@example.com", "is_admin": false, "type": "user" } ``` ### Update User ``` PUT /api/v1/users/update?user_id={user_id} ``` Updates user information. **Request Body** ```json { "name": "Jane Smith", "email": "jane.smith@example.com" } ``` **Response**: `200 OK` with updated user object ### Delete User ``` DELETE /api/v1/users/delete?user_id={user_id} ``` Soft-deletes a user (marks as deleted, invalidates API keys). **Response**: `204 No Content` ### Change User Password ``` POST /api/v1/users/change-password?user_id={user_id} ``` Changes a user's password. **Request Body** ```json { "new_password": "new-secure-password", "current_password": "old-password" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `new_password` | string | Yes | New password (8-128 characters) | | `current_password` | string | Conditional | Required when changing own password | **Response**: `200 OK` **Note**: Admins can change any user's password without providing `current_password`. Users changing their own password must provide `current_password`. ### Toggle Admin Status ``` POST /api/v1/users/toggle-admin?user_id={user_id} ``` Grants or revokes admin privileges. **Request Body** ```json { "is_admin": true } ``` **Response**: `200 OK` with updated user object **Note**: Cannot remove admin privileges from the last admin user. ### Create Service Account ``` POST /api/v1/api-users ``` Creates a service account (API user) for programmatic access. **Request Body** ```json { "name": "ci-production", "tenant_id": "550e8400-e29b-41d4-a716-446655440000" } ``` **Response** ```json { "user_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "token": "eyJhbGciOiJIUzI1NiIs..." } ``` **Important**: The token is only returned once. Save it immediately. --- ## State Endpoints ### List States ``` GET /api/v1/tenants/{tenant_id}/states ``` Returns all states for a tenant. **Parameters** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `tenant_id` | path | Yes | Tenant ID | | `page` | query | No | Pagination cursor | | `limit` | query | No | Max results (default: 100) | | `q` | query | No | SQL filter query | **Response** ```json { "results": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "networking", "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "workspace": "default", "created_at": "2024-01-15T10:30:00Z" } ] } ``` ### Create State ``` POST /api/v1/tenants/{tenant_id}/states ``` Creates a new state. **Request Body** ```json { "name": "networking", "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "workspace": "production" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | State name | | `group_id` | uuid | No | Group identifier (UUID; defaults to a generated UUID) | | `workspace` | string | No | Workspace name (default: `default`) | **Response**: `201 Created` with state object ### Import State ``` POST /api/v1/tenants/{tenant_id}/states/import ``` Imports an existing Terraform state file. **Request Body** ```json { "name": "networking", "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "workspace": "production", "state": { ... }, "tags": { "source": "migration" } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | State name | | `state` | object | Yes | Terraform state JSON | | `group_id` | uuid | No | Group identifier (UUID; defaults to a generated UUID) | | `workspace` | string | No | Workspace name | | `tags` | object | No | Metadata tags | ### Export State ``` GET /api/v1/states/{state_id}/export ``` Returns the full Terraform state as JSON (equivalent to `terraform state pull` / `stategraph states export`). ```bash curl "http://localhost:8080/api/v1/states/$STATE_ID/export" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" ``` ### Delete State ``` DELETE /api/v1/states/{state_id} ``` Permanently deletes a state and all of its related data (resources, instances, providers, outputs, raw states, transaction logs, and check entries/results). This cannot be undone. Requires admin privileges. **Parameters** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `state_id` | path | Yes | State ID (UUID) | **Authorization** Requires admin privileges. **Response Codes** | Code | Description | |------|-------------| | `200 OK` | State successfully deleted | | `401 Unauthorized` | Not authenticated | | `403 Forbidden` | User is not an admin | | `404 Not Found` | State does not exist | | `500 Internal Server Error` | Server error occurred | **Example** ```bash curl -X DELETE "http://localhost:8080/api/v1/states/550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" ``` **Error Response (403 Forbidden)** ```json { "id": "ADMIN_REQUIRED", "data": "Admin privileges required" } ``` **Error Response (404 Not Found)** ```json { "id": "STATE_NOT_FOUND", "data": "State not found" } ``` --- ## Instance Endpoints ### List Instances ``` GET /api/v1/states/{state_id}/instances ``` Returns resource instances for a state. **Parameters** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `state_id` | path | Yes | State ID | | `page` | query | No | Pagination cursor | | `limit` | query | No | Max results | | `q` | query | No | SQL filter | **Response** ```json { "results": [ { "address": "aws_instance.web", "type": "aws_instance", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "module": null, "attributes": { ... }, "dependencies": ["aws_subnet.main", "aws_security_group.web"] } ] } ``` ### Get Blast Radius ``` GET /api/v1/states/{state_id}/instances/{instance_address}/blast-radius ``` Returns resources affected by changes to the specified instance. **Parameters** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `state_id` | path | Yes | State ID | | `instance_address` | path | Yes | URL-encoded instance address | **Response** ```json { "results": [ { "address": "aws_eip.web", "resource_address": "aws_eip.web", "distance": 1 } ] } ``` --- ## Module Endpoints ### List Modules ``` GET /api/v1/states/{state_id}/modules ``` Returns modules in a state. **Response** ```json { "results": [ { "name": "module.vpc", "instance_count": 25, "resource_count": 10 } ] } ``` --- ## Summary Endpoints ### State Summary ``` GET /api/v1/states/{state_id}/summary ``` Returns aggregate statistics for a state. **Response** ```json { "instances": 150, "resources": 45, "modules": 8, "providers": 3, "edges": 234 } ``` ### Resources Summary ``` GET /api/v1/states/{state_id}/resources/summary ``` Returns instance counts by resource type. **Response** ```json { "aws_instance": { "instances": 20 }, "aws_security_group": { "instances": 15 }, "aws_subnet": { "instances": 6 } } ``` ### Tenant Summary ``` GET /api/v1/tenants/{tenant_id}/summary[?state_id={state_id}] ``` Returns inventory aggregates across a tenant (or a single state with `?state_id`): totals, provider and resource-type distributions, graph structure, largest/most-deployed modules, and orphaned-resource counts — the data behind the overview dashboard. **Response** (abridged) ```json { "total_states": 15, "total_resources": 1234, "total_instances": 1890, "total_modules": 42, "total_providers": 3, "total_edges": 2310, "provider_distribution": [], "resource_type_distribution": [], "top_resource_type": "aws_iam_role", "largest_module": "module.vpc", "most_deployed_module": "module.service", "orphaned_count": 7, "graph_roots": 12, "graph_leaves": 340 } ``` --- ## Query Endpoints ### Execute SQL Query ``` GET /api/v1/mql ``` Executes an SQL query across all states. **Parameters** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `q` | query | Yes | SQL query string | | `page` | query | No | Pagination cursor | | `tz` | query | No | Timezone for date formatting | **Example** ```bash curl "http://localhost:8080/api/v1/mql?q=SELECT%20*%20FROM%20resources%20WHERE%20type%20%3D%20%27aws_instance%27" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" ``` **Response** ```json [ { "address": "aws_instance.web", "type": "aws_instance", ... } ] ``` **Result size** The number of rows returned is controlled by the query's own `LIMIT` clause, capped at a maximum of 1000 rows. A `limit` query parameter is **not** supported and is ignored; set the bound with `LIMIT` inside the query instead. A query with **no** `LIMIT` clause is capped at a default of 20 rows. When that default truncates the result, the response carries an `mql-default-limit-applied` header so clients can detect the truncation instead of silently undercounting. Add an explicit `LIMIT` (or paginate with `ORDER BY`) to fetch more. The active `default_limit` and `max_limit` values are published by the schema endpoint below. **Cursor pagination** Cursor pagination is exposed through an RFC 5988 `Link` response header, and is only available when the query includes an `ORDER BY` clause (a stable sort order is required to page deterministically). - With `ORDER BY`: when more rows remain, the response includes a `Link` header with `rel="next"` (and `rel="prev"` when paging through results). Follow the `Link` URL to fetch the next page. ``` Link: ; rel="next" ``` - Without `ORDER BY`: no `Link` header is emitted. Instead the response carries an `mql-pagination-error: ORDER_BY_MISSING` header, signalling that the result cannot be paginated. Add an `ORDER BY` clause to enable paging. ### Get SQL Schema ``` GET /api/v1/mql/schema ``` Returns the SQL schema for autocomplete. The response also reports `default_limit` (the row limit applied to a query with no `LIMIT` of its own) and `max_limit` (the ceiling an explicit `LIMIT` is capped to) alongside the `tables` object. --- ## Transaction Endpoints ### List Transactions ``` GET /api/v1/tenants/{tenant_id}/tx ``` Returns transactions for a tenant. **Response** ```json { "results": [ { "id": "455fe705-f27f-4335-9355-dbe8f14098df", "created_at": "2024-01-15T10:30:00Z", "created_by": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "completed_at": "2024-01-15T10:30:05Z", "completed_by": "f30ed1f9-be44-46a3-9050-03e9561e94f0", "state": "committed", "tags": {"desc": "Backend update"} } ] } ``` ### Create Transaction ``` POST /api/v1/tenants/{tenant_id}/tx/create ``` Creates a new transaction. **Request Body** ```json { "tags": { "pipeline": "github-actions", "commit": "abc123" } } ``` ### Get Transaction Logs ``` GET /api/v1/tx/{tx_id}/logs ``` Returns logs for a transaction. **Response** ```json { "results": [ { "id": "log-123", "action": "state_set", "object_type": "instance", "created_at": "2024-01-15T10:30:00Z", "state_id": "state-123", "data": { ... } } ] } ``` ### Abort Transaction ``` POST /api/v1/tx/{tx_id}/abort ``` Aborts an active transaction. --- ## API Key Endpoints API keys (access tokens) authenticate the CLI, Terraform, and API requests as a Bearer token. ### Create API Key ``` POST /api/v1/user/access-tokens ``` **Request Body** The required `name` labels the token. An optional `capabilities` object scopes what the token may do (the same model returned by `whoami`): ```json { "name": "ci-prod-apply", "capabilities": { "commit": { "states": { "a1b2c3d4-5678-90ab-cdef-1234567890ab": ["*"] } } } } ``` When `capabilities` is omitted, the token inherits the creator's full session capabilities. Any requested capabilities are masked against the creator's, so a token can never exceed the identity that created it. See [Access Tokens & Capabilities](/docs/authentication/access-tokens) for the CLI equivalent and the full capability model. **Response** — the token is returned once; store it securely. ```json { "token": "eyJhbGciOiJIUzI1NiIs..." } ``` ### List API Keys ``` GET /api/v1/user/access-tokens ``` **Response** ```json { "tokens": [ { "id": "...", "name": "my-api-key", "created_at": "2026-06-04T10:30:00Z", "owner_id": "...", "owner_name": "jane@example.com", "owner_type": "user" } ] } ``` Each token object always includes `id`, `name`, `created_at`, `owner_id`, `owner_name`, and `owner_type`. The `expiration` field is only present when an expiry has been set on the token; it is omitted for tokens that never expire. ### Revoke API Key ``` POST /api/v1/user/access-tokens/revoke?token_id={id} ``` Revokes the token with the given ID. **Response** ```json { "id": "f02791c8-aa63-4cdf-acae-a7c968d8a831", "revoked": true } ``` > For Terraform CI/CD, a per-run **session token** (used as `TF_HTTP_PASSWORD`) is minted separately > via `POST /api/v1/tx/session/create` — see [Transactions](/docs/velocity/transactions). --- ## Gap Analysis Endpoints ### Get Gap Analysis Config ``` GET /api/v1/tenants/{tenant_id}/gaps/config ``` Returns gap analysis configuration status. **Parameters** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `provider` | query | Yes | Cloud provider (e.g., `aws`) | **Response** ```json { "status": "ready", "ready_for_gap_analysis": true, "ready_for_terraform_import": true, "has_aggregator": true, "aggregator_region": "us-east-1", "indexed_regions": ["us-east-1", "us-west-2"], "index_count": 2, "warnings": [] } ``` ### Run Gap Analysis ``` GET /api/v1/tenants/{tenant_id}/gaps ``` Returns unmanaged resources. Gap analysis runs asynchronously: the first request for a provider starts a background scan and returns `{ "status": "running", "started_at": }`. Repeat the request to retrieve the result once the scan completes (subsequent calls are served from cache). **Parameters** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `provider` | query | Yes | Cloud provider | | `source` | query | No | `cache` (default) or `no-cache` | **Response** ```json { "summary": { "total_aws_resources": 1500, "managed_by_stategraph": 1200, "unmanaged": 300, "phantom_filtered": 8 }, "unmanaged_resources": [ ... ], "fetched_at": 1705312800 } ``` ### Generate Import ``` POST /api/v1/tenants/{tenant_id}/gaps/import ``` Generates Terraform import blocks. **Request Body** ```json { "provider": "aws", "resources": [ { "arn": "arn:aws:s3:::bucket-name", "service": "s3", "resource_type": "s3:bucket", "region": "us-east-1", "owning_account_id": "123456789012" } ] } ``` **Response** ```json { "import_blocks": "import { ... }", "provider_hcl": "provider \"aws\" { ... }", "generated_hcl": "resource \"aws_s3_bucket\" { ... }", "supported_count": 1, "unsupported_count": 0, "unsupported_resources": [] } ``` --- ## Version ``` GET /api/v1/version ``` Returns the server version. **Response** ```json { "version": "1.2.3" } ``` ## Cost Endpoints Estimate and read infrastructure costs. See [Cost Analysis](/docs/cost) for the full guide, response shapes, and SQL access. Monetary fields are strings (parse as decimals). Cost analysis requires the server to be configured with a pricing service. | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/states/{state_id}/costs` | Latest cost snapshot for a state with per-resource breakdown (`404` if never priced) | | `POST` | `/api/v1/states/{state_id}/costs/calculate` | Trigger a recompute; returns `202` with a task (poll `/api/v1/tasks/{id}`); `503` if no pricing service | | `GET` | `/api/v1/states/{state_id}/costs/unsupported` | Resources that did not contribute to the totals (coverage gaps) | | `GET` | `/api/v1/tenants/{tenant_id}/costs` | Current cost rollup across all states (`?tag_key` to break down by a tag) | | `GET` | `/api/v1/tenants/{tenant_id}/costs/history` | Cost over time, one point per day (`?from`, `?to`, `?group_by`, `?tag_key`) | | `GET` | `/api/v1/tenants/{tenant_id}/costs/tag-keys` | Tag keys available for grouping | | `GET` | `/api/v1/tx/{tx_id}/costs` | Plan-time cost delta for a pending transaction (see [Plan-time cost](#plan-time-cost)) | **Cost history parameters** The `from` and `to` query parameters require a full ISO 8601 / RFC 3339 timestamp (for example `2026-06-01T00:00:00Z`). A bare calendar date such as `2026-06-01` is rejected with `400 Bad Request` and body `{"id": "INVALID_DATE_PARAM", "data": "..."}`. `group_by` accepts `provider`, `type`, or `tag`. When `group_by=tag`, a `tag_key` query parameter is required; omitting it (or passing an unknown `group_by` value) returns `400 Bad Request` with body `{"id": "INVALID_GROUP_BY", "data": "..."}`. ### Plan-time cost ``` GET /api/v1/tx/{tx_id}/costs ``` Returns the current-vs-planned cost delta for a transaction opened by `stategraph tf plan` / `tf mtx`. See [Plan-Time Cost](/docs/cost/plan-time) for the CLI and the full response shape. - `200` — a `tx-cost-delta`: `totals` (each metric as `current_*` / `planned_*` / `delta_*`), a per-state `states[]` list with per-resource `resources[]` (each carrying a `change_kind` of `added` / `removed` / `changed`), and `by_provider` / `by_type` / `by_tag` delta breakdowns. - `202` — `{"status": "computing"}`: the preview isn't ready yet, or the transaction wasn't opened through `stategraph` planning. Retry shortly. - `404` — the transaction was aborted (which deletes its planned cost). - `401` — the caller isn't a member of the transaction's tenant. ## Billing Source Endpoints Admin-only endpoints for connecting cloud billing (FOCUS) exports — your actual cloud spend. See [Cloud Billing (FOCUS)](/docs/cost/billing-sources). A non-admin caller receives `403`. | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/tenants/{tenant_id}/billing-sources` | List billing sources | | `POST` | `/api/v1/tenants/{tenant_id}/billing-sources` | Create a source — body `{provider, source_uri, region?, window_months?, enabled?}` | | `PUT` | `/api/v1/tenants/{tenant_id}/billing-sources/{id}` | Update a source (omitted fields unchanged) | | `POST` | `/api/v1/tenants/{tenant_id}/billing-sources/{id}/sync` | Trigger a sync — body `{window_start?}` | | `DELETE` | `/api/v1/tenants/{tenant_id}/billing-sources/{id}` | Delete a source and its loaded billing rows | ## OpenAPI Schema ### Get OpenAPI Schema ``` GET /api/v1/openapi ``` Returns the full OpenAPI schema for the Stategraph API as JSON. This endpoint is unauthenticated and can be used to discover available endpoints, request/response types, and integrate with tools that consume OpenAPI specifications. **Example** ```bash curl http://localhost:8080/api/v1/openapi ``` **Response**: `200 OK` with the complete OpenAPI JSON schema. The schema is also available as a downloadable file from each [release](/docs/reference/releases). --- ## Error Responses ### 400 Bad Request The response body for a `400` is endpoint-specific. Many endpoints return a structured error of the form: ```json { "id": "INVALID_DATE_PARAM", "data": "Error description" } ``` where `id` is a machine-readable error code (for example `INVALID_DATE_PARAM` or `INVALID_GROUP_BY`). Some validation failures — such as rejecting a malformed UUID when creating a state — instead return a `400` with an **empty body**. Do not assume a JSON body is always present on a `400`; check the status code first. ### 401 Unauthorized No body. Authentication required. ### 404 Not Found Resource not found. ### 500 Internal Server Error Server error occurred. --- ## Pagination List endpoints support cursor-based pagination: ```bash # First page curl "http://localhost:8080/api/v1/tenants/$TENANT_ID/states?limit=50" \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" # Next page (use cursor from previous response) curl "http://localhost:8080/api/v1/tenants/$TENANT_ID/states?limit=50&page=cursor..." \ -H "Authorization: Bearer $STATEGRAPH_API_KEY" ``` --- # Architecture Source: https://stategraph.com/docs/reference/architecture Stategraph architecture overview. How the local CLI, the coordinating Server, and your cloud interact to plan and apply Terraform changes. Stategraph has two parts you run — the **CLI** (local) and the **Server** (a coordinator backed by PostgreSQL). On every plan and apply, the CLI sends your change to the Server, the Server computes the minimal set of configuration the change needs, and the CLI runs Terraform/OpenTofu **locally** against your cloud. Your state lives on the Server; your cloud credentials never leave the machine where you run the CLI. ## Component Flow *Diagram: Stategraph component flow: the User runs the local CLI, which sends the change to the Server. The Server returns a minimal configuration bundle, and the CLI runs Terraform/OpenTofu locally against your cloud, then shows the result.* ## Components **User** — The engineer or service account initiating the change. Drives the workflow through the CLI. **CLI (local)** — `stategraph` is the command-line entrypoint you run on your machine or CI runner. It sends your configuration and the requested change to the Server, then **executes Terraform/OpenTofu locally** — pulling the minimal bundle the Server prepared, running it against your cloud with your local credentials, and reporting the outcome back. Authoritative state lives on the Server; execution and cloud credentials stay with the CLI. **Server** — The central coordinator. Stores state in PostgreSQL, records the transaction timeline, coordinates concurrent changes at the resource level (conflict detection at commit time), and computes the minimal, self-contained bundle of configuration each change needs. The Server **never runs Terraform and never sees your cloud credentials**. See [Deployment](/docs/deployment) for installation options. **Your Cloud** — Where Terraform/OpenTofu actually creates, updates, and destroys resources. It is reached by the CLI during execution, not by the Server. ## Flow 1. **Run a plan or apply** — The user invokes `stategraph tf plan` or `stategraph tf apply` from the CLI. 2. **Send the change** — The CLI sends the current configuration and the requested change to the Server. 3. **Return a minimal bundle** — The Server records the change as a transaction and computes the minimal, self-contained subset of configuration it needs (its dependencies and everything the change affects), then returns that bundle to the CLI. 4. **Run locally** — The CLI writes the bundle to a temporary directory and runs Terraform/OpenTofu locally against your cloud, then reports the outcome back to the Server, which records it in the transaction timeline. 5. **Show the result** — The CLI renders the outcome — diff, errors, or summary — back to the user. ## Related - [Deployment](/docs/deployment) — Run the Server on Docker Compose, Kubernetes, or ECS (the CLI runs wherever you invoke it) - [Velocity](/docs/velocity) — How the Server uses the dependency graph for parallel execution - [Transactions](/docs/velocity/transactions) — The transaction lifecycle that drives the flow above - [CLI Reference](/docs/cli) — Full command-line interface --- # Environment Variables Source: https://stategraph.com/docs/reference/environment-variables Complete reference of all Stategraph configuration options. Database, authentication, storage, and feature flag environment variables. Complete reference of all environment variables for configuring Stategraph. ## Required Variables These variables must be set for Stategraph to start. ### STATEGRAPH_UI_BASE **Required** Public URL where users access Stategraph. ```bash STATEGRAPH_UI_BASE=https://stategraph.example.com ``` Used for: - OAuth redirect URLs - Internal link generation - CORS configuration ### Database Configuration **All required** ```bash DB_HOST=postgres.example.com DB_USER=stategraph DB_PASS=your-secure-password DB_NAME=stategraph ``` | Variable | Description | |----------|-------------| | `DB_HOST` | PostgreSQL hostname | | `DB_USER` | Database username | | `DB_PASS` | Database password | | `DB_NAME` | Database name | --- ## Optional Variables ### Server Configuration #### STATEGRAPH_PORT Internal port the backend server listens on. In containerized deployments, nginx proxies from external port 8080 to this internal port. ```bash STATEGRAPH_PORT=8180 ``` **Default**: `8180` #### DB_PORT PostgreSQL port. ```bash DB_PORT=5432 ``` **Default**: `5432` (standard PostgreSQL port) #### DB_CONNECT_TIMEOUT Database connection timeout in seconds. ```bash DB_CONNECT_TIMEOUT=120 ``` **Default**: `120` #### DB_MAX_POOL_SIZE Maximum database connection pool size. ```bash DB_MAX_POOL_SIZE=100 ``` **Default**: `100` #### DB_IDLE_TX_TIMEOUT Idle transaction timeout. ```bash DB_IDLE_TX_TIMEOUT=180s ``` **Default**: `180s` #### STATEGRAPH_DB_STATEMENT_TIMEOUT Database statement timeout. ```bash STATEGRAPH_DB_STATEMENT_TIMEOUT=30s ``` **Default**: `30s` #### STATEGRAPH_TRANSACTION_SUBGRAPH_RETENTION_DAYS How many days a committed transaction's subgraph is retained before database garbage collection reclaims the space. ```bash STATEGRAPH_TRANSACTION_SUBGRAPH_RETENTION_DAYS=7 ``` **Default**: `7` --- ### Nginx Configuration #### STATEGRAPH_ACCESS_LOG Enable nginx access logging. ```bash STATEGRAPH_ACCESS_LOG=/dev/stdout # Enable STATEGRAPH_ACCESS_LOG=off # Disable ``` **Default**: `off` #### STATEGRAPH_CLIENT_MAX_BODY_SIZE Maximum request body size (for large state files). ```bash STATEGRAPH_CLIENT_MAX_BODY_SIZE=512m ``` **Default**: `512m` #### DISABLE_IPV6 Disable IPv6 in nginx. ```bash DISABLE_IPV6=1 # Disable DISABLE_IPV6=0 # Enable ``` **Default**: `0` (IPv6 enabled) --- ### CORS Configuration #### STATEGRAPH_ENABLE_CORS Enable CORS headers. ```bash STATEGRAPH_ENABLE_CORS=true ``` **Default**: `false` Only needed for development when UI runs on a different port. #### STATEGRAPH_CORS_DEFAULT_ORIGIN Default CORS origin. ```bash STATEGRAPH_CORS_DEFAULT_ORIGIN=http://localhost:3000 ``` **Default**: `http://localhost:3000` --- ## OAuth Configuration ### Basic OAuth #### STATEGRAPH_OAUTH_TYPE OAuth provider type. ```bash STATEGRAPH_OAUTH_TYPE=google # Google OAuth STATEGRAPH_OAUTH_TYPE=oidc # Generic OIDC ``` **Values**: `google`, `oidc` **Default**: Not set (OAuth disabled) #### STATEGRAPH_OAUTH_CLIENT_ID **Required when OAuth enabled** OAuth client ID from your provider. ```bash STATEGRAPH_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com ``` #### STATEGRAPH_OAUTH_CLIENT_SECRET **Required when OAuth enabled** OAuth client secret from your provider. ```bash STATEGRAPH_OAUTH_CLIENT_SECRET=your-client-secret ``` #### STATEGRAPH_OAUTH_COOKIE_SECRET Secret used to sign the OAuth session/CSRF cookies. Must be **16, 24, or 32 characters**. ```bash STATEGRAPH_OAUTH_COOKIE_SECRET=$(openssl rand -hex 16) # 32 chars ``` **Default**: a random value generated at startup. **Set this explicitly when running more than one replica.** If it is left unset, each replica generates its own secret, so a login whose callback is load-balanced to a different replica than it started on fails with `403 — invalid CSRF token`. Pin the same value on every replica (and keep it stable across restarts) to avoid this. Single-replica deployments work with the generated default, but pinning it also keeps users logged in across restarts. #### STATEGRAPH_OAUTH_EMAIL_DOMAIN Restrict access to specific email domain. ```bash STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com # Single domain STATEGRAPH_OAUTH_EMAIL_DOMAIN=* # All domains ``` **Default**: `*` (all domains allowed) #### STATEGRAPH_OAUTH_DISPLAY_NAME Provider name for login button (displayed as "Sign in with {name}"). ```bash STATEGRAPH_OAUTH_DISPLAY_NAME="Google" ``` **Default**: `Google` (for google) / `SSO` (for oidc) #### STATEGRAPH_OAUTH_REDIRECT_BASE Base URL for OAuth callbacks. **Important**: In production, you must set this to your public URL. ```bash STATEGRAPH_OAUTH_REDIRECT_BASE=https://stategraph.example.com ``` **Default**: `http://localhost:{STATEGRAPH_PORT}` **Warning**: If not explicitly set, OAuth redirects will use localhost, which will fail in production environments. --- ### Google-Specific OAuth #### STATEGRAPH_OAUTH_GOOGLE_GROUP Google Group email for access restriction. ```bash STATEGRAPH_OAUTH_GOOGLE_GROUP=stategraph-users@yourcompany.com ``` Requires service account configuration. #### STATEGRAPH_OAUTH_GOOGLE_ADMIN_EMAIL Admin email for Google Groups API access. ```bash STATEGRAPH_OAUTH_GOOGLE_ADMIN_EMAIL=admin@yourcompany.com ``` Must be a Google Workspace super admin. #### STATEGRAPH_OAUTH_GOOGLE_SERVICE_ACCOUNT_JSON Service account JSON key for Google Admin API. ```bash STATEGRAPH_OAUTH_GOOGLE_SERVICE_ACCOUNT_JSON='{"type":"service_account",...}' ``` Required for Google Groups integration. --- ### OIDC-Specific OAuth #### STATEGRAPH_OAUTH_OIDC_ISSUER_URL **Required when `STATEGRAPH_OAUTH_TYPE=oidc`** OIDC provider issuer URL. ```bash STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://your-provider.com ``` The provider must support OIDC discovery at `{issuer}/.well-known/openid-configuration`. --- ### Internal OAuth Configuration #### STATEGRAPH_OAUTH2_API_KEY API key for internal session storage. ```bash STATEGRAPH_OAUTH2_API_KEY=your-random-key ``` **Default**: Auto-generated random key #### STATEGRAPH_OAUTH2_PROXY_PATH Path to oauth2-proxy binary (Docker internal use). ```bash STATEGRAPH_OAUTH2_PROXY_PATH=/usr/local/bin/oauth2-proxy ``` **Default**: `/usr/local/bin/oauth2-proxy` --- ## Complete Example ### Development ```bash # Database DB_HOST=localhost DB_PORT=5432 DB_USER=stategraph DB_PASS=stategraph DB_NAME=stategraph # Server STATEGRAPH_UI_BASE=http://localhost:8080 STATEGRAPH_PORT=8080 # Development CORS (for separate UI server) STATEGRAPH_ENABLE_CORS=true STATEGRAPH_CORS_DEFAULT_ORIGIN=http://localhost:3000 ``` ### Production without OAuth ```bash # Database DB_HOST=postgres.internal.example.com DB_PORT=5432 DB_USER=stategraph DB_PASS=${DB_PASSWORD} # From secrets manager DB_NAME=stategraph # Server STATEGRAPH_UI_BASE=https://stategraph.example.com STATEGRAPH_PORT=8080 # Logging STATEGRAPH_ACCESS_LOG=/dev/stdout ``` ### Production with Google OAuth ```bash # Database DB_HOST=postgres.internal.example.com DB_PORT=5432 DB_USER=stategraph DB_PASS=${DB_PASSWORD} DB_NAME=stategraph # Server STATEGRAPH_UI_BASE=https://stategraph.example.com STATEGRAPH_PORT=8080 # OAuth STATEGRAPH_OAUTH_TYPE=google STATEGRAPH_OAUTH_CLIENT_ID=${GOOGLE_CLIENT_ID} STATEGRAPH_OAUTH_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com STATEGRAPH_OAUTH_REDIRECT_BASE=https://stategraph.example.com # Optional: Google Groups STATEGRAPH_OAUTH_GOOGLE_GROUP=stategraph-users@yourcompany.com STATEGRAPH_OAUTH_GOOGLE_ADMIN_EMAIL=admin@yourcompany.com STATEGRAPH_OAUTH_GOOGLE_SERVICE_ACCOUNT_JSON=${GOOGLE_SERVICE_ACCOUNT} # Logging STATEGRAPH_ACCESS_LOG=/dev/stdout ``` ### Production with OIDC ```bash # Database DB_HOST=postgres.internal.example.com DB_PORT=5432 DB_USER=stategraph DB_PASS=${DB_PASSWORD} DB_NAME=stategraph # Server STATEGRAPH_UI_BASE=https://stategraph.example.com STATEGRAPH_PORT=8080 # OAuth STATEGRAPH_OAUTH_TYPE=oidc STATEGRAPH_OAUTH_OIDC_ISSUER_URL=https://your-okta.okta.com STATEGRAPH_OAUTH_CLIENT_ID=${OIDC_CLIENT_ID} STATEGRAPH_OAUTH_CLIENT_SECRET=${OIDC_CLIENT_SECRET} STATEGRAPH_OAUTH_EMAIL_DOMAIN=yourcompany.com STATEGRAPH_OAUTH_REDIRECT_BASE=https://stategraph.example.com # Logging STATEGRAPH_ACCESS_LOG=/dev/stdout ``` --- ## Gap Analysis Configuration Environment variables for configuring gap analysis behavior. ### GCP Configuration #### GOOGLE_CLOUD_PROJECT GCP project ID for gap analysis scope. ```bash GOOGLE_CLOUD_PROJECT=my-project-id ``` **Default**: Auto-detected from Application Default Credentials #### GOOGLE_CLOUD_FOLDER GCP folder ID for broader gap analysis scope (covers all projects in folder). ```bash GOOGLE_CLOUD_FOLDER=123456789 ``` **Default**: Not set #### GOOGLE_CLOUD_ORGANIZATION GCP organization ID for full gap analysis visibility. ```bash GOOGLE_CLOUD_ORGANIZATION=123456789 ``` **Default**: Not set ### AWS Configuration #### AWS_DEFAULT_REGION AWS region for Resource Explorer queries. ```bash AWS_DEFAULT_REGION=us-east-1 ``` **Default**: From AWS credentials/config ### Cache Configuration #### GAP_ANALYSIS_CACHE_TTL Gap analysis cache time-to-live in seconds. ```bash GAP_ANALYSIS_CACHE_TTL=10800 # 3 hours ``` **Default**: `10800` (3 hours) #### SG_GAP_ANALYSIS_CACHE Directory for gap analysis cache files. ```bash SG_GAP_ANALYSIS_CACHE=/var/cache/stategraph/gap-analysis ``` **Default**: `/var/cache/stategraph/gap-analysis` --- ## Cost Analysis Configuration Environment variables for [Cost Analysis](/docs/cost). Cost analysis is disabled until `STATEGRAPH_COST_ENABLED` is set to `true`. #### STATEGRAPH_COST_ENABLED Master on/off switch for cost analysis. Set it to `true` to turn cost on; leave it unset (or `false`) and cost analysis stays off — `POST /costs/calculate` returns `503` and `capabilities.costs.enabled` is `false`. Because it lives in your deployment environment, cost analysis survives redeploys, restarts, and reschedules. ```bash STATEGRAPH_COST_ENABLED=true ``` **Default**: `false` (cost analysis disabled) #### STATEGRAPH_PRICING_SERVICE_URL Endpoint of the pricing service used to estimate costs. Defaults to `http://localhost:8090`, the pricing service bundled in the all-in-one image, so most deployments leave it unset. Set it only to point the server at an **external** pricing service. ```bash STATEGRAPH_PRICING_SERVICE_URL=https://pricing.internal.example.com # external service ``` **Default**: `http://localhost:8090` (bundled in-image pricing service) #### PRICING_REFRESH_HOURS How often the pricing service reloads the cloud price book into the `cloud_pricing` database. Set to `0` to disable automatic refresh. ```bash PRICING_REFRESH_HOURS=168 ``` **Default**: `168` (weekly) #### STATEGRAPH_PRICING_DEFAULT_REGION Region assumed when a resource does not specify one. ```bash STATEGRAPH_PRICING_DEFAULT_REGION=us-east-1 ``` **Default**: `us-east-1` #### STATEGRAPH_COST_SCHEDULE_HOURS How often cost snapshots are recomputed on a schedule, in hours. ```bash STATEGRAPH_COST_SCHEDULE_HOURS=24 ``` **Default**: `24` #### STATEGRAPH_COST_EVENT_DEBOUNCE_HOURS Minimum number of hours before an apply triggers another recompute. ```bash STATEGRAPH_COST_EVENT_DEBOUNCE_HOURS=6 ``` **Default**: `6` #### STATEGRAPH_COST_PRICING_CALL_TIMEOUT_SECONDS Timeout for a single pricing-service call, in seconds. ```bash STATEGRAPH_COST_PRICING_CALL_TIMEOUT_SECONDS=30 ``` **Default**: `30` #### Price-book database and loader The pricing service loads the cloud price book into the `cloud_pricing` database on first boot and refreshes it on the `PRICING_REFRESH_HOURS` cadence. Override where the price book lives, or mirror its download source, with `PRICING_DB_HOST` / `PRICING_DB_PORT`, `PRICING_DB_USER` / `PRICING_DB_PASSWORD`, `PRICING_DB_NAME` (default `cloud_pricing`), and `PRICING_DATA_URL`. These default to the bundled database and Stategraph's hosted price book, so most deployments leave them unset. See [Cost Setup](/docs/cost/setup#configuration) for the full price-book reference and air-gapped installs. #### STATEGRAPH_COSTS_WAIT_SECONDS Client-side (CLI) variable — not a server setting. Sets the deployment-level default for the `--costs-wait` flag on [`stategraph tf plan` and `stategraph tf apply`](/docs/cli/tf), the number of seconds the CLI waits for the cost delta to become available. Unlike the flag, this variable may be set alongside `--skip-costs`; when both apply, `--skip-costs` takes precedence. ```bash STATEGRAPH_COSTS_WAIT_SECONDS=3 ``` **Default**: `3` --- ## Variable Reference Table | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `STATEGRAPH_UI_BASE` | Yes | - | Public URL | | `DB_HOST` | Yes | - | PostgreSQL host | | `DB_USER` | Yes | - | Database user | | `DB_PASS` | Yes | - | Database password | | `DB_NAME` | Yes | - | Database name | | `DB_PORT` | No | `5432` | Database port | | `DB_CONNECT_TIMEOUT` | No | `120` | Connection timeout | | `DB_MAX_POOL_SIZE` | No | `100` | Max connections | | `DB_IDLE_TX_TIMEOUT` | No | `180s` | Idle transaction timeout | | `STATEGRAPH_PORT` | No | `8180` | Internal server port | | `STATEGRAPH_DB_STATEMENT_TIMEOUT` | No | `30s` | Query timeout | | `STATEGRAPH_TRANSACTION_SUBGRAPH_RETENTION_DAYS` | No | `7` | Days a committed transaction's subgraph is retained before database garbage collection | | `STATEGRAPH_ACCESS_LOG` | No | `off` | Access logging | | `STATEGRAPH_CLIENT_MAX_BODY_SIZE` | No | `512m` | Max request size | | `DISABLE_IPV6` | No | `0` | Disable IPv6 | | `STATEGRAPH_ENABLE_CORS` | No | `false` | Enable CORS | | `STATEGRAPH_CORS_DEFAULT_ORIGIN` | No | `http://localhost:3000` | CORS origin | | `STATEGRAPH_OAUTH_TYPE` | No | - | OAuth provider | | `STATEGRAPH_OAUTH_CLIENT_ID` | If OAuth | - | OAuth client ID | | `STATEGRAPH_OAUTH_CLIENT_SECRET` | If OAuth | - | OAuth client secret | | `STATEGRAPH_OAUTH_EMAIL_DOMAIN` | No | `*` | Email domain filter | | `STATEGRAPH_OAUTH_DISPLAY_NAME` | No | `Google` / `SSO` | Provider name for login button | | `STATEGRAPH_OAUTH_REDIRECT_BASE` | No | `http://localhost:{port}` | OAuth callback base (set in production!) | | `STATEGRAPH_OAUTH_OIDC_ISSUER_URL` | If OIDC | - | OIDC issuer URL | | `STATEGRAPH_OAUTH_GOOGLE_GROUP` | No | - | Google Group email | | `STATEGRAPH_OAUTH_GOOGLE_ADMIN_EMAIL` | If Group | - | Admin email | | `STATEGRAPH_OAUTH_GOOGLE_SERVICE_ACCOUNT_JSON` | If Group | - | Service account JSON | | `GOOGLE_CLOUD_PROJECT` | No | Auto-detect | GCP project for gap analysis | | `GOOGLE_CLOUD_FOLDER` | No | - | GCP folder for gap analysis | | `GOOGLE_CLOUD_ORGANIZATION` | No | - | GCP organization for gap analysis | | `AWS_DEFAULT_REGION` | No | From config | AWS region for Resource Explorer | | `GAP_ANALYSIS_CACHE_TTL` | No | `10800` | Gap analysis cache TTL (seconds) | | `SG_GAP_ANALYSIS_CACHE` | No | `/var/cache/stategraph/gap-analysis` | Gap analysis cache directory | | `STATEGRAPH_COST_ENABLED` | For cost | `false` | Master on/off switch for cost analysis (set `true` to enable) | | `STATEGRAPH_PRICING_SERVICE_URL` | No | `http://localhost:8090` | Pricing service endpoint; defaults to the bundled in-image service, set only for an external one | | `PRICING_REFRESH_HOURS` | No | `168` | Price-book refresh cadence in hours (`0` disables) | | `STATEGRAPH_PRICING_DEFAULT_REGION` | No | `us-east-1` | Default pricing region | | `STATEGRAPH_COST_SCHEDULE_HOURS` | No | `24` | Cost snapshot schedule (hours) | | `STATEGRAPH_COST_EVENT_DEBOUNCE_HOURS` | No | `6` | Debounce before apply-triggered recompute (hours) | | `STATEGRAPH_COST_PRICING_CALL_TIMEOUT_SECONDS` | No | `30` | Pricing call timeout (seconds) | | `STATEGRAPH_COSTS_WAIT_SECONDS` | No | `3` | Client-side default for the `--costs-wait` flag on `tf plan`/`tf apply` (seconds); `--skip-costs` takes precedence | --- # Health Checks Source: https://stategraph.com/docs/reference/health-checks Configuring liveness and readiness probes for load balancers and container orchestrators. Health endpoints for Kubernetes, ECS, and other platforms. Stategraph provides two health check endpoints for deployment environments like ECS/ALB, Kubernetes, and other container orchestrators. ## Endpoints ### Liveness Probe ``` GET /health/live ``` Returns `200 OK` as long as the nginx process is running. This endpoint is served directly by nginx and does **not** depend on the backend application or database. Use this for: - ALB target group health checks - ECS container health checks - Kubernetes liveness probes ### Readiness Probe ``` GET /health/ready ``` Returns `200 OK` only when the backend application is running and ready to serve requests. This endpoint is proxied by nginx to the backend and will return a non-200 status if the backend has not started yet (e.g., during database migrations). Use this for: - Kubernetes readiness probes - Load balancer routing decisions (only send traffic to ready instances) - Monitoring systems ### Legacy Endpoint ``` GET /api/v1/health ``` This endpoint is served by the backend application and behaves the same as `/health/ready` — it returns `200 OK` only when the backend is running. It is supported for backwards compatibility but new deployments should use `/health/live` and `/health/ready`. ## Startup Behavior During container startup, Stategraph runs database migrations before starting the backend HTTP server. The timeline looks like this: ``` t=0 Container starts t=1 nginx starts listening on port 8080 t=1 /health/live returns 200 t=2 Database migrations begin t=5+ Migrations complete, backend starts t=5+ /health/ready returns 200 ``` During the migration window: - `/health/live` returns **200** (nginx is up) - `/health/ready` returns **502** (backend not yet listening) After migrations complete: - `/health/live` returns **200** - `/health/ready` returns **200** ## ALB / ECS Configuration ### Recommended Settings | Setting | Value | Reason | |---------|-------|--------| | Health check path | `/health/live` | Available immediately, survives migration window | | Health check interval | 30s | Standard interval | | Healthy threshold | 2 | Two consecutive successes | | Unhealthy threshold | 5 | Tolerant during startup | | Health check grace period | 120s | Allow time for migrations on first deploy | ### ECS Task Definition ```json { "healthCheck": { "command": ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"], "interval": 30, "timeout": 5, "retries": 5, "startPeriod": 120 } } ``` The `startPeriod` of 120 seconds gives migrations time to complete before ECS starts checking health. ### ALB Target Group ```hcl resource "aws_lb_target_group" "stategraph" { # ... health_check { path = "/health/live" interval = 30 timeout = 5 healthy_threshold = 2 unhealthy_threshold = 5 } } ``` ### Verifying Readiness Before Routing Traffic If you want to ensure the backend is fully ready before routing traffic, use `/health/ready` as the ALB health check path instead. Set a longer grace period (180s) to account for migration time: ```hcl health_check { path = "/health/ready" interval = 30 timeout = 5 healthy_threshold = 2 unhealthy_threshold = 10 } ``` ## Kubernetes Configuration ```yaml livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 10 failureThreshold: 10 ``` --- # SQL Syntax Reference Source: https://stategraph.com/docs/reference/sql-syntax Complete grammar reference for the SQL query language. Syntax, operators, functions, and query patterns for infrastructure exploration. Complete grammar reference for Stategraph SQL (Structured Query Language). ## Grammar Overview Stategraph supports a subset of SQL for querying Terraform state data. ``` query := [cte] SELECT select_list FROM table [join ...] [WHERE expr] [GROUP BY exprs] [HAVING expr] [ORDER BY order_exprs] [LIMIT n] [UNION query] join := [INNER | LEFT | RIGHT] JOIN table ON expr ``` **Every query requires a `FROM` clause.** There is no constant-only form — `SELECT 1` (with no table) is a parse error. Always select from one of the [available tables](#available-tables), even when computing scalar values. ## SELECT Statement ### Basic Syntax ```sql SELECT columns FROM table ``` ### Select All Columns ```sql SELECT * FROM instances ``` ### Select Specific Columns ```sql SELECT address, type, provider FROM resources ``` ### Column Aliases ```sql SELECT address AS resource_address, type AS resource_type FROM resources ``` ## FROM Clause ### Single Table ```sql SELECT * FROM instances ``` ### Table Aliases Alias a table with `AS`, then qualify columns with the alias: ```sql SELECT r.type, r.provider FROM resources AS r ``` ### JOINs Combine tables with `INNER JOIN`, `LEFT JOIN`, or `RIGHT JOIN`. Use an `ON` clause to match rows: ```sql SELECT i.address, r.type FROM instances AS i INNER JOIN resources AS r ON i.resource_address = r.address AND i.state_id = r.state_id WHERE r.type = 'aws_instance' ``` Multiple joins in a single query are supported. ### Available Tables | Table | Description | |-------|-------------| | `instances` | Resource instances (attributes, dependencies, state) | | `resources` | Resource definitions (type, provider, module) | | `states` | Terraform states | | `providers` | Provider information | | `outputs` | State outputs | | `transactions` | Transaction history | | `transaction_logs` | Transaction log entries | | `tenants` | Tenant information | | `users` | User information | | `check_results` | Check results | | `check_entries` | Check entries | | `cost_snapshots` | Cost snapshot summaries (see [Cost Analysis](/docs/cost)) | | `cost_snapshot_resources` | Per-resource cost breakdown | | `hcl`, `hcl_refs` | Stored HCL blocks and the references between them | | `files`, `tfvars` | Data files and variable definitions captured for a state | | `transaction_subgraphs`, `revision_hashes`, `revision_tx_hashes` | Transaction/revision bookkeeping | Run `stategraph sql schema` (or see [Query (SQL)](/docs/inventory/query)) for the authoritative list of tables and columns. ## WHERE Clause ### Basic Filtering ```sql SELECT * FROM resources WHERE type = 'aws_instance' ``` ### Multiple Conditions ```sql SELECT * FROM resources WHERE type = 'aws_instance' AND provider LIKE '%hashicorp/aws%' ``` ## Expressions ### Literals | Type | Examples | |------|----------| | String | `'hello'`, `'aws_instance'` | | Integer | `42`, `-1`, `0` | | Float | `3.14`, `-0.5` | | Boolean | `true`, `false` | | Null | `null` | ### Identifiers Column and table names: ```sql address type instances.address ``` ### Operators #### Comparison Operators | Operator | Description | Example | |----------|-------------|---------| | `=` | Equal | `type = 'aws_instance'` | | `<>` | Not equal | `type <> 'null_resource'` | | `<` | Less than | `count < 10` | | `>` | Greater than | `count > 100` | | `<=` | Less or equal | `count <= 50` | | `>=` | Greater or equal | `count >= 5` | #### Logical Operators | Operator | Description | Example | |----------|-------------|---------| | `AND` | Logical AND | `a = 1 AND b = 2` | | `OR` | Logical OR | `a = 1 OR b = 2` | | `NOT` | Logical NOT | `NOT a = 1` | #### Arithmetic Operators | Operator | Description | Example | |----------|-------------|---------| | `+` | Addition | `a + b` | | `-` | Subtraction | `a - b` | | `*` | Multiplication | `a * b` | | `/` | Division | `a / b` | | `-` (unary) | Negation | `-a` | #### String Operators | Operator | Description | Example | |----------|-------------|---------| | `\|\|` | Concatenation | `'hello' \|\| ' world'` | ### NULL Handling | Expression | Description | |------------|-------------| | `IS NULL` | Value is null | | `IS NOT NULL` | Value is not null | | `IS DISTINCT FROM` | NULL-safe not equal | | `IS NOT DISTINCT FROM` | NULL-safe equal | ```sql SELECT * FROM resources WHERE module IS NULL SELECT * FROM resources WHERE module IS NOT NULL ``` ### Pattern Matching #### IN ```sql SELECT * FROM resources WHERE type IN ('aws_instance', 'aws_ebs_volume', 'aws_eip') ``` #### LIKE / ILIKE `LIKE` matches a pattern case-sensitively; `ILIKE` is case-insensitive. Use `%` for any sequence of characters and `_` for a single character. Both support a negated form (`NOT LIKE`, `NOT ILIKE`). ```sql -- Resource types starting with "aws_" SELECT * FROM resources WHERE type LIKE 'aws_%' -- Case-insensitive substring match SELECT * FROM resources WHERE name ILIKE '%prod%' -- Negated match SELECT * FROM resources WHERE type NOT LIKE 'null_%' ``` ### Type Casting ```sql SELECT state_id::text FROM instances SELECT (attributes->>'volume_size')::integer FROM instances ``` Syntax: `expression::type`. Allowed target types: `text`, `integer`, `bigint`, `smallint`, `real`, `boolean`/`bool`, `jsonb`, `timestamptz`, `uuid`. ## JSON Operators Access JSON data stored in the `attributes` column. ### Arrow Operators | Operator | Description | Returns | |----------|-------------|---------| | `->` | Get JSON value | JSON | | `->>` | Get JSON as text | Text | | `#>>` | Get nested path as text | Text | ### Examples ```sql -- Get JSON value SELECT attributes->'tags' FROM instances -- Get as text SELECT attributes->>'instance_type' FROM instances -- Nested path SELECT attributes#>>'{tags,Name}' FROM instances ``` ### JSON Contains ```sql -- Check if JSON contains value (the RHS JSON literal must be cast to ::jsonb) SELECT * FROM instances WHERE attributes->'tags' @> '{"Environment": "production"}'::jsonb ``` ## Aggregation ### COUNT ```sql SELECT COUNT(*) FROM instances SELECT COUNT(*) FROM resources ``` ### GROUP BY `count` is a reserved keyword, so an aggregate cannot be aliased `AS count`. Use a non-reserved alias such as `total`: ```sql SELECT type, COUNT(*) AS total FROM resources GROUP BY type ``` ### HAVING ```sql SELECT type, COUNT(*) AS total FROM resources GROUP BY type HAVING COUNT(*) > 10 ``` ## Ordering ### ORDER BY ```sql SELECT * FROM resources ORDER BY type SELECT * FROM resources ORDER BY type ASC SELECT * FROM resources ORDER BY type DESC ``` ### Multiple Columns ```sql SELECT * FROM resources ORDER BY type ASC, address DESC ``` ## Limiting Results ### LIMIT ```sql SELECT * FROM instances LIMIT 100 ``` A query with no `LIMIT` of its own is capped at a default of **20 rows**, and an explicit `LIMIT` is capped at a maximum of **1000 rows**. Always add an explicit `LIMIT` when you need a predictable result size. Over the API, a result truncated by the default limit is flagged with an `mql-default-limit-applied` response header (see [API Reference](/docs/reference/api)). ## Common Table Expressions (CTEs) ### WITH Clause ```sql WITH aws_resources AS ( SELECT * FROM resources WHERE provider LIKE '%hashicorp/aws%' ) SELECT type, COUNT(*) FROM aws_resources GROUP BY type ``` ### Multiple CTEs ```sql WITH aws AS (SELECT * FROM resources WHERE provider LIKE '%hashicorp/aws%'), gcp AS (SELECT * FROM resources WHERE provider LIKE '%hashicorp/google%') SELECT 'aws' AS cloud, COUNT(*) FROM aws UNION SELECT 'gcp' AS cloud, COUNT(*) FROM gcp ``` ## Functions ### Built-in Functions **Note**: Function names are case-sensitive and must be lowercase. | Function | Description | Example | |----------|-------------|---------| | `count(*)` | Count rows | `count(*)` | | `sum(expr)` | Sum values | `sum((attributes->>'volume_size')::integer)` | | `avg(expr)` | Average values | `avg((attributes->>'volume_size')::integer)` | | `nullif(a, b)` | Return null if a = b | `nullif(value, 0)` | | `to_char(expr, fmt)` | Format as string | `to_char(created_at, 'YYYY-MM-DD')` | | `json_build_object(...)` | Build JSON object | `json_build_object('key', value)` | `count` is a reserved keyword, so only `count(*)` is valid — you cannot write `count(some_column)` or `count(DISTINCT ...)`. Many other PostgreSQL functions are available, including `coalesce`, `min`, `max`, `lower`, `upper`, `length`, `trim`, `substr`, `replace`, `split_part`, `regexp_replace`, `round`, `floor`, `ceil`, `string_agg`, `array_agg`, `json_agg`, `jsonb_build_object`, `array_length`, `now`, `date_trunc`, `date_part`, and `age`. **Note**: Integer literals bind as `bigint`, so numeric position and length arguments to functions like `substr`, `split_part`, and `array_length` may need an explicit `::integer` cast — for example `substr(name, 1::integer, 3::integer)` or `array_length(dependencies, 1::integer)`. ## Operator Precedence From highest (binds tightest) to lowest: 1. `.` (field access), `[]` (index) 2. `::` (type cast) 3. `-` (unary negation) 4. `->`, `->>`, `#>>` (JSON) 5. `\|\|` (concatenation) 6. `*`, `/` 7. `+`, `-` 8. `=`, `<>`, `<`, `>`, `<=`, `>=`, `@>`, `IS`, `IS NOT`, `IS DISTINCT FROM`, `IS NOT DISTINCT FROM`, `IN`, `LIKE`, `ILIKE`, `NOT` 9. `AND` 10. `OR` (Comparison, JSON-contains `@>`, `IS`/`IS DISTINCT`, `IN`, `LIKE`/`ILIKE`, and `NOT` all share one level.) When in doubt, use parentheses. Use parentheses to override precedence: ```sql SELECT * FROM instances WHERE (a = 1 OR b = 2) AND c = 3 ``` ## Unsupported Constructs Stategraph SQL is a subset of standard SQL. The following constructs are **not** supported and return an error: - `CASE ... WHEN ... THEN ... END` expressions - `DISTINCT`, including `COUNT(DISTINCT ...)` - `OFFSET` - `NOT IN` - Derived tables (subqueries in the `FROM` clause) - Qualified star (`t.*`) — list the columns explicitly Subqueries are supported in `EXISTS (...)` and `IN (...)`, and `UNION` / `UNION ALL`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`, and CTEs (including `MATERIALIZED`) all work. ## Reserved Keywords ``` ALL, AND, AS, ASC, BY, COUNT, DESC, EXISTS, FALSE, FROM, GROUP, HAVING, ILIKE, IN, INNER, IS, JOIN, LEFT, LIKE, LIMIT, MATERIALIZED, NOT, NULL, ON, OR, ORDER, RIGHT, SELECT, TRUE, UNION, UNNEST, WHERE, WITH ``` Keywords are case-insensitive. ## Comments Stategraph SQL does not currently support comments in queries. ## Query Examples ### Basic Queries ```sql -- All instances SELECT * FROM instances -- All resources SELECT * FROM resources -- Specific type (use resources table) SELECT * FROM resources WHERE type = 'aws_instance' -- Multiple conditions SELECT * FROM resources WHERE type = 'aws_instance' AND module IS NOT NULL ``` ### Aggregations ```sql -- Count by type (order by the aggregate expression, not the alias) SELECT type, COUNT(*) AS total FROM resources GROUP BY type ORDER BY COUNT(*) DESC -- With minimum count SELECT type, COUNT(*) AS total FROM resources GROUP BY type HAVING COUNT(*) > 5 ORDER BY COUNT(*) DESC ``` ### JSON Queries ```sql -- Get specific attribute SELECT address, attributes->>'instance_type' AS instance_type FROM instances -- Filter by attribute SELECT address FROM instances WHERE attributes->>'instance_type' = 't3.micro' -- Check tags SELECT address FROM instances WHERE attributes->'tags'->>'Environment' = 'production' ``` ### Complex Queries ```sql -- Resources by type with counts SELECT type, COUNT(*) AS total FROM resources WHERE provider LIKE '%hashicorp/aws%' GROUP BY type ORDER BY COUNT(*) DESC ``` ## Error Handling ### Syntax Errors Invalid syntax returns an error with position information: ``` Error at line 1, column 15: Expected 'FROM' ``` ### Type Errors Type mismatches return descriptive errors: ``` Error: Cannot compare string with integer ``` ### Unknown Columns References to non-existent columns: ``` Error: Unknown column 'foo' ``` --- # Releases & Downloads Source: https://stategraph.com/docs/reference/releases Download Stategraph server and CLI binaries. Latest releases, version history, and installation instructions for all platforms. All Stategraph releases are published on GitHub. Visit the [releases page](https://github.com/stategraph/releases/releases) for the latest version. ## CLI Installation ### Quick install (Linux and macOS) The fastest way to install the Stategraph CLI. The script detects your platform, downloads the latest release, and installs it to `/usr/local/bin`: ```bash curl -fsSL https://get.stategraph.com/install.sh | sh ``` Pass `--no-sudo` to install without `sudo` (for example, to a directory you own): ```bash curl -fsSL https://get.stategraph.com/install.sh | sh -s -- --no-sudo ``` ### Homebrew (macOS) ```bash brew tap stategraph/stategraph brew install stategraph ``` Upgrade with `brew upgrade stategraph`. ### Debian / Ubuntu (apt) ```bash curl -fsSL https://stategraph.github.io/releases/apt/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/stategraph-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/stategraph-archive-keyring.gpg] https://stategraph.github.io/releases/apt stable main" | sudo tee /etc/apt/sources.list.d/stategraph.list > /dev/null sudo apt-get update && sudo apt-get install -y stategraph ``` ### RHEL / Fedora / CentOS (yum) ```bash sudo rpm --import https://stategraph.github.io/releases/yum/KEY.gpg sudo tee /etc/yum.repos.d/stategraph.repo <<'EOF' [stategraph] name=Stategraph baseurl=https://stategraph.github.io/releases/yum/$basearch enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=https://stategraph.github.io/releases/yum/KEY.gpg EOF sudo yum install -y stategraph ``` The apt and yum repositories always resolve to the latest release, so `apt-get upgrade` / `yum update` keeps the CLI current. ## Docker Images The CLI image is publicly available on GitHub Container Registry. The server image is distributed privately — see [Server](#server) below for access. ### Server The server image includes the API backend and web console. It's distributed privately so we can make sure every team has the support and onboarding they need to run Stategraph well. [Get in touch through our contact page](https://stategraph.com/contact) and we'll get you set up with access and aligned on your deployment. Once you have access, the server image powers: - [Docker Compose deployments](/docs/deployment/docker-compose) - [Kubernetes deployments](/docs/deployment/kubernetes) ### CLI The CLI image contains the `stategraph` command-line tool. ```bash docker pull ghcr.io/stategraph/stategraph: ``` See [CLI documentation](/docs/cli) for usage. ## Binary Downloads For air-gapped environments or manual installs, native binaries are available for direct download from the [releases page](https://github.com/stategraph/releases/releases). For most users the [quick install script](#quick-install-linux-and-macos) is simpler. | Platform | Architecture | File | |----------|--------------|------| | Linux | x86_64 | `stategraph--linux-amd64.tar.gz` | | Linux | ARM64 | `stategraph--linux-arm64.tar.gz` | | macOS | Apple Silicon | `stategraph--macos-arm64.tar.gz` | | macOS | Intel | `stategraph--macos-amd64.tar.gz` | ### Installation This resolves the latest release automatically, so there is no version to keep up to date: ```bash # Resolve the latest version VERSION=$(curl -s https://api.github.com/repos/stategraph/releases/releases/latest \ | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4) # Download and extract (Linux x86_64 shown — swap the suffix for your platform) curl -LO https://github.com/stategraph/releases/releases/download/$VERSION/stategraph-$VERSION-linux-amd64.tar.gz tar xzf stategraph-$VERSION-linux-amd64.tar.gz # Move to PATH and verify sudo mv stategraph /usr/local/bin/ stategraph --help ``` ## API Schema Each release includes the OpenAPI schema (`api.json`) as a downloadable artifact. This can be used for code generation, API client creation, or integration with tools that consume OpenAPI specifications. The schema is also available at runtime via the [`GET /api/v1/openapi`](/docs/reference/api#get-openapi-schema) endpoint. ## Version Scheme Stategraph follows [semantic versioning](https://semver.org/): - **Major** (X.0.0) - Breaking changes - **Minor** (0.X.0) - New features, backwards compatible - **Patch** (0.0.X) - Bug fixes ## Release Notes See the [GitHub releases page](https://github.com/stategraph/releases/releases) for detailed release notes and changelogs. --- # Glossary Source: https://stategraph.com/docs/reference/glossary Definitions of key Stategraph terms and how they map to Terraform concepts. State, tenant, transaction, workspace, and infrastructure terminology. Definitions of key terms used throughout Stategraph. ## Terms ### Tenant An organizational boundary in Stategraph. All states, transactions, and users belong to a tenant. A tenant is similar to a team or organization — it keeps resources isolated between groups. Every API operation that creates or queries states requires a tenant ID. ### State A Terraform state file tracked by Stategraph. Each state belongs to a tenant and is uniquely identified by the combination of its group ID and workspace. States contain the resource inventory that Terraform uses to map real infrastructure to configuration. ### Group ID A UUID that identifies a state in Stategraph. Multiple workspaces can share the same group ID — Stategraph uses the pair of `group_id + workspace` to locate the correct state. ### Workspace Maps to [Terraform workspaces](https://developer.hashicorp.com/terraform/language/state/workspaces). Defaults to `"default"` if not specified. Multiple workspaces can share the same group ID, allowing you to manage environment variants (e.g., staging, production) under a single logical grouping. ### State ID An internal UUID that uniquely identifies a state record. Used in API calls such as listing resource instances or viewing state details. This is distinct from the group ID — the state ID is Stategraph's primary key, while the group ID is used for Terraform backend routing. ### Transaction A recorded change to state. Each `terraform apply` that modifies state creates a transaction. Transactions provide an audit trail of infrastructure changes, including who made the change and when. Transactions can be tagged with metadata and move through a lifecycle: `open` → `previewing` → `committing` → `committed`, or end in `failed` or `aborted`. ### Orphaned Candidate A Terraform resource flagged by Stategraph's graph analysis as potentially unused. A resource is considered an orphaned candidate when **both** of the following are true: (1) no other resource depends on it (leaf node), and (2) it is either outside any module or has at most one dependency. Resources inside a module with multiple dependencies are excluded because modules typically bundle related resources intentionally. Not every flagged resource is actually orphaned — review each one to decide whether to remove it or leave it in place. ### Blast Radius The set of resources affected when a given resource changes or is destroyed — every resource that transitively depends on it. Stategraph computes blast radius from the dependency graph so you can assess the impact of a change before applying it. It is available in the UI, via the `stategraph states instances blast-radius` CLI command, and through the API. See [Blast Radius Analysis](/docs/insights/blast-radius). ### Gap Analysis A comparison between the resources Stategraph manages in state and the resources that actually exist in your cloud account, surfacing unmanaged resources and coverage gaps. See [Gap Analysis](/docs/inventory/gap-analysis). ### API Key A bearer token used to authenticate CLI and programmatic API requests. API keys are created in the Settings page of the Stategraph UI. ### Billing Source (FOCUS) A connection to your cloud provider's [FOCUS](https://focus.finops.org/) cost-and-usage export (actual billed spend), ingested by Stategraph so it can attribute real costs to managed resources. Billing sources cover **actual** spend, distinct from the estimated costs Stategraph derives from your Terraform state. Configure them with `stategraph cost billing-source`. See [Billing Sources](/docs/cost/billing-sources). ## Mapping to Terraform and HCP Terraform | Stategraph | Terraform / HCP Terraform | Notes | |------------|---------------------------|-------| | Tenant | HCP Organization | Organizational boundary for resources | | State | State file | A tracked `.tfstate` | | Group ID | — | Stategraph-specific; identifies a state | | Workspace | Workspace | Same concept — isolates state for different environments | | Transaction | — | Stategraph-specific; audit log entry for each state change | | API Key | HCP API Token / Team Token | Authentication credential for programmatic access | --- # Terraform CLI Compatibility Source: https://stategraph.com/docs/reference/terraform-compatibility How Terraform CLI commands and HCL constructs map to Stategraph — what works directly, what the transaction model subsumes, and what has no analog. Stategraph is a state management and visualization layer that drives Terraform or OpenTofu for plan and apply. Some CLI commands have direct Stategraph equivalents, some are subsumed by Stategraph's transaction model, and some have no analog (and do not need one). A few commands (`query`, `stacks`, `state identities`) exist only in Terraform, not OpenTofu; the tables note these. The tables below map each standard Terraform or OpenTofu CLI command (and the HCL constructs that replace some of them) to its closest Stategraph counterpart. ## Lifecycle | Terraform | Stategraph | Notes | |-----------|------------|-------| | `terraform init` | — | No equivalent. Stategraph does not initialize a working directory; HCL is supplied to commands directly. | | `terraform plan` | `stategraph tf plan` (alias: `stategraph plan`) | Plans against state stored in Stategraph; changes are recorded in a transaction. | | `terraform apply` | `stategraph tf apply` (alias: `stategraph apply`) | Materializes the minimal HCL subset and runs `terraform apply`. | | `terraform destroy` | — | No dedicated destroy verb. Whole-state deletion is `stategraph states delete`; per-resource removal flows through the refactor or transaction model. | | `terraform refresh` | — | No equivalent. State is sourced from imports and applies. | | `terraform validate` | `stategraph hcl eval`, `stategraph hcl json` (partial) | Closest analog is HCL parsing and evaluation. | | `terraform fmt` | — | Out of scope. | | `terraform console` | `stategraph sql query` (alias: `stategraph query`) | Different paradigm: SQL queries over infrastructure, not an HCL expression REPL. | | `terraform show` (plan file) | `stategraph tf show` | Plan files only. | | `terraform show` (state) | `stategraph states export`, `stategraph states summary` | `tf show` does not display state; use the `states` group. | | `terraform output` | `stategraph sql query` | Outputs are queryable via SQL; no dedicated `output` verb. | | `terraform graph` | — | No Graphviz/DOT export. For dependency analysis, use `stategraph states instances blast-radius` (alias: `stategraph blast-radius`) or the Graph Explorer in the UI. | | `terraform providers` | `stategraph states summary` | Surfaces providers per state. | | `terraform test` | — | No equivalent. | | `terraform metadata functions` | — | No equivalent. | | `terraform modules` | `stategraph states modules list` | Lists modules in a stored state (rather than the working directory). `terraform modules` is Terraform-only; OpenTofu has no `modules` command. | | `terraform query` | — | No equivalent. Terraform-only (not in OpenTofu); runs `list` blocks to search remote infrastructure. | | `terraform stacks` | — | No equivalent. Terraform-only; manages HCP Terraform Stacks. Out of scope. | ## Plan and apply flags Stategraph supports a subset of the flags that modify how `terraform plan` and `terraform apply` behave. | Terraform flag | Stategraph | Notes | |----------------|------------|-------| | `-var=NAME=VALUE` | `stategraph tf plan --var ...` | Pass-through. | | `-var-file=FILE` | `stategraph tf plan --var-file ...` | Pass-through. | | `-refresh=false` | `stategraph tf plan --skip-refresh` | | | `-target=ADDRESS` | `stategraph tf plan --force` (glob-based) | `--force` scopes the plan via glob patterns; it is not Terraform's address syntax. | | `-detailed-exitcode` | `stategraph tf plan --detailed-exitcode` | Returns a tofu/terraform-compatible detailed exit code (also available on `stategraph tf mtx`). | | `-auto-approve` | `stategraph tf apply --auto-approve` | Skip the approval prompt on a plan-less `stategraph tf apply`. | | `-out=FILE` | `stategraph tf plan --out ...` | Save the plan to a file for a later `stategraph tf apply`. Omit it for a read-only preview. | | `-replace=ADDRESS` | — | No equivalent. | | `-refresh-only` | — | No equivalent. | | `-destroy` | — | No equivalent. | ## State manipulation | Terraform | Stategraph | Notes | |-----------|------------|-------| | `terraform state list` | `stategraph states list`, `stategraph states resources summary`, `stategraph states instances query` | Multiple lenses on resources and instances. | | `terraform state show` | `stategraph states export`, `stategraph sql query` | No single-instance "show" verb; query via SQL. | | `terraform state mv` | `stategraph refactor start` / `step` / `complete` | Refactor sessions model address moves. See also the [`moved` block](#hcl-native-alternatives-to-state-commands) below. | | `terraform state rm` | `stategraph states delete` (whole state); refactor flow (per-resource) | No direct per-resource `rm`. | | `terraform state pull` | `stategraph states export` | Dump full state. | | `terraform state push` | `stategraph states import` | Import a state file. | | `terraform state replace-provider` | — | No equivalent. | | `terraform state identities` | — | No equivalent. Terraform-only (not in OpenTofu); lists resource identities. | | `terraform import` | `stategraph import tf`, `stategraph hcl import`, `stategraph states import` | Onboards state and HCL from a directory or file. See also the [`import` block](#hcl-native-alternatives-to-state-commands) below. | | `terraform taint` / `untaint` | — | No equivalent (deprecated upstream as well; use `-replace` on apply, which Stategraph also does not support). | | `terraform force-unlock` | `stategraph tx abort` | Transactions replace state-file locking; the Stategraph HTTP backend does not implement Terraform's lock/unlock semantics. | ## HCL-native alternatives to state commands Modern Terraform exposes several HCL blocks that replace older CLI state operations. Stategraph's support varies by block. | Terraform HCL | Replaces | Stategraph | |---------------|----------|------------| | `moved` block (Terraform 1.1+) | `terraform state mv` | `stategraph refactor complete` writes detected renames and module moves into `moved_stategraph.tf`. Pre-existing `moved` blocks in your HCL are parsed and de-duplicated during evaluation (kept, not dropped). | | `import` block (Terraform 1.5+) | `terraform import` (CLI) | **Evaluated.** Import blocks are parsed (their dependencies are tracked) and passed through to Terraform/OpenTofu at apply — their `to` address is rewritten to match the reified module layout. For bulk onboarding from an existing state, use `stategraph import tf`. | | `removed` block (Terraform 1.7+) | `terraform state rm` | **Evaluated.** `removed` blocks are parsed and passed through to Terraform/OpenTofu at apply — their `from` address is rewritten to match the reified module layout, so Terraform honors the block as written (for example, `lifecycle { destroy = false }` forgets the resource from state without destroying it). Per-resource removal can also flow through the refactor or transaction model. (Release 2.3.21, #1186) | | `check` block (Terraform 1.5+) | Pre/post-condition assertions | References are extracted for the dependency graph; the assertions are not enforced by Stategraph. | | `lifecycle { ignore_changes }` | Selective `refresh` control | Not honored by Stategraph evaluation. | | `lifecycle { prevent_destroy }` | Guard against accidental destroy | Not honored by Stategraph evaluation. | | `lifecycle { create_before_destroy }` | Apply ordering | Passed through to Terraform at apply time. | | `lifecycle { replace_triggered_by }` (Terraform 1.2+) | Replacement for `taint` | Passed through to Terraform at apply time; the referenced resource is retained as a boundary (not literalized), so Terraform/OpenTofu honors the replacement on apply. `terraform_data.triggers_replace` is also supported (Release 2.3.8, #1035). | ## Workspaces | Terraform | Stategraph | Notes | |-----------|------------|-------| | `terraform workspace new` | `stategraph states create --workspace` | A workspace maps to a Stategraph state. | | `terraform workspace select` | `stategraph states resolve` | Resolve a state ID for a workspace. | | `terraform workspace list` | `stategraph states list` | | | `terraform workspace show` | `stategraph info` | Surfaces current context (user, tenants, server). | | `terraform workspace delete` | `stategraph states delete` | | ## Backend configuration Stategraph implements the Terraform HTTP backend, so a workspace can store its state directly in Stategraph instead of S3, GCS, Consul, or Terraform Cloud. | Terraform | Stategraph | Notes | |-----------|------------|-------| | `terraform { backend "http" { ... } }` | `POST` / `GET /api/v1/states/backend/{group_id}[/{workspace}]` | Stategraph exposes the HTTP backend endpoints used by `terraform init`/`apply`. | | Backend `lock` / `unlock` | — | The HTTP backend implements GET and POST only; concurrency is managed by Stategraph transactions, not file locks. | | Other backends (S3, GCS, Consul, …) | — | Out of scope. | ## Auth and miscellaneous | Terraform | Stategraph | Notes | |-----------|------------|-------| | `terraform login` / `logout` | (handled out of band) | Auth via tenant and user setup. See `stategraph user whoami` and `stategraph info`. | | `terraform version` | `stategraph version client`, `stategraph version server` | | | `terraform get` | — | No module-fetch concept exposed. | ## Stategraph-only commands These have no Terraform CLI equivalent — they are part of what Stategraph adds on top. | Command | Purpose | |---------|---------| | `stategraph tx create` / `list` / `abort` / `logs list` | Transactional change model | | `stategraph tf mtx` | Multi-state transactions | | `stategraph states instances blast-radius` (alias: `stategraph blast-radius`) | Dependency and dependent analysis | | `stategraph tenant gaps analyze` (alias: `stategraph gaps`) | Discover unmanaged cloud resources | | `stategraph sql query` / `schema` | SQL across infrastructure | | `stategraph states anonymize` | Anonymize state JSON | | `stategraph user tenants list` | Multi-tenant identity (tenants the user belongs to) | ## See also - [CLI overview](/docs/cli) — full command reference - [States commands](/docs/cli/states) - [Transactions](/docs/cli/transactions) - [Refactor](/docs/cli/refactor) --- # Troubleshooting Source: https://stategraph.com/docs/reference/troubleshooting Common issues and solutions for Stategraph. Deployment problems, database errors, authentication issues, and performance troubleshooting. Common issues and solutions when running Stategraph. ## Deployment Issues ### Container won't start **Symptoms**: Server container exits immediately or keeps restarting. **Check logs**: ```bash docker compose logs server ``` **Missing required environment variables** ``` Error: Key_error "STATEGRAPH_UI_BASE" ``` **Solution**: - Set all required environment variables (see [Environment Variables](/docs/reference/environment-variables)) **Database connection failed** ``` Error: Could not connect to database ``` **Solution**: - Verify database is running and credentials are correct **Port already in use** ``` Error: bind: address already in use ``` **Solution**: - Stop the service using the port or change `STATEGRAPH_PORT` ### Database connection errors **Symptoms**: Server starts but can't connect to PostgreSQL. **Checklist**: - PostgreSQL container is healthy: `docker compose ps` - `DB_HOST` matches the service name (e.g., `db` for Docker Compose) - `DB_PORT` is correct (default: `5432`) - `DB_USER`, `DB_PASS`, `DB_NAME` match PostgreSQL configuration - Network connectivity between containers **Test connection**: ```bash docker compose exec server nc -zv db 5432 ``` ### Health check failing **Symptoms**: Container marked unhealthy, restarts repeatedly. **Check health endpoint**: ```bash curl http://localhost:8080/api/v1/health ``` **Common causes**: - Database not ready yet (increase `depends_on` timeout) - Port mismatch between health check and actual port - Internal service not starting ## Authentication Issues ### OAuth redirect errors **"redirect_uri_mismatch"** The callback URL doesn't match your OAuth provider configuration. **Solution**: 1. Check `STATEGRAPH_UI_BASE` matches your access URL exactly 2. Add exact callback URL to OAuth provider: - For Google: `{STATEGRAPH_UI_BASE}/oauth2/google/callback` - For OIDC: `{STATEGRAPH_UI_BASE}/oauth2/oidc/callback` 3. Verify protocol (http vs https) matches **"invalid_client"** Client ID or secret is incorrect. **Solution**: - Verify credentials from OAuth provider dashboard - Check for extra whitespace or newlines - Regenerate secret if needed ### Session not persisting **Symptoms**: Login succeeds but immediately redirects back to login. **Causes**: 1. **URL mismatch**: `STATEGRAPH_UI_BASE` differs from access URL 2. **Cookie not set**: Proxy stripping cookies, or SameSite issues 3. **HTTPS mismatch**: Accessing via http when configured for https **Solutions**: - Verify `STATEGRAPH_UI_BASE` exactly matches your browser URL - Check for reverse proxy cookie handling - Use consistent protocol ### Login fails with "invalid CSRF token" (multi-replica) **Symptoms**: Login intermittently fails at the OAuth callback with `403 Forbidden — Unable to find a valid CSRF token`. It may succeed on one attempt and fail on the next, and often breaks after a restart or scale-up. **Cause**: With more than one replica, each replica signs the OAuth session/CSRF cookies with its own secret unless a shared one is configured. When `/oauth2/start` and the OAuth callback are load-balanced to different replicas, the callback replica can't validate the cookie the first one issued. **Solutions**: - Set `STATEGRAPH_OAUTH_COOKIE_SECRET` to the same value (16/24/32 chars) on every replica so any replica can validate the cookie. This also keeps users logged in across restarts. (Helm/Terraform users: the chart and the ECS module generate and share a stable value automatically.) - As a stopgap, enable session affinity (sticky sessions) on your load balancer so a client's whole OAuth flow lands on one replica. ### "Access denied" after authentication **Causes**: 1. Email domain restriction: User email not in allowed domain 2. Google Groups: User not in required group 3. OAuth app not approved in organization **Solutions**: - Check `STATEGRAPH_OAUTH_EMAIL_DOMAIN` setting - Verify group membership (Google Groups) - Request app approval from organization admin ## Terraform Backend Issues ### "Failed to get existing workspaces" **Symptoms**: `terraform init` fails with HTTP error. **Causes**: 1. Stategraph server not running 2. URL incorrect 3. Network connectivity 4. Authentication failure **Solutions**: ```bash # Test connectivity curl http://localhost:8080/api/v1/health # Test with credentials curl -H "Authorization: Bearer $STATEGRAPH_API_KEY" http://localhost:8080/api/v1/whoami ``` ### Transaction conflict (`TX_CONFLICT`, HTTP 409) **Symptoms**: A commit fails because another transaction touched overlapping resources. Stategraph's HTTP backend does not implement Terraform's `lock`/`unlock`, so `terraform force-unlock` does not apply — concurrency is managed by transactions. **Solutions**: 1. Wait for the in-flight transaction to complete, then retry 2. If a transaction is stuck, abort it: ```bash stategraph tx abort --tx ``` 3. List open transactions to find the ID: `stategraph tx list --tenant ` ### "HTTP error: 401 Unauthorized" **Causes**: 1. API key invalid or expired 2. Username not set to `session` 3. Token has leading/trailing whitespace **Solutions**: - Create a new API key - Verify `username = "session"` in backend config - Check token value for whitespace ### Large state timeout **Symptoms**: Operations fail for large state files. **Solutions**: 1. Increase `STATEGRAPH_CLIENT_MAX_BODY_SIZE` (default: `512m`) 2. Check reverse proxy timeouts 3. Consider splitting state into smaller files ### "command not found" running plan or apply **Symptoms**: `stategraph tf plan` or `stategraph tf apply` exits immediately without running Terraform or OpenTofu. **Error message** ``` Error: tofu: command not found. Install OpenTofu or Terraform, or set the TF_CMD environment variable to your binary. ``` **Solution**: - Install OpenTofu or Terraform and make sure it is on your `PATH` - Or set `TF_CMD` to the full path of your `tofu` or `terraform` binary ### Plan appears to hang **Symptoms**: `stategraph tf plan` stops making progress and never finishes, producing no further output. **Cause**: Terraform or OpenTofu is waiting for an interactive answer (for example an input variable prompt), but no human is attached to respond. **Solution**: - Supply every required variable non-interactively with `-var`, a `*.tfvars` file, or `TF_VAR_*` environment variables so nothing prompts - Upgrade to the latest CLI, which detects an unanswered prompt instead of hanging ### "has not been declared" during plan **Symptoms**: `stategraph tf plan` fails while evaluating your configuration. **Error message** ``` An input variable with the name "..." has not been declared. ``` (a `Local value with the name "..." has not been declared.` variant can appear the same way) **Solution**: - Trace exactly how Stategraph evaluates the module with `stategraph diagnostics run `, which walks the root module directory and writes an evaluation trace (to stderr, or to a file with `--out`). Pass `--var` / `--var-file` and `--workspace` to match the inputs and workspace your plan uses - Confirm the referenced variable or `local` is actually declared in the evaluated module, and that its source module resolves as expected - Upgrade to the latest CLI — several evaluation fixes for this error shipped in recent releases ## UI Issues ### Page won't load **Symptoms**: Browser shows blank page or error. **Check**: 1. Browser developer console for JavaScript errors 2. Network tab for failed requests 3. Server logs for backend errors **Solutions**: - Clear browser cache - Try incognito/private mode - Check CORS settings if UI is separate ### Query returns no results **Symptoms**: SQL query runs but returns empty. **Causes**: 1. No matching data 2. Query syntax issue 3. Wrong table or column names **Debug steps**: ```sql -- Verify data exists SELECT count(*) FROM instances -- Check available types SELECT DISTINCT type FROM resources ORDER BY type -- Simplify query SELECT * FROM instances LIMIT 10 ``` ### Graph won't render **Symptoms**: Dependency graph blank or shows error. **Causes**: 1. State has no resources 2. Very large state causing performance issues 3. Browser memory limitations **Solutions**: - Check state has resources in the list view - Apply filters to reduce graph size - Try a different browser ## Performance Issues ### Slow queries **Symptoms**: SQL queries take long to execute. **Solutions**: 1. Add LIMIT clause: ```sql SELECT * FROM instances LIMIT 100 ``` 2. Use specific columns instead of `*` 3. Add filters early in query 4. Check database indexes ### High memory usage **Causes**: 1. Large state files 2. Many concurrent connections 3. Memory leak (report as bug) **Solutions**: - Increase container memory limits - Reduce `DB_MAX_POOL_SIZE` - Monitor and restart periodically if needed ### Database connection exhaustion **Symptoms**: "too many connections" errors. **Solutions**: 1. Reduce `DB_MAX_POOL_SIZE` 2. Increase PostgreSQL `max_connections` 3. Check for connection leaks ## Gap Analysis Issues ### "Not ready for gap analysis" **Symptoms**: Gap analysis reports not ready. **Causes**: 1. AWS Config not enabled 2. Aggregator not configured 3. Missing IAM permissions **Solutions**: - Enable AWS Config in your account - Create a configuration aggregator - Grant Stategraph required permissions ### AWS resources not appearing **Causes**: 1. AWS Config not recording resource types 2. Aggregator missing regions 3. Stale cache **Solutions**: - Verify AWS Config recording settings - Check aggregator configuration - Use `source=no-cache` to force refresh ## Cost Analysis Issues ### Cost analysis disabled or returns 503 **Symptoms**: `capabilities.costs.enabled` is `false`, `POST .../costs/calculate` returns `503`, or cost analysis worked and then disappeared after a redeploy, restart, or pod/task reschedule. **Cause**: The server booted without `STATEGRAPH_COST_ENABLED=true` set in its environment — for example it was added to one container but not to the deployment config, so a recreated or rescheduled container came up without it. **Solution**: - Set `STATEGRAPH_COST_ENABLED=true` durably in your deployment environment (Compose `.env` / `environment:`, Kubernetes Deployment env, or the ECS task definition), then redeploy. - See [Cost Setup](/docs/cost/setup) for the durable enablement steps and verification. ## Getting Help ### Collect diagnostic information Before reporting issues, gather: 1. **Server logs**: ```bash docker compose logs server > server.log 2>&1 ``` 2. **Environment** (redact secrets): ```bash docker compose config ``` 3. **Version information**: ```bash docker compose images ``` 4. **Health check output**: ```bash curl http://localhost:8080/api/v1/health ``` ### Reporting issues Report issues at: https://github.com/stategraph/releases/issues Include: - Description of the problem - Steps to reproduce - Expected vs actual behavior - Diagnostic information (above) - Screenshots if applicable ## Common Error Messages | Error | Cause | Solution | |-------|-------|----------| | `Key_error "..."` | Missing environment variable | Set the required variable | | `Connection refused` | Service not running | Start the service | | `401 Unauthorized` | Invalid credentials | Check token/session | | `redirect_uri_mismatch` | OAuth URL mismatch | Fix callback URL | | `command not found` (tf plan/apply) | OpenTofu/Terraform not on `PATH` | Install it, or set `TF_CMD` | | `TX_CONFLICT` (409) | Concurrent transaction | Wait, or abort the other transaction with `stategraph tx abort` | | `503` on `/costs/calculate` | Cost analysis disabled | Set `STATEGRAPH_COST_ENABLED=true` durably ([Cost Setup](/docs/cost/setup)) | | `too many connections` | Pool exhausted | Reduce pool size |