Skip to main content

Getting started

This page runs the whole stack on your machine: database, one collector run, and the dashboard. There is no published package or hosted instance yet. You work from a clone of the (private) repository.

Prerequisites​

  • Node.js 24+ and pnpm 10 (the repo pins pnpm@10.33.2 via packageManager).
  • A real Postgres (16 is what the tests use). packages/db's SQL creates roles, so run migrations as a user that can CREATE ROLE.
  • Docker, only if you want to run the test suites. They start their own throwaway postgres:16 containers.
  • For the collector: the env-sync binary on PATH (the TS or Go build; either satisfies the JSON contract), plus everything env-sync itself needs to read real targets: the aws, gh, and op CLIs, OP_SERVICE_ACCOUNT_TOKEN, and each target platform's credentials. You can skip all of that for a first run. See a no-credential smoke run.
  • For the viewer: a Supabase project for Auth, with email confirmation turned on. The app checks email_confirmed_at, but it cannot turn that setting on for you.
git clone https://github.com/catesworks/env-sync-viewer.git
cd env-sync-viewer
pnpm install
pnpm build

1. Create the schema and roles​

packages/db applies Drizzle migrations first (db:migrate), then the hand-written RLS/role SQL in packages/db/sql/*.sql (db:post). db:deploy runs both in order. Use an admin connection here. The app roles come next.

export DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/env_sync_viewer
pnpm db:deploy

This creates everything in the env_sync schema (never public). It also creates two NOLOGIN group roles, collector and viewer. db:post is safe to re-run.

Create one login role per app, out of band, and keep the passwords in your secret store:

CREATE ROLE env_sync_collector LOGIN PASSWORD '<secret>' IN ROLE collector; -- INSERT-only
CREATE ROLE env_sync_viewer_app LOGIN PASSWORD '<secret>' IN ROLE viewer; -- SELECT-only

Never point either app at a superuser, the table owner, or Supabase's service_role.

2. Run the collector once​

The collector reads an inventory: a JSON list of (manifestDir, env, services?) entries. Each entry becomes one env-sync diff ... --format json subprocess, with manifestDir as its working directory. Relative paths resolve against the inventory file's own directory.

[{"manifestDir": "/path/to/a/repo/with/secrets.manifest.yml", "env": "dev", "services": ["api"]}]

List only the (manifest, env, service) combinations that are expected to exist. If you omit services, the collector runs env-sync diff --all. Do that only when every service in the manifest is deployed to that env.

Option A: against a real manifest​

Write the inventory above to a scratch file (for example /tmp/inventory.json), then:

DATABASE_URL=postgresql://env_sync_collector:<secret>@127.0.0.1:5432/env_sync_viewer \
node apps/collector/dist/main.js /tmp/inventory.json

This reads live platform state through env-sync, so it needs real credentials. Start with a disposable, non-production manifest.

Option B: no-credential smoke run​

The collector's test suite ships a fake env-sync (apps/collector/test/fake-bin/env-sync). It accepts only the collector's exact invocation shape and replies from files in its working directory: fake-stdout, fake-exit, and fake-stderr. You can use it to fill the dashboard with no credentials at all:

mkdir -p /tmp/esv-demo
cat > /tmp/esv-demo/fake-stdout <<'EOF'
{"schemaVersion":1,"generatedAt":"2026-09-28T10:00:00.000Z","env":"dev","services":{"api":{"targets":[
{"target":"vercel my-app","platform":"vercel","diffMode":"fingerprint","status":"ok","keys":[
{"key":"API_KEY","status":"MATCH","expectedFingerprint":"0123456789ab","actualFingerprint":"0123456789ab"},
{"key":"DB_URL","status":"DRIFT","expectedFingerprint":"0123456789ab","actualFingerprint":"ba9876543210"}]}]}}}
EOF
echo '[{"manifestDir": "/tmp/esv-demo", "env": "dev", "services": ["api"]}]' > /tmp/esv-demo/inventory.json

PATH="$PWD/apps/collector/test/fake-bin:$PATH" \
DATABASE_URL=postgresql://env_sync_collector:<secret>@127.0.0.1:5432/env_sync_viewer \
node apps/collector/dist/main.js /tmp/esv-demo/inventory.json

Each invocation writes one collector_runs row, with outcome success, partial, or failed. It also writes that run's snapshot rows, all in one transaction. The process exits 1 if any invocation failed, so a scheduler sees the failure.

The subprocess never sees your DATABASE_* variables. The collector strips them before it spawns env-sync.

3. Run the dashboard​

Put the viewer's settings in apps/web/.env.local:

DATABASE_URL=postgresql://env_sync_viewer_app:<secret>@127.0.0.1:5432/env_sync_viewer
NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<publishable key>
VIEWER_ALLOWLIST=you@example.com,teammate@example.com
# STALE_AFTER_HOURS=26 # default

Then start it:

pnpm --filter @env-sync-viewer/web dev # http://localhost:3054

Sign in with a confirmed Supabase account whose email is on VIEWER_ALLOWLIST. If there is no session, you are redirected to /login. If you are signed in but not allowlisted (or not confirmed), you get a 403. If VIEWER_ALLOWLIST is empty or unset, nobody gets in.

Both at once​

pnpm dev at the repo root runs process-compose. It runs the collector once, then starts the web app. It reads DATABASE_URL and env-sync from the shell you launch it from.

Run the tests​

pnpm --filter @env-sync-viewer/db test:unit # RLS suite, real Postgres (Docker)
pnpm --filter @env-sync-viewer/collector test:unit # real Postgres + fake env-sync (Docker)
pnpm --filter @env-sync-viewer/web test:unit

Each Docker-backed suite starts postgres:16 on a random localhost port, keeps its data on tmpfs, and removes the container on exit, whether the tests pass or fail.

Running it for real​

The collector is designed to run on a schedule in GitHub Actions, not on Vercel cron or any serverless platform, because env-sync needs the aws/gh/op binaries on its host. That host holds credentials for every platform at once. Run it in a dedicated repo/CI environment with protected branches and required reviewers, and never on a shared self-hosted runner. See Architecture → credentials.