Arcwayv0.3.0

Vault

Arcway's vault is an app's cryptographic backbone: it derives the framework's intrinsic secrets, and exposes a crypto toolkit on ctx.vault for your handlers. Everything starts from a single root key —

Arcway's vault is an app's cryptographic backbone: it derives the framework's intrinsic secrets, and exposes a crypto toolkit on ctx.vault for your handlers. Everything starts from a single root key — the master secret — from which the framework deterministically derives everything else. You manage one value; the framework derives what it needs, and your app derives its own keys from the same root.

The master secret

The master secret is a 32-byte key, encoded as v1:<base64-32-bytes> (a 32+ character raw string is also accepted). Generate one with the CLI:

npx arcway vault generate-key

Configure it under the vault block in arcway.config.js. You wire it to wherever you keep secrets — an env var of your choosing, a secrets manager, or a literal:

export default {
  vault: {
    masterSecret: process.env.MY_MASTER_SECRET,
  },
};

There are no assumed environment-variable names. Arcway never reads a hardcoded ARCWAY_MASTER_SECRET (or any other implicit name) behind your back — the only place a secret enters the framework is the vault config you write. The wiring stays explicit: nothing is silently picked up from the environment where a stray variable could change behaviour.

Derived secrets

From the master key, Arcway derives its intrinsic secrets with HKDF-SHA256 (salt arcway:infra-secrets:v1, a distinct info namespace per secret), so the values are independent and unrecoverable from one another:

SecretUsed by
sessionCookie session signing/encryption
plugin-vaultPlugin vault encryption
mcp-apiMCP debug endpoint auth
jwtManaged ctx.vault JWT signing/verification
callbackSealed callback-token encryption/authentication

These five are the only secrets the framework derives, because they're the only crypto the framework itself needs. Anything application-specific — push keys, app API tokens, custom signing keys — is the app's concern; derive those yourself from the same master with the primitives below, so you still manage just one root secret.

Deriving your own keys

ctx.vault exposes two deterministic derivation primitives, so your app gets the single-root-secret benefit without baking app concepts into the framework:

// symmetric key material for a label
const apiKey = ctx.vault.deriveKey('my-service-api');

// deterministic asymmetric keypair (EC; defaults to P-256)
const { publicKey, privateKey } = ctx.vault.deriveKeyPair('vapid');

deriveKey(label) returns symmetric key bytes. deriveKeyPair(label, { curve = 'prime256v1' }) returns a deterministic { publicKey, privateKey } EC pair — expose the public half, keep the private half internal. Both are stable: the same master + label always yields the same key, on every process and box, with no shared key store. (Web-push VAPID keys, for instance, are just deriveKeyPair('vapid') — an application concern, not a framework one.)

Providing the master secret

The vault requires a master secret whenever it's enabled — there is no auto-generation, in any mode. If vault.masterSecret is unset, the boot fails closed with an error pointing you to npx arcway vault generate-key. Dev and production behave identically: you always provide a key, explicitly.

arcway bootstrap does this for you on a new project — it generates a key into a fresh .env and scaffolds the config to read it:

# .env — created by bootstrap
ARCWAY_MASTER_SECRET=v1:…
// arcway.config.js — scaffolded by bootstrap
vault: {
  masterSecret: process.env.ARCWAY_MASTER_SECRET,
}

The env-var name is just the scaffold's choice, wired explicitly in your config — rename it freely; the framework only reads what vault.masterSecret points at.

Rotation — the keyring

masterSecret accepts an array as well as a string. As an array it's a keyring, ordered oldest → newest, with the last element active:

vault: {
  masterSecret: [process.env.OLD_MASTER, process.env.NEW_MASTER], // last = active
}

The active (last) key encrypts and signs everything new. Older keys stay as decrypt/verify fallbacks: on read, Arcway walks the ring newest → oldest until one works, then re-encrypts at-rest data under the active key on the fly. To rotate, append a new key and redeploy; once everything sealed under an old key has aged out (sessions expired, data re-encrypted), drop it off the front. The ring can be as deep as you like, so rotations never force a flag-day re-encryption.

Sessions ride the same ring: the per-key session secrets are all handed to the cookie layer under stable, key-derived ids, so rotating the master doesn't invalidate existing sessions — a cookie sealed under any key still in the ring keeps validating, regardless of where that key sits in the array.

The vault API

Beyond key derivation, every request context carries ctx.vault — the crypto toolkit your handlers use directly: password hashing, JWT encode/decode, symmetric encryption, ID generation, and the deriveKey/deriveKeyPair primitives above. ctx.vault.jwtEncode(payload, options) and ctx.vault.jwtDecode(token, options) are keyless by design: Arcway signs and verifies with the managed jwt secret derived from the master. If your app needs JWTs signed by a custom external key, use a JWT library directly instead of the managed vault helper.

Those utilities are covered in Sessions & Vault; this page is about where the keys behind them come from.

On this page