dayzero
v0.2.0
Published
Auth once per machine. One command provisions every service a new project needs and writes .env.local.
Downloads
270
Maintainers
Readme
dayzero (d0)
Auth once per machine. Then one command provisions every service a new project needs (repos, databases, DNS, API keys), writes .env.local, and unblocks your agent.
d0 init # scaffold stack.yaml
d0 login github # interactive, once per machine — tokens go to the OS keychain
d0 up # non-interactive: provision everything, write .env.localPrinciples
- Human auths, agent provisions. Anything needing a browser or MFA is a
loginconcern.upruns with zero prompts — a missing login is a structured, machine-readable error naming the exact fix. - Zero-knowledge, zero backend. No dayzero servers. Credentials live in your OS keychain and travel only from your machine to the provider's API.
- Secrets never enter the agent. Agents learn env var names and status, never values.
- Idempotent. Running
d0 uptwice is safe — state-file reconciliation, not blind creation. - Least privilege. Project-scoped, minimally-permissioned keys wherever the provider supports it.
dayzero is not Terraform. It provisions the start of a project, not ongoing infra state. If a resource needs ongoing declarative management, that's your IaC tool's job.
Commands
d0 init [project] # scaffold stack.yaml
d0 login <provider|all> # interactive auth → OS keychain
d0 logout <provider> # revoke where possible + remove from keychain
d0 status [--json] # auth state + provisioned resources
d0 up [--only a,b] [--dry-run] [--json]
d0 down [--only a,b] [--yes]
d0 env [--check] [--json] # env var names + presence (never values)
d0 doctor # keychain, token validity, API reachability
d0 exec [--only a,b] -- <cmd> # run a command with provider credentials injected
d0 mcp # MCP server (stdio) for agent useRunning provider tooling
Credentials live in the keychain, so a provider's own CLI cannot see them.
d0 exec bridges that gap — it injects each provider's expected variable
(CLOUDFLARE_API_TOKEN, SUPABASE_ACCESS_TOKEN, NEON_API_KEY, GH_TOKEN,
…) plus your env file into a child process, and nowhere else:
d0 exec -- wrangler deploy
d0 exec -- supabase db push
d0 exec --print-env # names only, never valuesThis keeps the secret out of your shell history, out of an agent's context, and out of a second credential store. Without it, tooling has to be authenticated separately — which is how you end up with two credentials for the same provider and only one of them managed.
MCP server
d0 mcp exposes the same core to agents over the Model Context Protocol (stdio). Register it with Claude Code:
claude mcp add d0 -- d0 mcpSix tools: check_auth, provision_plan, provision_stack, get_env_status, open_login, teardown (refuses without confirm: true). No tool ever returns a secret value — a redaction layer scrubs every payload before transport, on top of handlers that only deal in env var names. Errors are structured: { provider, code, humanFix, agentRetryable } — when auth is missing, the agent relays the humanFix line (e.g. d0 login railway) and the human does the one interactive step.
stack.yaml
version: 1
project: acme-dashboard
providers:
github:
repo: acme-dashboard
private: true
env:
file: .env.localStatus
Early, macOS only for now (keychain via the security CLI, zero native deps).
| Provider | Auth | Provisions | Env vars |
|---|---|---|---|
| github | device flow / PAT | repo | GITHUB_REPO_URL |
| supabase | PAT | project + keys | SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_DB_PASSWORD, DATABASE_URL |
| cloudflare | PAT | Pages, R2, KV, DNS, wrangler.jsonc | CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_PAGES_URL, CLOUDFLARE_R2_BUCKET_*, CLOUDFLARE_KV_*_ID |
| neon | PAT | Postgres project | NEON_PROJECT_ID, DATABASE_URL |
| railway | PAT | project, services, project token | RAILWAY_PROJECT_ID, RAILWAY_TOKEN |
| resend | PAT | API key, sending domain | RESEND_API_KEY |
| webflow | PAT | CMS collections, webhooks (site must already exist) | WEBFLOW_SITE_ID, WEBFLOW_API_TOKEN, WEBFLOW_COLLECTION_*_ID |
Webflow is a partial provider by necessity. Webflow has no site-creation endpoint — sites are built in the Designer — so d0 resolves an existing site (auto if the token sees exactly one, else set site:) and provisions what the API does support: CMS collections and webhooks. d0 down removes collections and webhooks d0 created and never touches the site.
Webflow multi-site access (OAuth)
A Webflow site token reaches exactly one site, and d0 stores one credential per provider — so with site tokens you can only wire up one Webflow site per machine. Workspace tokens do not solve this: their only scope is workspace_activity:read (audit logs), they can't reach site endpoints at all, and they're Enterprise-only.
The real multi-site path is an OAuth App, where Webflow asks which sites to authorize. Register one once:
- Create an App at webflow.com → Workspace settings → Apps & integrations → Develop an App.
- Set its redirect URI to exactly
http://localhost:4471/callback. - Request scopes
sites:read,sites:write,cms:read,cms:write. - Run
d0 login webflow. The first time, it asks whether you have an app and prompts for the Client ID and Secret (found under Apps & Integrations → App Development), then stores them in your keychain. Every later login skips straight to the browser.
$ d0 login webflow
Webflow site tokens reach a single site. An OAuth app lets you authorize several sites at once.
◇ Do you have a Webflow App registered? › Yes
◇ Webflow App Client ID › ...
◇ Webflow App Client Secret › ...
✓ Webflow authenticated as Acme Site, Other Sited0 opens Webflow, you pick the sites, and the callback lands on a loopback listener bound to 127.0.0.1 — never exposed to the network. Choose which site a project uses with site: in stack.yaml.
Answer "no" and login falls back to the site-token paste, warning that it reaches only one site. D0_WEBFLOW_CLIENT_ID / D0_WEBFLOW_CLIENT_SECRET still take precedence when set, which is the path to use in CI. Override the port with D0_OAUTH_PORT (it must match the registered redirect URI), and D0_OAUTH_HOST=127.0.0.1 if you'd rather register the literal loopback address per RFC 8252.
webflow:
site: acme # optional: id, shortName, or displayName
collections:
- Blog Posts # slug + singular derived; or { name, singular, slug }
webhooks:
- { trigger: form_submission, url: https://api.example.com/webflow }Wrangler config
Set wrangler: true on the Cloudflare block and d0 writes a wrangler.jsonc
wiring up the bindings it just provisioned — real KV namespace ids, R2 bucket
names, and your account id — so wrangler deploy works without hand-wiring
anything:
cloudflare:
r2: [uploads]
kv: [cache]
wrangler: { buildOutputDir: ./dist } # or just `wrangler: true`An existing wrangler.jsonc is never overwritten: d0 writes its version to
wrangler.jsonc.d0 alongside and warns, so you (or an agent) can merge.
Verified by running wrangler types against a generated config — it resolved
KVNamespace and R2Bucket bindings correctly.
GitHub, Supabase, Neon, Cloudflare, Resend, and Webflow verified live against real accounts (Webflow via OAuth across 13 sites: collection created, re-run adopted without duplicating, teardown removed the collection and left the site and its other collections untouched). Railway is tested against API fixtures only. Generated secrets that providers only return once (DB passwords, project tokens, minted keys) are kept in the keychain so re-running up always reproduces a complete .env.local.
GitHub auth uses OAuth device flow when D0_GITHUB_CLIENT_ID is set; otherwise it opens the token-creation page with the right scopes preselected and validates the pasted PAT before storing.
Development
npm install
npm run build # tsup → dist/cli.js
npm test # vitest (includes a live macOS keychain round-trip)
node dist/cli.js --help