OpenTaco (Layer-0) is a CLI + lightweight service for state control—create/read/update/delete and lock Terraform/OpenTofu state files, and act as an HTTP state backend proxy, paving the way for dependency awareness and RBAC. Today, the service runs stateless with an S3 “bucket-only” adapter for state storage (with an in-memory fallback for local demos).
- Live docs: https://opentaco.mintlify.app/
- Source: opentaco/docs/(Mintlify). When changing APIs, CLI behavior, storage semantics, or examples, update the relevant docs pages (overview, getting-started, cli, service-backend, provider, storage, demo, troubleshooting, reference) in the same PR.- New: see docs/s3-compat.mdfor the S3‑compatible backend.
 
- New: see 
OpenTaco is an open-source "Terraform Companion" that starts with state control: a CLI + lightweight service focused on managing state files and access to them, not CI jobs. The long game is a self-hostable alternative to Terraform Cloud / Enterprise: state + RBAC first, then remote execution, PR automation, drift, and policy as later layers. This repo already includes a working S3 adapter, a Terraform HTTP backend proxy (GET/POST/PUT/LOCK/UNLOCK), and usable CLI + provider.
- Layer-0 = State control (CRUD + lock, plus a backend proxy). No runs, no PR automation, no UI in this layer.
- CLI-first to settle semantics; UI comes later.
- Self-hosted, bucket-only later (S3 as the only stateful store when we add real storage; the service remains stateless).
- Backwards compatibility with existing S3 layouts (incl. Terragrunt) when we wire storage later; adoption should be drop-in.
- Import from TFC should be easy (later); keep shapes friendly to that path.
- Go 1.25 or later
- Make
make allmake svc # Service starts on http://localhost:8080 # Health: curl http://localhost:8080/healthz # Ready: curl http://localhost:8080/readyzAuth is enabled by default. To temporarily bypass it (e.g., provider dev):
./opentacosvc -auth-disable -storage memory# Build the CLI make cli # Create a state ./taco state create myapp/prod # List states ./taco state ls # Get state metadata (size, lock status, last updated) ./taco state info myapp/prod # Delete a state ./taco state rm myapp/prod # Download state data ./taco state pull myapp/prod output.tfstate # Upload state data  ./taco state push myapp/prod input.tfstate # Lock a state manually ./taco state lock myapp/prod # Unlock a state ./taco state unlock myapp/prod # Acquire (lock + download in one operation) ./taco state acquire myapp/prod output.tfstate # Release (upload + unlock in one operation) ./taco state release myapp/prod input.tfstate # Auth commands ./taco login --issuer <OIDC_ISSUER> --client-id <CLIENT_ID> # Runs PKCE flow and saves tokens # or simply: # ./taco login --server http://localhost:8080 # CLI fetches issuer/client_id from /v1/auth/config ./taco whoami # Prints current identity (if logged in) ./taco creds --json # Prints AWS Process Credentials JSON via service ./taco logout # Removes saved tokens for --serverConfigure OIDC so taco login works and protected endpoints require login.
- WorkOS setup
- Create a User Management project and a Native (PKCE) OAuth application.
- Add redirect URI: http://127.0.0.1:8585/callback.
- Note values: - Client ID: <WORKOS_CLIENT_ID>
- Issuer: https://api.workos.com/user_management
- Authorization endpoint: https://api.workos.com/user_management/authorize
- Token endpoint: https://api.workos.com/user_management/token
 
