Architecture
The provider contract
Each platform is an adapter that implements a small contract. The orchestration layer (diff, push, validate) knows nothing platform-specific. It only calls these hooks:
| Hook | Purpose |
|---|---|
describeTarget(target) | A stable, human-readable label, such as ssm:/myapp/api or vercel:myapp-web[development]. It is used in output, the audit log, and the JSON diff. |
readCurrent(target, env) | Returns the platform's current state as fingerprints or presence markers, never raw values. |
pushValues(target, env, values) | Writes the values and reports success or failure. |
DIFF_MODE | fingerprint (the default) or presence, for platforms whose secrets cannot be read back. |
validateKeys(fields) | Optional. A key-name check that only the source field list can answer. |
Adapters are wired through a static registry. There is no dynamic import(), so the set of platforms is exactly what ships.
The six adapters
| Platform | Target fields | Notes |
|---|---|---|
ssm | path | Writes to <path>/<env>/<KEY>. The environment is part of the path. Guards against the wrong AWS account or region before a write. |
dotenv | path | A local file. Useful for dev machines and Expo-style .env.local files. |
vercel | project, vercel_targets | vercel_targets must be a subset of development, preview, production. |
render | serviceId | Finds the right key when you hold one Render API key per team. |
caprover | appName, captainDomain | Read-only by default. Another pipeline usually owns CapRover env vars. read_only: false opts in and prints a loud "second writer" warning. |
github-actions | repo, optional github_env | Secrets are write-only, so this adapter uses presence mode. |
Any target can set read_only: true (diff and validate only, never pushed) or non_diffable (keys that are reported but never compared).
The protection rule
The most important design point in env-sync is how it decides whether a push --apply needs a human to confirm.
The obvious design checks the --env string: dev is safe, anything else asks. That design is wrong. For Vercel, Render, and CapRover, the adapter ignores --env entirely. The live write scope comes from vercel_targets, serviceId, or appName. So push --env dev --apply could silently write to a production Render service.
The next design checks each target's manifest fields for proof that the write stays dev-scoped. An early version auto-approved Vercel targets declared as vercel_targets: [development]. An adversarial review found a real bug in that rule. The Vercel adapter updates an existing entry in place, and that entry's live scope can be wider than the manifest says. For example, an earlier "All Environments" write leaves the entry scoped to production, preview, and development. The manifest cannot see live Vercel state, so it cannot prove that the write stays in development.
The rule that shipped is deliberately conservative. A target is auto-approvable only when its live scope is derived from --env in code:
- SSM: the path is
<path>/<env>. Always provable. - GitHub Actions: provable only when
github_envis set and is dev-tier. - Everything else: never provable, so always protected.
A false "please confirm" costs one prompt or a --yes flag. A false "auto-approved" is an unattended write to a production secret. With --all, the rule runs across every target before any write, so a partial apply cannot happen. The Go implementation had this fix from its first commit.
Two runtimes, one contract
- TypeScript (
packages/core+apps/cli) is a faithful port of the originalsecrets-source-of-truthscripts. The original 191 tests are carried over as a pinned baseline. - Go (
go/) is an independent, idiomatic implementation of the same contract. It uses Cobra, the official 1Password and AWS SDKs, andgo-githubwithnacl/boxfor GitHub secret sealing. The goal is one static binary with no Node dependency.
The link between the two runtimes is a set of golden conformance fixtures (packages/core/fixtures/). Each fixture pairs a target and a recorded platform state with the expected readCurrent/pushValues outcome. The fixtures were recorded from the original adapters. The Go test harness loads the same files in place, with fake HTTP servers and injectable clients. Both runtimes must match every expected result.
Honest status: the Go binary is fixture-verified against the same behavior as the TypeScript binary. It has not yet run against a real 1Password Service Account. The TypeScript CLI is the implementation to use today.