Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .product-factory/discovery/pool-accounts-2026-08-18/DECISIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Decisions — pool-passport

Run: pool-accounts-2026-08-18. Base commit 82d3104.

| # | Decision | Class | Basis |
|---|---|---|---|
| D1 | Magic links are copy-and-send. The server never sends email. | Settled | User selected. No SMTP anywhere in repo; a ~50-person pool does not justify a sending domain, DKIM, bounce handling, and an API-key secret. |
| D2 | Members sign in with email + argon2id password. Passkey (WebAuthn) is an optional additional credential a member may enrol and then use instead of the password. | Settled | User selected "password primary, passkey optional add-on". |
| D3 | All 50 existing PoolUsers keep working and are auto-converted into guest passes preserving their existing `ID`. | Settled | User selected. ID preservation is forced by `user_hourly_usage` keying (`storage.go:347`) and `purgeNonPoolUsers` (`storage.go:1233`). |
| D4 | A guest pass may use the pool and see only its own usage. It may not see other users, pool-wide analytics, or manage provider accounts. | Settled | User selected option 1. |
| D5 | A guest pass carries an **optional** expiry set at creation; default is no expiry. | Settled | User selected option 3 as an addition to option 1. |
| D6 | Every guest pass carries an admin-editable free-text note recording who it was handed to. The note is required at creation. | Directed | User instruction mid-run: "an admin way of tagging these with a note so we can write down who it was." |
| D7 | Upgrade the product to Go 1.25; use `go-webauthn/webauthn` v0.17.4 and `x/crypto` v0.52.0 for the server half, `@simplewebauthn/browser` for the browser half. | Settled | Preflight proved v0.17.4 requires Go 1.25. Darvell explicitly selected upgrading the toolchain rather than using WebAuthn v0.15.0 on Go 1.24. |
| D8 | argon2id via `golang.org/x/crypto/argon2` v0.52.0, promoted to direct as part of the approved Go 1.25/WebAuthn upgrade. | Settled | Required by go-webauthn v0.17.4; Darvell approved the toolchain upgrade. |
| D9 | Browser auth is an opaque server-side session in an `HttpOnly; Secure; SameSite=Strict` cookie. `localStorage` stops holding credentials. | Confirmed | Join and recovery tokens live in URL fragments, are removed from history, and are POSTed same-origin before the cookie is set; no cross-site cookie is required. Today `friendSession` holds plaintext long-lived provider credentials in `localStorage` (`types.ts:3-14`). |
| D10 | The join link is a bearer credential, multi-use until expiry or revocation, and bookmarkable. | Recommended | Single-use breaks re-entry on a new device, which defeats the zero-friction goal. Leak exposure is bounded by revoke-and-reissue plus a visible distinct-origin count per pass. Stated as an explicit no-go boundary rather than hidden. |
| D11 | Revocation is enforced by a principal-status check on every user-facing route plus a per-principal `CredentialsValidAfter` cutoff checked against each credential's existing signed issue time. | Confirmed | Review rejected an epoch field: the old Gemini API-key and Claude parsers require exactly three fields, so the shape and rollback claims were false. All four access formats already carry a signed timestamp. A zero cutoff preserves all 50; advancing one principal's cutoff revokes only that principal without changing any envelope. |
| D12 | The four provider credential envelopes are unchanged. Identity is replaced above the credential layer. | Confirmed | CLIs parse these shapes; documented as deliberate at `pool_users.go:350-353,364-366,410-412,562-565`. |
| D13 | The analytics hash salt is decoupled from the friend code before the friend code is removed. Existing `origin_*` history is preserved by keeping the historical salt value as a frozen `analytics_salt` config key. | Confirmed | `poolHashSalt(friendCode)` (`utils.go:58-64`) feeds every anonymous origin ID. Removing the code without this orphans all origin history. |
| D14 | DuckDB is the canonical analytics ledger; Bolt remains the control-plane store and carries a durable analytics outbox. | Settled | User selected DuckDB after challenging the aggregate-bucket design. The current SQLite queue silently drops (`analytics_store.go:165-171`), while adding a Bolt bucket for every dimension calcifies future questions. Each completed usage observation is first committed to a Bolt outbox, then idempotently appended to DuckDB and removed only after commit. |
| D15 | One immutable `usage_events` fact table retains request-level dimensions and the cost calculated at ingestion, including pricing-version provenance. Hourly, daily, provider, model, and user series are SQL views/queries, not permanent bucket families. | Recommended | DuckDB is built for analytical grouping and column scans. Request-level facts preserve future questions. Storing the calculated cost and price version prevents later pricing-table changes from rewriting history. |
| D16 | Request-level DuckDB events are retained indefinitely at the approved envelope; raw Bolt `usage_requests` remains a 30-day recovery source, and the redundant long-lived Bolt user/global hourly buckets are retired after migration validation. | Recommended | At ~500k requests/month, ~6M rows/year is ordinary for embedded DuckDB and is more valuable than lossy downsampling. Production `proxy.db` is already 627MB largely because Bolt stores raw JSON and overlapping aggregates. |
| D21 | DuckDB queries run only in the codex-pool process, with one writer connection, bounded reader connections, statement timeouts, and memory/temp-directory limits. | Confirmed | DuckDB supports concurrent reads and appends within one process, but its native file is not a general multi-process write store. The CLI must not be pointed at the live file while the service owns it. |
| D22 | The Linux release binary is built inside a pinned Linux container because the official DuckDB Go driver uses native bindings. | Confirmed | The existing `GOOS=linux GOARCH=amd64 go build` from macOS is no longer sufficient. The official `github.com/duckdb/duckdb-go/v2` client is used at the pinned DuckDB release; Docker produces the deployable binary. |
| D23 | Bring-your-own-provider-credential passthrough remains enabled and untracked. It is explicitly outside pool-capacity and per-user accounting. | Settled | User selected. The dashboard reports aggregate passthrough request volume and labels pool totals as excluding passthrough. It does not fabricate identity or cost from traffic whose upstream credential belongs to the caller. |
| D24 | DuckDB is not a user-query surface. Only predefined parameterized queries run; extension autoinstall/autoload and external file/network access are disabled. | Recommended | Embedded analytical engines can read files and load extensions. The product needs charts, not an SQL workbench. This removes an unnecessary data-exfiltration and supply-chain surface. |
| D25 | If durable analytics recording fails, reserve space provides a grace window; after exhaustion pool traffic keeps serving but the product records and displays an explicit accounting-gap interval once storage recovers. | Settled | User selected. Availability wins after the grace reserve, but the UI never presents totals spanning the gap as complete. |
| D26 | Guest-link and client-setup-token plaintext is encrypted at rest under a dedicated `POOL_AUTH_ENCRYPTION_KEY`; its SHA-256 digest remains the lookup key. Sessions and single-use recovery links stay digest-only. | Recommended | The product promises authorized re-copy of multi-use guest and setup links. A digest cannot provide that; plaintext storage makes a database leak usable. AEAD preserves re-copy without conflating it with session retrieval. |
| D27 | Every principal may mint up to 20 labelled client credentials. Each embeds principal + client ID in the existing identity slot of all four envelope formats and receives independent analytics, expiry, rotation, and revocation. Existing credentials map to `legacy-default`. | Directed | User requested minting their own token for per-machine stats. Labels follow the token and are not hardware attestation. |
| D28 | The authenticated dashboard is self-contained: self-hosted fonts, strict CSP, no-store, no framing, no referrer, and no service-worker caching of authenticated data. | Recommended | Identity, usage, guest links, and provider setup are sensitive. Third-party font/script origins and browser/shared caches add exposure without buying a product property. |
| D29 | Any principal may set a nickname and a short avatar emoji/glyph. Analytics resolves current profile metadata by principal ID rather than copying it into immutable facts. Image uploads are excluded. | Directed | User requested nicknames and little avatars. Glyphs deliver the social identity without image storage, decoding, crop, moderation, or backup lifecycle. |
| D17 | Timestamps stay UTC in storage; the browser renders in its own timezone with an explicit UTC toggle. | Recommended | Stored UTC hour already supports this (`storage.go:341`). Timezone-aware buckets would be a second, divergent write path. |
| D18 | Latency, HTTP status, and error-class analytics are excluded from this release. | Recommended | Real gap (`RequestUsage` has no duration field, `pool.go:170-196`) but not requested. Adding it means a new field threaded through ~12 recording call sites for a question nobody asked. |
| D19 | `templates/friend_landing.html` is deleted, along with its `go:embed` entry. | Confirmed | 3989 lines / 211KB, embedded at `frontend.go:21`, read by no Go code. Only reference is an assertion in `provider_xiaomi_test.go:482`. It encodes a divergent design system. |
| D20 | The Signal Room visual language is preserved exactly. New surfaces are built from the existing tokens, panel grammar, and section codes. | Recommended | It is coherent, accessible, and distinctive. A second design system inside one binary is how the current dead template happened. |

## Rejected

- **Per-user friend codes.** Keeps a shared-secret model with more secrets. Does not give sessions, revocation, notes, or expiry.
- **An external identity provider (Auth0/Clerk/WorkOS).** Adds a network dependency and a vendor to a single-binary droplet deployment, for 50 users, and does not solve the provider-credential-envelope problem at all.
- **Postgres for the identity store.** The deployment is one binary plus embedded stores. Bolt already holds the analytics this must join against.
- **Keeping `pool_users.json`.** Whole-file rewrite on every mutation with no transaction (`pool_users.go:67-77`). Adding sessions, passkeys, notes, expiry, and audit to it multiplies the corruption window.

## Unresolved design-changing decisions

None.
Loading