- Client ID: 
- Service config (verifies ID tokens and issues OpenTaco tokens)
export OPENTACO_AUTH_ISSUER="https://api.workos.com/user_management" export OPENTACO_AUTH_CLIENT_ID="<WORKOS_CLIENT_ID>" ./opentacosvc -storage memory- Login via CLI (PKCE)
./taco login \ --server http://localhost:8080 \ --issuer https://api.workos.com/user_management \ --client-id <WORKOS_CLIENT_ID> \ --auth-url https://api.workos.com/user_management/authorize \ --token-url https://api.workos.com/user_management/tokenThis opens a browser (also prints the URL). After you authenticate, the CLI exchanges the OIDC ID token with the service and saves OpenTaco tokens to ~/.config/opentaco/credentials.json. To force the login box even if an SSO session exists, add --force-login.
Auth0 variant:
export OPENTACO_AUTH_ISSUER="https://<TENANT>.auth0.com" # or <region>.auth0.com export OPENTACO_AUTH_CLIENT_ID="<AUTH0_NATIVE_APP_CLIENT_ID>" ./opentacosvc -storage memory # No flags needed; CLI uses discovery via /v1/auth/config ./taco login --server http://localhost:8080- Verify auth
# Without login (or in a fresh shell without saved tokens): should return 401 curl -i http://localhost:8080/v1/states # Using CLI (adds bearer automatically) ./taco state ls# Build the provider make build-prov # Example usage cd providers/terraform/opentaco/examples/basic # Configure the provider cat > main.tf << 'EOF' terraform {  required_providers {  opentaco = {  source = "digger/opentaco"  }  } }  provider "opentaco" {  endpoint = "http://localhost:8080" # Or use OPENTACO_ENDPOINT env var }  # Create a state registration resource "opentaco_state" "example" {  id = "myapp/prod"    labels = {  environment = "production"  team = "infrastructure"  } }  # Read state metadata data "opentaco_state" "example" {  id = opentaco_state.example.id }  output "state_info" {  value = {  id = data.opentaco_state.example.id  size = data.opentaco_state.example.size  locked = data.opentaco_state.example.locked  updated = data.opentaco_state.example.updated  } } EOF # Run Terraform terraform init terraform apply #### Local Provider Install (for development) When using the local, unpublished provider: Option A — dev overrides in `~/.terraformrc`: ```hcl provider_installation {  dev_overrides { "digger/opentaco" = "/absolute/path/to/opentaco/providers/terraform/opentaco" }  direct {} }Option B — install into the plugin directory:
~/.terraform.d/plugins/digger/opentaco/0.0.0/<os>_<arch>/terraform-provider-opentaco Then run terraform init again to pick up the local build.
 ### Provider Bootstrap (taco provider init) Quickly scaffold a Terraform workspace which: - Stores its own TF state in the OpenTaco HTTP backend at `__opentaco_system_state`. - Configures the OpenTaco provider against your server. - Includes a demo `opentaco_state` resource (e.g., `myapp/prod`). Steps: ```bash # 1) Start the service on S3 OPENTACO_S3_BUCKET=<bucket> \ OPENTACO_S3_REGION=<region> \ OPENTACO_S3_PREFIX=<prefix> \ ./opentacosvc # 2) Scaffold the provider workspace ./taco provider init opentaco-config --server http://localhost:8080 # 3) Initialize and apply cd opentaco-config terraform init terraform apply -auto-approve # 4) Verify in S3 aws s3 ls s3://$OPENTACO_S3_BUCKET/$OPENTACO_S3_PREFIX/__opentaco_system_state/ aws s3 ls s3://$OPENTACO_S3_BUCKET/$OPENTACO_S3_PREFIX/myapp/prod/ Notes:
- System state defaults to __opentaco_system_state. Override with--system-state <id>if desired.
- The CLI creates the system state by convention (skip with --no-create).
- You can scaffold into the current directory with: ./taco provider init . --server http://localhost:8080.
- Reserved names starting with __opentaco_are platform‑owned and should not be used for user stacks.
- Default system state ID is __opentaco_system_state, stored alongside user states under the same S3 prefix.
- The backend treats this like any other state; the CLI drives creation by convention.
terraform { backend "http" { address = "http://localhost:8080/v1/backend/myapp/prod" lock_address = "http://localhost:8080/v1/backend/myapp/prod" unlock_address = "http://localhost:8080/v1/backend/myapp/prod" } }-  Service ( cmd/opentacosvc/) - HTTP server with two surfaces:- Management API (/v1) for CRUD operations on states
- Terraform HTTP backend proxy (/v1/backend/{id}) for Terraform/OpenTofu
 
