Cloudflare Worker setup
The cf vault kind is two halves: arca-vault-cf in the CLI (client-side age encryption + blocking HTTP) and a Worker you self-host — a thin store fronting D1 that holds opaque ciphertext only. The worker has no key material and cannot read what it serves.
What you're deploying
The worker source, schema, and the authoritative deploy walkthrough live in the repo:
crates/arca-vault-cf/worker/README.md— wire protocol (arca-vault-cf/2),wranglerstepsworker/index.js— the Worker (constant-time auth, scoped tokens, ArcaHmac verification + REST verbs)worker/schema.sql— the D1 table (namecolumn added in v2 —ALTER TABLE secrets ADD COLUMN name TEXTon an existing db)worker/wrangler.toml— binding config
The short version:
cd crates/arca-vault-cf/worker
wrangler d1 create arca-vault # → paste database_id into wrangler.toml
wrangler d1 execute arca-vault --file=schema.sql
wrangler secret put ARCA_VAULT_TOKEN # legacy single bearer, or:
wrangler secret put ARCA_VAULT_TOKENS # scoped token map (JSON) — below
wrangler deployThen point a vault at it in arca.toml:
[vaults.cloud]
kind = "cf"
url = "https://arca-vault.you.workers.dev"
token_env = "ARCA_CF_TOKEN"
[envs]
prod = { vault = "cloud" }and export the bearer token under the name token_env chose (export ARCA_CF_TOKEN=…).
Scoped tokens (wire v2)
One static bearer for every env is a big blast radius. Wire v2 scopes tokens per env: the worker reads ARCA_VAULT_TOKENS, a JSON map of token_id → {token, scopes}, and enforces {env|"*" → {read, write}} after auth — a leaked dev token can't read prod.
{
"ops/cf-token-dev": {
"token": "<dev-token-secret>",
"scopes": { "dev": { "read": true, "write": true } }
},
"ops/cf-token-viewer": {
"token": "<viewer-secret>",
"scopes": { "*": { "read": true, "write": false } }
}
}On the client side, each scoped credential is an arca:// ref — the token itself lives as a secret in the local vault under the env it scopes; the ref path is the wire token_id:
[vaults.cloud.tokens]
dev = "arca://ops/cf-token-dev"
prod = "arca://ops/cf-token-prod"printf '%s' '<dev-token-secret>' | arca put arca://ops/cf-token-dev --env dev
printf '%s' '<prod-token-secret>' | arca put arca://ops/cf-token-prod --env prodWith a scoped token configured the client signs every request — Authorization: ArcaHmac <token_id>, <ts>, <sig> where sig = hex HMAC-SHA256(token, method+path+ts+sha256(body)) and ts must be within the worker's 60-second replay window. The token never travels. Bearer stays accepted during migration, so a mixed fleet upgrades gracefully.
Rotation is wrangler secret put — replace the value inside ARCA_VAULT_TOKENS, then arca put the new secret at the token ref. There is deliberately no admin route.
Private refs (opt-in)
By default ref and env are visible to the store — values are ciphertext, but which secrets exist is metadata. With private_refs = true the client derives ref_key = HMAC(identity-derived-key, env + "/" + ref) and uses it as the storage key; each record also carries a name blob — age ciphertext of the real ref — so list maps keys back while the store sees only hex:
[vaults.cloud]
kind = "cf"
url = "…"
private_refs = trueExisting rows aren't rewritten by the flag alone — run:
arca migrate --private-refswhich re-keys every plaintext-named row (ciphertext moves verbatim, old keys deleted after new ones land). A vault mixing plain and private rows is mixed-mode: list/check fail closed until you migrate — deliberately, since reading the wrong interpretation silently is worse than failing. Turning private_refs off again on a private vault fails closed the other way; migrating back is a manual re-put.
Client-side crypto boundary
put seals the value with age before it leaves the machine; get unseals after it arrives. The wire — and the D1 row — carries {"ciphertext", "name", "version", created_at, rotated_at}. A compromised worker yields ciphertext and, at most, opaque keys — not values, not ref names (with private_refs). Your local identity.txt is the only key, same file the local age vault uses; the private-refs derivation is keyed by it, so rotating the identity means a full re-key + migrate.
Verify
arca check --env prod
# vault "cloud" (cf): readable ← probes the worker over HTTPSAn unreachable worker or a bad token shows up as a finding — check fails open in the report, never panics.
Deploy sinks vs the vault
Don't confuse the two CF surfaces:
[vaults.cloud] kind = "cf"— a readable ciphertext store (your Worker + D1). Source of truth.[deploy.prod] kind = "cf-secrets-store"— a write-only Cloudflare Secrets Store sinkarca deploypushes plaintext into for Workers to consume via bindings. See deploy to a sink.