Spec

Credentials & Secrets Decision

Option C locked (hybrid): our-managed in board DB, client-supplied in 1Password.

docs/superpowers/specs/2026-04-21-jed-macmini-credentials-decision.md

Jed Mac Mini Setup Wizard — Credentials & Secrets Decision

Date: 2026-04-21 Session source: Other session brainstorming the wizard → runbook flow Companion doc: docs/superpowers/specs/2026-04-21-jed-macmini-setup-wizard-design.md (may live at /home/krish/board/... on geo) Status: Open decision — needs consultant + partner sign-off before implementation


Context

The Mac Mini deployment flow is:

  1. Wizard — consultant fills out a form (client, package tier, integrations)
  2. Board DB — stores the client record + which components are in scope
  3. Live runbook — a generated page the consultant follows on-site (or remote-in), with export FOO=... steps and "copy secret" buttons
  4. Installed state — Mac Mini provisioned, credentials live on the client's machine, board tracks what was installed

Credentials that will show up during a typical engagement:

  • Our-managed: OpenRouter API key (billed through us), Claude Code auth, our MCP skill licenses
  • Client-supplied: Gmail OAuth, Slack bot token, CRM API keys, MLS credentials, Shopify, Plaud, Canva — anything tied to their identity or billing

Question: where does each secret live during wizard → runbook → installed?


Options on the Table

A. All secrets in board DB

  • Consultant types every secret into the wizard
  • Board stores encrypted-at-rest
  • Runbook page renders "copy secret" buttons inline
  • Secrets persist in board DB as part of the client record (for monthly support)

Pros: Simplest flow. One-stop shop. Support team can diagnose without chasing the client. Cons: Board DB becomes a high-value target. Compliance burden grows (per-client encryption keys, key rotation, breach disclosure). Client integrations (their Gmail) end up on our infra.

B. All secrets out-of-band

  • Wizard stores only slots (names + hints), never values
  • Runbook steps instruct consultant to fetch each one in real time: "Paste OpenRouter key now (from 1Password: client-X-openrouter)"
  • Board never sees a secret value

Pros: Safer. No encrypted-secret blast radius. Clear audit path — the vault is the source of truth. Cons: Depends entirely on 1Password / external vault discipline. Friction during install if consultant is offline or slow on the vault. Lost-credential recovery is harder.

C. Hybrid — our credentials in DB, client credentials out-of-band (recommended)

  • Our-managed secrets (OpenRouter, Claude Code, anything billed through our monthly fee) live in board DB encrypted-at-rest
  • Client-supplied secrets (Gmail, CRM, Shopify, Plaud) stay in 1Password or the consultant's vault; runbook references the vault location
  • Each wizard slot is tagged our-managed or client-supplied

Pros: Matches the billing model — we pay for OpenRouter, we provision the key, we track usage. Client identity integrations never touch our DB. Clean audit: board DB = our infra, vault = client-specific. Consultant has one code path per tag. Cons: Two systems to manage. Tag definitions need to be correct from day one (miscategorizing a secret has legal implications).

D. Defer

  • Wizard v1 collects no secrets
  • Consultant handles all of it manually off-screen
  • Credential layer added in v2 once we see how it plays out with the first client

Pros: Ships v1 fastest. No security architecture decisions blocking launch. Cons: First-client experience is worse. We collect no data on which secrets are actually needed, so v2 is still guesswork.


Recommendation

Option C — Hybrid. Three reasons:

  1. Matches the billing model. OpenRouter is almost certainly part of the monthly support fee — we provision the key, we bill through it, we track usage. That secret should live where we can rotate and monitor it. Client Gmail tokens should not.
  2. Clean audit path. Board DB = our infra, 1Password = client-specific. If a lawyer ever asks "where is the client's data," the answer is one sentence.
  3. Forces the tagging discipline up front. Tagging every slot our-managed vs client-supplied during wizard v1 pays off in every downstream tool: support playbooks, offboarding, breach runbooks.

