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:
- Wizard — consultant fills out a form (client, package tier, integrations)
- Board DB — stores the client record + which components are in scope
- Live runbook — a generated page the consultant follows on-site (or remote-in), with
export FOO=...steps and "copy secret" buttons - 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-managedorclient-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:
- 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.
- 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.
- Forces the tagging discipline up front. Tagging every slot
our-managedvsclient-suppliedduring 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
- AES-GCM per-client key (derived from a board-level master + client-id HKDF)
- Master key in Mac env (
KEYCHAIN) for dev, AWS KMS or similar for prod - Audit log of every decrypt:
{user, secret_slot, client_id, timestamp}→ separate table, append-only - Secret values never logged, never in error messages, never in analytics
- 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)
- Master key storage in production — Keychain on which machine? AWS KMS? HashiCorp Vault? Needs ops decision.
- 1Password account structure — shared team vault or per-client vaults? Affects the
hintfield format. - Offboarding SLA — how many days after cancellation do we purge
client_credentials? Default 30? - Support visibility — can support staff view redacted slot list for a client without decrypting? (Yes, via
component_credential_slotsjoin; values stay server-side.) - 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:
- Read this file and
2026-04-21-jed-macmini-setup-wizard-design.md(the parent spec) together - Close out §7 item 3 (credentials model) in the parent spec by copying the "Recommendation" section above into the decision log (§12)
- Answer the 5 Open Follow-ups with the consultant + partner
- Then move to the writing-plans skill with C as the locked choice
- First schema migration should add
component_credential_slotsandclient_credentialstables (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. |