- Management API (
-  CLI ( cmd/taco/) - Command-line interface that calls the service for all operations
-  SDK ( pkg/sdk/) - Typed HTTP client used by both CLI and Terraform provider
-  Terraform Provider ( providers/terraform/opentaco/) - Manage states as Terraform resources
- S3 Store (default): Uses your AWS account “bucket-only” layout. Configure via flags or env (standard AWS SDK chain is used for auth).
- Memory Store (fallback): Automatically used if S3 configuration is missing or fails at startup; resets on restart.
S3 object layout per state:
- <prefix>/<state-id>/terraform.tfstate
- <prefix>/<state-id>/terraform.tfstate.lock(present only while locked)
Auth: All management endpoints require Authorization: Bearer <access>, unless the service is started with -auth-disable.
Note: State IDs containing slashes (e.g., myapp/prod) are URL-encoded by replacing / with __ in the path.
-  POST /v1/states- Create a new state- Body: {"id": "myapp/prod"}
- Response: {"id": "myapp/prod", "created": "2025-01-01T00:00:00Z"}
 
- Body: 
-  GET /v1/states?prefix=- List states with optional prefix filter- Response: {"states": [...], "count": 10}
 
- Response: 
-  GET /v1/states/{encoded_id}- Get state metadata- Example: /v1/states/myapp__prod
- Response: {"id": "myapp/prod", "size": 1024, "updated": "...", "locked": false}
 
- Example: 
-  DELETE /v1/states/{encoded_id}- Delete a state
-  GET /v1/states/{encoded_id}/download- Download state file- Returns: Raw state file content
 
-  POST /v1/states/{encoded_id}/upload- Upload state file- Body: Raw state file content
- Query param: ?if_locked_by={lock_id}(optional)
 
-  POST /v1/states/{encoded_id}/lock- Lock a state- Body: {"id": "lock-uuid", "who": "user@host", "version": "1.0.0"}(optional)
- Response: Lock info or 409 Conflict with current lock info
 
- Body: 
-  DELETE /v1/states/{encoded_id}/unlock- Unlock a state- Body: {"id": "lock-uuid"}
 
- Body: 
- GET /v1/backend/{id}- Get state for Terraform
- POST /v1/backend/{id}- Update state from Terraform
- PUT /v1/backend/{id}- Update state from Terraform (alias of POST)
- LOCK /v1/backend/{id}- Acquire lock for Terraform
- UNLOCK /v1/backend/{id}- Release lock from Terraform
Note: Terraform lock coordination uses the X-Terraform-Lock-ID header; the service respects this header on update and unlock operations.
- GET /v1/auth/config– Server OIDC config (issuer, client_id, optional endpoints, redirect URIs)
- POST /v1/auth/exchange– Exchange OIDC ID token for OpenTaco access/refresh
- POST /v1/auth/token– Refresh to new access (rotates refresh)
- POST /v1/auth/issue-s3-creds– Issue stateless STS creds; requires- Authorization: Bearer <access>
- GET /v1/auth/me– Echo subject/roles/groups from Bearer if present
- GET /oidc/jwks.json– JWKS with current signing key
- taco state create <id>- Register a new state
- taco state ls [prefix]- List states, optionally filtered by prefix
- taco state info <id>- Show state metadata (aliases:- show,- describe)
- taco state rm <id>- Delete a state (aliases:- delete,- remove)
- taco state pull <id> [file]- Download state data (stdout if no file specified)
- taco state push <id> <file>- Upload state data from file
- taco state lock <id>- Manually lock a state
- taco state unlock <id> [lock-id]- Unlock a state (uses saved lock ID if not provided)
- taco state acquire <id> [file]- Lock + download in one operation
- taco state release <id> <file>- Upload + unlock in one operation
- --server URL- OpenTaco server URL (default:- http://localhost:8080, env:- OPENTACO_SERVER)
- -v, --verbose- Enable verbose output
- taco provider init [dir]- Scaffold a Terraform workspace for the OpenTaco provider- Flags: - --dir <path>: Output directory (default- opentaco-config; positional- [dir]takes precedence if given)
- --system-state <id>: System state for the backend (default- __opentaco_system_state)
- --force: Overwrite files if they exist
- --no-create: Do not create the system state (scaffold only)
 
 
- Flags: 
- CLI: OPENTACO_SERVERsets the default server URL fortaco.
- Terraform provider: OPENTACO_ENDPOINTsets the default provider endpoint.
- taco login [--force-login]– PKCE login; saves tokens to- ~/.config/opentaco/credentials.json
- taco whoami– Prints current identity
- taco creds --json– Prints AWS Process Credentials JSON via- /v1/auth/issue-s3-creds
- taco logout– Removes saved tokens for- --server
opentaco/ ├── cmd/ │ ├── opentacosvc/ # Service binary │ └── taco/ # CLI binary │ └── commands/ # Cobra commands package ├── internal/ │ ├── api/ # HTTP handlers │ ├── backend/ # Terraform backend proxy │ ├── domain/ # Business logic │ ├── auth/ # Auth handlers & JWKS (stub) │ ├── oidc/ # OIDC verifier abstraction (stub) │ ├── sts/ # STS issuer interface (stub) │ ├── rbac/ # RBAC checker (permissive stub) │ ├── middleware/ # AuthN/AuthZ middlewares, 501 helper │ ├── storage/ # Storage interfaces │ └── observability/ # Health/metrics ├── pkg/ │ └── sdk/ # Go client library └── providers/ └── terraform/ # Terraform provider └── opentaco/ # Initialize modules (first time only; skip if go.mod files exist) make init # Build everything make all # Build individual components make build-svc # Service only make build-cli # CLI only make build-prov # Provider onlymake testmake lintmake clean- Example auth config shape is provided in configs/auth.yaml(not yet enforced).
- Docs placeholders for upcoming auth/STS work: - docs/backend_profile_guide.md
- docs/auth_config_examples.md
- docs/final_spec_state_auth_sts.md
 
# Run with S3 storage (default) # Uses standard AWS credential/config chain (env, shared config, IAM role) OPENTACO_S3_BUCKET=my-bucket \ OPENTACO_S3_PREFIX=opentaco \ OPENTACO_S3_REGION=us-east-1 \ ./opentacosvc # Explicit flags (optional) ./opentacosvc -storage s3 \ -s3-bucket my-bucket \ -s3-prefix opentaco \ -s3-region us-east-1 # Force in-memory storage ./opentacosvc -storage memory-  405 on LOCK/UNLOCK during terraform init/apply:- Cause: routes for custom HTTP verbs not wired.
- Fix (service): add e.Add("LOCK", "/v1/backend/*", handler)ande.Add("UNLOCK", "/v1/backend/*", handler), rebuild, restart.
 
-  409 on POST/PUT ("Failed to save state"): - Cause: backend not reading lock ID from Terraform query ?ID=<uuid>.
- Fix (service): in update handler, read lock ID from header X-Terraform-Lock-IDOR queryID/id.
 
- Cause: backend not reading lock ID from Terraform query 
-  409 on Create in provider ("State already exists"): - Cause: remote state with same idalready exists; renaming the Terraform resource block does not change the backend ID.
- Fix options: - Import: terraform import opentaco_state.NAME <id>.
- Change ID: update id = "..."to a new value.
- Remove remote: ./taco --server <url> state rm <id>then apply.
 
- Import: 
 
- Cause: remote state with same 
If Terraform cannot find the local provider, add a workspace-local CLI config and re-init:
# From repo root (path to provider source dir) ABS="$(pwd)/providers/terraform/opentaco" # Write a local override inside your scaffolded dir cat > opentaco-config/.terraformrc <<EOF provider_installation {  dev_overrides { "digger/opentaco" = "${ABS}" }  direct {} } EOF export TF_CLI_CONFIG_FILE="$PWD/opentaco-config/.terraformrc" cd opentaco-config && terraform init -upgrade- User-facing IDs use natural paths: myapp/prod
- HTTP routes encode slashes as double underscores: myapp__prod
- This is handled automatically by the CLI and SDK
- Locks are cooperative - clients must respect them
- Lock IDs are UUIDs generated by clients
- The CLI saves lock IDs locally in .taco/for convenience
- Terraform backend operations handle locking automatically
- Default storage: S3 (bucket-only). Uses AWS default credential chain.
- Fallback: if S3 is not configured or init fails, the service warns and falls back to in-memory storage.
- S3 object layout per state: - <prefix>/<state-id>/terraform.tfstate
- <prefix>/<state-id>/terraform.tfstate.lock
 
- System state convention: - Reserved IDs start with __opentaco_.
- Default system state is __opentaco_system_stateand is created by the CLI (not auto-created by the service).
 
- Reserved IDs start with 
- S3 Adapter: Production storage backend maintaining compatibility with existing state layouts
- State Versioning: Keep history of state changes
- Metrics: Prometheus metrics for monitoring
- State Graph: Track outputs and dependencies between states
- RBAC: Organizations, teams, users with SSO integration
- Audit Logging: Track all state operations
- Remote Execution: Run Terraform in controlled environments
- PR Automation: GitOps workflows with state management
- Policy Engine: OPA-based policy enforcement on state changes
- UI: Web interface for state management
[License information to be added]
You can point Terraform’s S3 backend at OpenTaco’s /s3 endpoint using process credentials minted by the CLI.
- AWS profile (~/.aws/config):
[profile opentaco-state-backend] region = auto credential_process = "/absolute/path/to/taco" creds --json --server http://localhost:8080 - Terraform backend block:
terraform { backend "s3" { bucket = "opentaco" key = "myapp/prod/terraform.tfstate" endpoints = { s3 = "http://localhost:8080/s3" } use_path_style = true skip_credentials_validation = true skip_region_validation = true skip_requesting_account_id = true use_lockfile = true # Terraform 1.13+ profile = "opentaco-state-backend" } }- Run:
./taco login --server http://localhost:8080 export AWS_SDK_LOAD_CONFIG=1 export AWS_PROFILE=opentaco-state-backend terraform init -reconfigure && terraform apply -auto-approveMore details in docs/s3-compat.md.