Option D (defer) is tempting for speed but loses the learning loop — we'd be guessing at v2 instead of iterating on real data.


Implementation Notes (for whoever picks this up)

Schema additions to the design doc's §9 draft

-- each component slot declares what credentials it needs
CREATE TABLE component_credential_slots (
  id SERIAL PRIMARY KEY,
  component_id INTEGER REFERENCES components(id),
  slot_key TEXT NOT NULL,             -- e.g. "OPENROUTER_API_KEY"
  display_name TEXT NOT NULL,         -- "OpenRouter API Key"
  tag TEXT NOT NULL CHECK (tag IN ('our-managed','client-supplied')),
  hint TEXT,                          -- vault path hint for client-supplied
  required BOOLEAN DEFAULT true
);

-- only filled for `our-managed` slots
CREATE TABLE client_credentials (
  id SERIAL PRIMARY KEY,
  client_id INTEGER REFERENCES clients(id),
  slot_id INTEGER REFERENCES component_credential_slots(id),
  encrypted_value BYTEA NOT NULL,     -- AES-GCM, per-client key
  created_at TIMESTAMPTZ DEFAULT NOW(),
  last_rotated_at TIMESTAMPTZ,
  UNIQUE(client_id, slot_id)
);
-- client-supplied slots are referenced only by name in runbook_steps;
-- the board never persists the value

Runbook step rendering

  • If slot.tag = 'our-managed': render <CopySecretButton secretId={...} /> that decrypts server-side on click and streams to clipboard (no DOM persistence, no logs).
  • If slot.tag = 'client-supplied': render <VaultHint path={slot.hint} /> that shows the 1Password/vault location and a "mark pasted" checkbox the consultant clicks after entering it locally.

Minimum security controls for v1

  1. AES-GCM per-client key (derived from a board-level master + client-id HKDF)
  2. Master key in Mac env (KEYCHAIN) for dev, AWS KMS or similar for prod
  3. Audit log of every decrypt: {user, secret_slot, client_id, timestamp} → separate table, append-only
  4. Secret values never logged, never in error messages, never in analytics
  5. Secrets dropped from DB automatically N days after client offboarding (configurable per client)

YAGNI for v1

  • Per-secret access control (one consultant role for now)
  • UI for rotating secrets (use DB script for first client)
  • Client-facing secret-entry page (consultant handles input)
  • Secret sharing between clients (not a thing — each slot is per-client)

Open Follow-ups (to resolve before coding)

  1. Master key storage in production — Keychain on which machine? AWS KMS? HashiCorp Vault? Needs ops decision.
  2. 1Password account structure — shared team vault or per-client vaults? Affects the hint field format.
  3. Offboarding SLA — how many days after cancellation do we purge client_credentials? Default 30?
  4. Support visibility — can support staff view redacted slot list for a client without decrypting? (Yes, via component_credential_slots join; values stay server-side.)
  5. OpenRouter key reuse — one key per client, or one pool key with per-client tracking? Cost vs blast radius.

Resume Instructions

When the Jed session picks this back up:

  1. Read this file and 2026-04-21-jed-macmini-setup-wizard-design.md (the parent spec) together
  2. Close out §7 item 3 (credentials model) in the parent spec by copying the "Recommendation" section above into the decision log (§12)
  3. Answer the 5 Open Follow-ups with the consultant + partner
  4. Then move to the writing-plans skill with C as the locked choice
  5. First schema migration should add component_credential_slots and client_credentials tables (tag-first, so every slot is born with the right classification)

Decision Log Entry (copy into parent spec §12)

Date Decision Reasoning
2026-04-21 Credentials model = Hybrid (Option C). Tag each slot our-managed or client-supplied. Matches billing model (OpenRouter is ours), keeps client identity data off our DB, enforces tag discipline from day one, clean audit path.