envpact

Centralized secrets manager for AI coding agents. Your secrets live in your own private GitHub repo; envpact reads them into project-scoped .env files, syncs them to AI agents over MCP, and rotates them across every project at once.

View the Project on GitHub chirag127/envpact

Vault Schema (v2)

The vault is a single secrets.json file in your private GitHub repo. This document specifies the exact schema; the formal JSON Schema is at https://envpact.oriz.in/schema/v2.json.

Top-level

{
  "$schema": "https://envpact.oriz.in/schema/v2.json",
  "version": 2,
  "shared":   { [key: string]: string },
  "projects": { [project: string]: ProjectEntry },
  "metadata": { created_at: string, updated_at: string, owner?: string }
}

ProjectEntry

{
  "_default_env"?: string,                          // optional metadata
  [key: string]: string | { [env: string]: string } // any number of secrets
}

Keys whose name starts with _ are metadata and are not exported to .env. Currently only _default_env is recognized; unknown _* keys are ignored gracefully.

Value forms

Form Example Behaviour
Plain string "PORT": "3000" Used as-is.
Shared reference "OPENAI_API_KEY": "shared.OPENAI_API_KEY" Looked up in shared. One level only — no recursion.
Encrypted "DB_PASSWORD": "enc:base64(armored-age-ciphertext)" Decrypted on read using ~/.envpact/age.key.
Per-environment "URL": { "dev": "...", "prod": "..." } Selects based on requested environment.
Mixed (per-env w/ shared) "URL": { "prod": "shared.URL_PROD" } Selected env value still subject to the shared. prefix rule.

Resolution Algorithm

See SHARED_SPEC §1. The exact order:

  1. Determine effective environment: argument override → _default_env'default'.
  2. For each project key:
    • String → resolve via the prefix rules.
    • Object → pick [env] or fall back to [default], then resolve the picked value via the prefix rules.
    • Anything else → mark INVALID.
  3. Return { resolved, unresolved, invalid, encrypted, environment }.

Validation

Migration: v1 → v2

The v1 schema only had flat strings (no per-environment objects). A v1 vault is automatically promoted to v2 on read — flat strings work identically. You only opt into per-environment values by writing an object value for a key.

Examples

Single project, single environment

{
  "version": 2,
  "shared": {},
  "projects": {
    "blog": {
      "PUBLIC_SITE_URL": "https://blog.oriz.in",
      "PUBLIC_CF_BEACON": "abc123"
    }
  }
}

Shared keys, two environments

{
  "version": 2,
  "shared": {
    "OPENAI_API_KEY": "sk-…",
    "STRIPE_SECRET_KEY_LIVE": "sk_live_…",
    "STRIPE_SECRET_KEY_TEST": "sk_test_…"
  },
  "projects": {
    "saas-app": {
      "_default_env": "production",
      "OPENAI_API_KEY": "shared.OPENAI_API_KEY",
      "STRIPE_SECRET_KEY": {
        "development": "shared.STRIPE_SECRET_KEY_TEST",
        "production":  "shared.STRIPE_SECRET_KEY_LIVE"
      },
      "PORT": "3000"
    }
  }
}

Encrypted values

{
  "version": 2,
  "shared": {
    "DB_PASSWORD": "enc:YWdlLWVuY3J5cHRpb24ub3JnL3YxC..."
  },
  "projects": { "any-app": { "DB_PASSWORD": "shared.DB_PASSWORD" } }
}