# Gift Graph Technical Decisions
> Gift Graph - Architecture Decision Records (TDRs): the "why" behind the build on Replit, v0.1 through v0.6.2 and the custom domain
> Last Updated: 2026-10-08 · Ordered newest first (add new TDRs at the top) · Mirrors `docs/technical-decisions.md` in the repo, with the decisions Agent recorded and the ones I made around it
## Format
Each TDR: **Context** → **Options Considered** → **Decision** → **Consequences**, with **Status:** `Accepted` | `Superseded` | `Proposed` | `Open`.
## Decision Map, by Row
The full map is on the [[Gift Graph Product Log#Decision Map|Product Log]]. Each row reads right to left, newest turn on the left. Copper is chosen, dashed copper is open, dim is declined.
![[gift-graph-decision-map-open-oct-08.png|720]]
*Open now · the six calls on the table on Oct 8*
![[gift-graph-decision-map-oct-08.png|360]]
*Oct 8 · the domain, and the account key it exposed*
![[gift-graph-decision-map-oct-07.png]]
*Oct 7 · v0.6.2: the seeded identities, hear, plain technical English, email, Teal ember, the audit*
![[gift-graph-decision-map-oct-06.png]]
*Oct 6 · Justin's first use: the card, the page for agents, the vanished hints, whose account*
![[gift-graph-decision-map-oct-05.png]]
*Oct 5 · OAuth, Hush, a lost link, one link for many, the release record, agent guidance, the nudge*
![[gift-graph-decision-map-oct-04.png]]
*Oct 4 · Neon, the handoff, who labels, the vocabulary grain, the noun, the order of work, no mail*
![[gift-graph-decision-map-oct-03.png]]
*Oct 3 · levels, a broad question, surprise-safe, the front door, invites, the self-migrating start, the reset at 006*
![[gift-graph-decision-map-open.png|720]]
*Open on Oct 2 · five calls, and the first four have since been settled*
![[gift-graph-decision-map-oct-02.png]]
*Oct 2 · copy, example pill, layouts, accounts, demo, uploads, and the state error*
![[gift-graph-decision-map-oct-01.png]]
*Oct 1 · the deck restructure, the repo, the live demo, then the v0.2 scope, drop model, judge, sign-in, production, and uptime*
![[gift-graph-decision-map-sep-30.png]]
*Sep 30 · from the idea to a deployed v0.1 in one night*
## Considerations Ahead
| Consideration | Where it stands | What decides it |
| --- | --- | --- |
| **The account split on the new domain** | Diagnosed 2026-10-08; the data sits intact on the old host | The fix in TDR-039 and its rehearsal on a branch of production ([[Project - Gift Graph/Build & Iterate/Bug Tracker#BUG-023\|Bug Tracker > BUG-023]]) |
| **Grants and circles** | Designed in `architecture.md`, section 6, as the next migration after the account fix | A design session with one-to-many in scope, public lists and reveal requests included |
| **A public list behind one link** | Asked 2026-10-04 and 2026-10-05; nothing built | Whether a posted link joins grants as one more kind of audience |
| **An account page** | Asked 2026-10-07 | v0.7, or a v0.6.3 beside the account fix |
| **The twenty-word cap** | Decided 2026-10-03 and never enforced; real secrets run twenty-five to thirty-five words | Raise it, retire it, or enforce it |
| **The web control for email requests** | Agent-only in v0.6.2 | Whether the dashboard needs it before the account page |
| **The audit's second half** | In the backlog | Each item in its own round: one session gate, the radiogroups, the connect sheet's tabs, the dashboard's command hook, the column drop |
| **Personas beyond the pair** | Open | [[Project - Gift Graph/Discovery & Planning/Product Plan#Who It Is For\|Product Plan > Who It Is For]] |
## TDR-039: Accounts Key on the Clerk User
**Date:** 2026-10-08 · **Status:** Proposed (fix pending) · **Related:** [[Project - Gift Graph/Build & Iterate/Bug Tracker#BUG-023|Bug Tracker > BUG-023]]
### Context
Gift Graph moved to giftgraph.aleksandar.app on 2026-10-08. In production the server derives the Clerk key from the host a request arrives on, which gives each domain its own issuer in the session. An account was keyed on the issuer and the Clerk user together, and my first sign-in on the new domain made a second, empty account.
### Options Considered
- **Key on the Clerk user alone** · one Clerk instance serves each database, which makes the user ID one person; the issuer stays on the row as a detail
- **Map every known host to one canonical issuer** · a list to keep in step with every domain Replit attaches
- **One domain, with the old one redirected** · leaves the same bug waiting for the next domain
### Decision
Key on the Clerk user alone, and fold an empty account a second domain made into the account with the data, in a migration rehearsed on a branch of production first. Until it ships, sign in at gift-graph.replit.app.
### Consequences
- A new domain changes nothing about who a person is
- The fold is a data migration, which takes the next number and moves grants one further
- An account with data on both sides waits for my call instead of a fold
## TDR-038: An Audit After a Large Change
**Date:** 2026-10-07 · **Status:** Accepted
### Context
Three days of renames ran across the server, the web app, and the tools, part of them written by parallel lanes. A sweep before the publish found a service class dead but for one method, a logger that logged nothing, an unmounted health route, a second hand-kept schema check, five web exports with no caller, and reads that took the owner's exclusive lock.
### Decision
Apply the plain wins before the publish, in one scripted pass that asserts every match, and send the findings that need a design of their own to the backlog. Reads run on the pool without the owner's lock, the dashboard's four reads run at once, and migration 011 adds the two indexes every owner read was missing. Long runs get a check every two minutes, and the environment gets one before any test run starts.
### Consequences
- Every removal went in with the full round green, 85 API tests among them
- A hear never waits behind the owner's own change
- The web refactors, the vestigial columns, and the judge's seed wait for rounds of their own
## TDR-037: Teal Ember
**Date:** 2026-10-07 · **Status:** Accepted
### Context
Oxblood and cream read as the familiar Claude palette. The new design handoff carried a dark palette, teal and ember on charcoal, with tokens named by role.
### Decision
Copy the handoff's tokens into the app and read them everywhere through semantic `--gg-*` names. Teal for Secret and primary actions, ember for Hint and focus, warm grey dashes for Silent, and the level icon colors itself by level. Clerk's widgets and the page for agents take the same values.
### Consequences
- A dark palette carries the sense of secrecy the product is about
- A light theme is one token file away, with no component touched
- The decision map keeps the vault's copper, since it belongs to the garden
## TDR-036: Ask to Hear Someone by Email, With One Answer Either Way
**Date:** 2026-10-07 · **Status:** Accepted (agent-only)
### Context
An invite link asks the owner to do the sending. Asking by email lets the person who wants to hear do the asking, which is the direction a first conversation runs. An address is itself a secret, and a lookup that answers differently for a known address tells the asker who has an account.
### Options Considered
- **A pending connection row on a match** · built first and caught in review, since the requester could see the row and learn the account existed
- **A request held on the owner's side alone** · the same answer whether or not an account matches
### Decision
`request_connection` takes an account ID or an email, one of the two. The server stores a keyed hash of each verified sign-in address, taken at sign-in, and stores no address. A request waits on the owner's side until the owner answers, a decline claims it with no row made, ten requests a day per account, and a request waits fourteen days for someone who has not joined. The key is `EMAIL_HASH_KEY`, set once and never changed.
### Consequences
- A declined request and an unknown address look the same from the outside
- The web control waits; the agent is the only door for now
- A changed key or a changed primary address leaves a stale hash, which sits in the backlog
## TDR-035: One Vocabulary at the Agent's Door
**Date:** 2026-10-07 · **Status:** Accepted · **Amends:** TDR-021
### Context
A first-time agent had to learn that drop meant secret and tier meant level before it could map a request to a tool. Justin's Muse spent its first minutes guessing.
### Options Considered
- **whisper and pull** · pull was the one word no page used
- **whisper and hear** · the product already speaks in hearing, on the home page, the dashboard, and the connect note
### Decision
The tools say what the pages say, `whisper`, `hear`, `list_secrets`, `update_secret`, `hush_secret`, `set_level`, and `remove_connection`, with `secret`, `level`, `hint`, and `labels` as the fields. Everything an agent reads follows ASD-STE100, plain technical English, one instruction per sentence. A `help` tool returns the flows and the consent rules as data, and three prompts start the same flows as commands. The database columns keep their names.
### Consequences
- The monitor's profile `v0.5` names the eighteen tools
- A client that cached the old names reconnects once
## TDR-034: The Seeded Identities Retire
**Date:** 2026-10-07 · **Status:** Accepted · **Supersedes:** the seeded users of TDR-001
### Context
The two seeded accounts from v0.1 were the one loose end in production after the Muse scare, and a first-time agent read the prototype's leftovers, a legacy mode and an audience-approval tool, with everything else.
### Decision
Migration 009 removes the seeded identities with everything in their name, the token sign-in, the env tokens, and the one-person mode. Every account is a sign-up through Clerk, and the users check allows nothing else. A leftover one-person secret on a sign-up account moves into its owner's Silent drawer with its words, level, and line.
### Consequences
- Every account comes in through one door, a sign-up
- The rehearsal on a branch of production confirmed every sign-up account, connection, and whisper kept
## TDR-033: One Surface Answers One Question
**Date:** 2026-10-06 · **Status:** Accepted
### Context
Justin's first use, on a phone. The two-column connection card stacked and ran long, and he met his own secrets before the answer he wanted. His agent opened the MCP URL as a page and found a bare 401. A day earlier, Muse had taken the person it hears for the account it holds, since one line at connect named both.
### Decision
A connection card holds one direction, the level the person hears at and what I hear from them, and each secret card names who hears it as person pills. The MCP URL opened as a page answers with a page for agents, the tools listed from the server itself. The note at connect names the account alone on its first line.
### Consequences
- The two-column card and its thinking stay on record in the repo
- The same rule now governs the agent's notes and the dashboard's cards
## TDR-032: The Agent Gives the Web App's Steps
**Date:** 2026-10-05 · **Status:** Accepted
### Context
Before Gift Graph went in front of other people, the web app guided and the MCP route did not. The data for asking someone back was already in the result, and nothing told the agent what to do with it, or to wait for a yes before confirming a preview.
### Decision
A result carries `next`, one plain sentence with the call that makes the step, wherever the web app offers one. Every preview tells the agent to show the owner who hears what and to confirm only on their yes. The benchmark is `agent-parity.md`, flow by flow.
### Consequences
- A sentence in the result reaches the agent at the moment it decides
- Through Claude, my agent showed each secret beside what others would hear and waited before saving
## TDR-031: The Release Record
**Date:** 2026-10-05 · **Status:** Accepted
### Context
Versions lived in the monitor's tool profiles and nowhere a person reads. I wanted to see at a glance what is live, what is built and waiting, and what is only scoped.
### Decision
The README's Versions section is the record, one bullet per feature, and each heading names the date it went out. Each publish from v0.6 on carries a git tag of the same name, and every backlog item carries its built and released dates.
### Consequences
- v0.6.1 went out inside v0.6.2 and has no tag of its own
- The server reports its version in the MCP handshake
## TDR-030: Shared and Silent, and Withdrawal Becomes Hush
**Date:** 2026-10-05 · **Status:** Accepted · **Supersedes:** TDR-013
### Context
A withdrawal that could not be undone made a person choose between Silent and gone for good, with nothing to tell the two apart from the outside.
### Decision
Secrets sort into two buckets, Shared and Silent. Withdraw becomes Hush on the card. A hushed secret keeps its level and hint, leaves every future hearing, and stays out through reapproval. Its owner shares it again through the same review as any widening.
### Consequences
- Two buckets are easier to hold than four states
- What someone has heard still cannot be taken back, and the dashboard says so
## TDR-029: Agents Sign In With OAuth, Each Agent a Row
**Date:** 2026-10-05 · **Status:** Accepted · **Supersedes:** TDR-014, TDR-017
### Context
A copied token was the hardest step for a person new to MCP. Clerk here is managed by Replit, with no place to set up OAuth applications, client registration, or a consent screen.
### Options Considered
- **Clerk as the authorization server** · unavailable under Replit's managed Clerk
- **Gift Graph as its own authorization server** · the MCP SDK's handlers on a Postgres provider
- **Pasted tokens only** · the step people drop
### Decision
Gift Graph runs its own OAuth. An agent adds the server by its URL, the person allows it on a consent page signed in with their web account, and the agent gets an hour's access token and a rotating thirty-day refresh token bound to `/mcp`. A second use of a refresh token or a code disconnects the agent. Each agent is a row in Your agents, and an agent without sign-in holds a pasted token of its own. Migration 008 moved each pasted token into an agent of its own.
### Consequences
- A person registers once, on the web, and OAuth registers that person's agents to the account
- Codex registers again at every sign-in and shows a second row until the first is disconnected
## TDR-028: The Web App First, Then OAuth, and No Mail
**Date:** 2026-10-04 · **Status:** Accepted
### Context
The web app is where most people meet Gift Graph, through a link or a screenshot I share. Polish at the first touch has paid off at every place I have worked.
### Decision
The surfaces from the design handoff come next, then OAuth. Embeddings, the drafter, and grants wait behind both. Gift Graph sends no mail; an invite travels through the person's own agent or the share sheet on a phone.
### Consequences
- Embeddings matter less now that the agent picks the label
- Mail stays out of scope until a feature needs it
## TDR-027: The Noun Is Secret
**Date:** 2026-10-04 · **Status:** Accepted
### Context
I had been saying both whisper and secret for the thing a person writes, and the handoff left the title open. "A whisper heard as a Secret" made the levels hard to read.
### Decision
The thing a person writes is a secret, on every page, in every tool reply, and in the docs. Whisper is the verb. The levels stay Secret, Hint, and Silent.
### Consequences
- Secret says what the thing is and what sharing it costs
- The tools caught up on 2026-10-07 (TDR-035)
## TDR-026: The Owner's Agent Picks the Label
**Date:** 2026-10-04 · **Status:** Accepted
### Context
The vocabulary audit scored version 1 as too fine and too noisy, and no word list reaches brands. The agent writing the hint already knows that a Baies is a candle.
### Options Considered
- **Embeddings on the server** · measured later, adopted only if they beat the list on the blind set
- **A drafter on the server** · sends the secret across a new boundary
- **The owner's agent picks from the full list** · processing on each person's side
### Decision
Version 2 of the vocabulary goes live, sixty-three kinds at the grain of a shop department, through migration 007 and a recorded map. The preview hands the agent the whole list, about two hundred tokens and only when whispering, and the server checks the pick. Nothing trains a model; the audit sets stay on my machine.
### Consequences
- A second blind set confirmed the first before the switch
- Every stored hint kept or broadened its label
## TDR-025: The Database Moves to Neon, and the Build Moves Local
**Date:** 2026-10-04 · **Status:** Accepted · inflection point
### Context
Every database refactor was written and tested locally and then handed to Replit to perform. Replit performed each one well, and each hand-off still cost a pull, a restart, a verification session, and a schema review.
### Decision
Postgres moves to Neon, with production and dev branches. Claude Code builds locally in WSL against Docker Postgres, works on the dev branch directly, and rehearses migrations on a branch of production. Replit keeps hosting, sign-in, the judge, and Publish.
### Consequences
- The token-rich side does the token-heavy work
- Replit's share is now the share a team would choose to keep there, which keeps my evaluation of it honest
- Replit Agent runs no SQL against Neon; the app's own start is the path
## TDR-024: The App Migrates Itself at Start
**Date:** 2026-10-03 · **Status:** Accepted · **Supersedes:** TDR-015
### Context
Production fell three migrations behind the moment attention moved on, and the hand-run step needed a shell and a copied production URL.
### Decision
The API server applies the guarded migration list at every start, under an advisory lock, and `/health` names the schema. A migration with data steps counts as applied only when its last constraint exists, or a condition on its data holds. A constraint over existing rows ships in two publishes, since Publish's schema review cannot be skipped.
### Consequences
- The publish loop is a merge, a publish, and a check of `/health`
- The first publish through the loop failed at the review, and production went out at 006 by reset
## TDR-023: Invites, a Link as Consent Given Ahead of Time
**Date:** 2026-10-03 · **Status:** Accepted
### Context
Getting Justin on needed an account ID sent by email, which is the step people drop.
### Decision
An owner mints a single-use link at Hint or Secret, open fourteen days, previewed before it exists, shown once and stored as a hash. Opening it signed in makes the connection and offers the share-back.
### Consequences
- A lost link gets a new one, and the old one stops (2026-10-05)
- Removing a person closes the open links in their name (2026-10-07)
## TDR-022: Hearing Returns Everything Allowed, Ranked
**Date:** 2026-10-03 · **Status:** Accepted · **Amends:** TDR-005
### Context
"Does Justin have any gifts shared with me?" returned nothing while "premium food" found his whisper. Literal filtering made a broad question look like an empty graph.
### Decision
A read returns every secret the asker may hear, with the ones that match the question first. One function projects each secret to what this asker may hear, and ranking reads only that projection.
### Consequences
- The asking agent does the reasoning about fit
- A question that names a hidden word surfaces nothing hidden
## TDR-021: Three Levels, With Labels on the Hint
**Date:** 2026-10-03 · **Status:** Accepted · **Supersedes:** TDR-004 · **Amends:** TDR-011
### Context
In production a category line leaked nearly the whole secret, and the form could not say why a category needed text.
### Decision
A secret travels at Secret, Hint, or Silent. A hint is up to two labels from a versioned public vocabulary plus one line the server checks for the secret's distinctive words and for numbers. What a person hears is the stricter of the secret's level and the connection's. The surprise-safe flag retires; its need belongs to the platform as Act.
### Consequences
- A label from a finite list reveals at most the choice among its entries
- The line is the one unbounded channel, and it alone is checked and approved
## TDR-020: Compact Layouts and Viewport Checks as a Standing Rule
**Date:** 2026-10-02 · **Status:** Accepted
### Context
The example pill fix showed how a page can look right at one width and wrong at another. Agent offered automated screen-size checks.
### Decision
Compact layouts are a standing preference in `replit.md`, and Agent checks pages at 1280x800, 1440x900, and 390x844 before reporting a layout change. Automated screen-size checks stay open until a page changes often enough to need them.
### Consequences
- Every layout change comes back with three viewports looked at, which costs a little time per change
- The rule lives beside the copy skill, which keeps words and layout under the same review
## TDR-019: Uploads Stay Out of Git History
**Date:** 2026-10-02 · **Status:** Accepted
### Context
Three uploads sat in the workspace, the original zip, a screenshot, and a mock. Agent asked which should go into GitHub history.
### Decision
Exclude all three. Uploads are inputs to a session, and the repo carries what the session produced.
### Consequences
- Screenshots and mocks live in this vault, which is where the record of the session belongs
- A future contributor sees code and docs in history, with nothing to redact
## TDR-018: A Demo With Synthetic People
**Date:** 2026-10-02 · **Status:** Accepted, live since v0.2
### Context
The landing page explained the tiers with one example. Nothing let a visitor try the loop without a token.
### Decision
A demo page with two synthetic people, Theo as the giver and Mira as the recipient, three whispers on Mira's side with editable tiers, four prompts on Theo's side, and a reset. The demo touches no account. The copy skill wrote the words.
### Consequences
- The first surface a visitor can stand on without reading documentation, which is what Probe 1 needs for tier comprehension
- Two named personas, which is where the persona question in the Product Plan started
## TDR-017: Per-Account IDs and Self-Issued MCP Tokens
**Date:** 2026-10-02 · **Status:** Superseded by TDR-029 · **Related:** [[Project - Gift Graph/Discovery & Planning/Product Plan|Product Plan]]
### Context
v0.1 had two seeded users whose tokens I generated by hand and pasted into Replit Secrets. A third person could not join without me doing the same for them.
### Decision
Each account gets an ID (`gg_…`) and the dashboard offers Get MCP token, which issues that account's own credential, shown once. The dashboard also shows the endpoint, transport, and header line a client needs. No sign-in provider yet; this is the rung below Clerk on the ladder.
### Consequences
- Tokens no longer pass through my hands, which removes the rotation-after-a-screenshot problem from v0.1
- Static tokens remain the client-side credential until MCP OAuth, which leaves the ladder's next rung unchanged
## TDR-016: A Second Model Reviews Each Milestone
**Date:** 2026-10-01 · **Status:** Accepted
### Context
I asked whether Replit could run a model as judge while we build. Agent confirmed managed model access can run an evaluator beside the build, at usage cost, and offered two shapes, reviewing build changes or evaluating Gift Graph's responses.
### Options Considered
1. **Review changes as we build:** check each milestone against the approved requirements before committing
2. **Evaluate responses:** synthetic cases against returned disclosures, which sends preferences to a judge and adds a data-processing boundary
3. **Both**
### Decision
Review changes at milestones, from a different model family than the builder, with findings tied to requirements and source locations, blocking findings fixed before commit, and the outcome recorded beside the Git commit (`pnpm review:milestone`). Automated tests stay the ground truth; a model's approval is supplementary evidence. Response evaluation waits until it can run on synthetic cases only.
### Consequences
- Every milestone leaves a reviewed record, which is the audit trail an enterprise reviewer asks for first
- Agent would not disclose which model it runs, which makes "different family" a request with no confirmation (open on the map)
## TDR-015: Production Stays Behind an Owner-Reviewed Process
**Date:** 2026-10-01 · **Status:** Superseded by TDR-024 · **Related:** [[Project - Gift Graph/Build & Iterate/Bug Tracker#BUG-011|Bug Tracker > BUG-011]]
### Context
v0.2 changes the schema and the owner's model of what a preference is. The live server had been serving the demo for a day.
### Decision
The migration runs in development only (`migrate:dev`), adds without deleting or merging, and startup performs readiness checks only. Production follows the separate process in `docs/production-v02.md`, reviewed by the owner. Milestone review gates with an audit trail sit in `package.json` and `replit.md`, and read-only synthetic checks exercise the MCP tools without writing.
### Consequences
- The v0.2 landing page is live while the production migration status is a question to answer, since the two can diverge
- Every milestone leaves a reviewed record, which is the audit trail an enterprise reviewer asks for first
- 2026-10-02: Replit's database tooling validated the migration (two new tables, `owner_login_limits` and `owner_confirmations`, among the changes) and offered Create preview deploy or Approve and publish. Preview first, per Considerations Ahead
## TDR-014: The Dashboard Reuses the Owner Credential as a Session
**Date:** 2026-10-01 · **Status:** Superseded by Clerk sign-in, then TDR-029 and TDR-034
### Context
The owner needed a page to read and edit their drops, and v0.1 had one credential per user, the bearer token.
### Decision
The owner signs in at `/owner` with the existing credential. The app exchanges it for a six-hour HTTP-only session and clears the input; sign out removes the server session. No second credential, no new identity system before v0.2 sign-in on the ladder.
### Consequences
- One secret per person still, which keeps the rotation story simple
- The session cookie is a second place that secret's authority lives, for six hours at a time
## TDR-013: Withdrawal Is Permanent
**Date:** 2026-10-01 · **Status:** Superseded by TDR-030 · **Related:** [[Project - Gift Graph/Build & Iterate/Bug Tracker#BUG-006|Bug Tracker > BUG-006]]
### Context
v0.1 left open what a reconnect after revocation restores. Agent's Unsure note said renewed approval restores access to existing drops.
### Decision
A withdrawn preference stays out of every future pull, reapproval included. The original stays with its owner. There is no restore operation, no pull history, and no activity indicator. Revocation of a connection blocks future pulls; neither action retracts information an agent has already received.
### Consequences
- Reconnect semantics are settled: a withdrawn preference stays withdrawn, everything else returns with reapproval under the widening checks
- The landing page states the limit in one line, which is the right place for it
## TDR-012: Widening Waits for a Second Call
**Date:** 2026-10-01 · **Status:** Accepted
### Context
A tier increase is the one owner action that moves more text toward a requester.
### Decision
A tier increase returns a five-minute preview, and an explicit second owner call applies it. Edits and legacy conversion also require confirmation; expanding the audience uses its own approval. Any change to owner state invalidates outstanding previews. Decreases apply immediately. Approval and reapproval cannot bypass the widening checks.
### Consequences
- The pause sits exactly where the consequence is, on disclosure going up
- Narrowing never waits, because nothing new leaves
## TDR-011: Owner-Wide Preferences With a Ceiling
**Date:** 2026-10-01 · **Status:** Amended by TDR-021 · **Supersedes the open call in TDR-006**
### Context
v0.1 scoped every drop to one requester. The concept page described drops as the owner's, visible to any approved connection by tier.
### Decision
Both, explicitly. A new owner-wide preference chooses all approved connections as its audience and carries a ceiling; effective disclosure is the more restrictive of the ceiling and the connection's limit. The owner writes every lower-tier text that could become visible. Existing requester-scoped drops keep their single audience and snapshot disclosure until the owner completes any missing lower-tier text and confirms conversion; a separate approval opens a preference to all approved connections, future ones included.
### Consequences
- The spec's reading and Agent's reading both survive, with the owner choosing per preference
- Strict tier validation from v0.1 becomes "write every visible tier up to the ceiling," which costs the owner more typing and keeps the no-model-summaries rule intact
## TDR-010: Multi-Artifact Layout for the Review Deck
**Date:** 2026-10-01 · **Status:** Accepted (applied; tests not yet re-run in the new layout) · **Related:** [[Project - Gift Graph/Build & Iterate/Bug Tracker#BUG-009|Bug Tracker > BUG-009]], [[Project - Gift Graph/Build & Iterate/Build Log|Build Log]]
### Context
The publish screen offered a slide deck. After I approved a five-slide outline, Agent reported that the deck viewer needs a multi-artifact project layout and asked to move the deployed server into it first (Task #5, "Preserve Gift Graph server"). The server had been live for under an hour, the night before a call where I planned to demo it.
### Options Considered
| Option | Description | Pros | Cons |
|--------|-------------|------|------|
| **A. Not now** | Decline the restructure, build the deck in a separate app from the three docs | Zero risk to the live server | A second app to manage |
| **B. Approve** | Let Agent move the server into `artifacts/`, `lib/`, `scripts/` with a `.migration-backup/` | One repo holds server and deck | Paths the build and run commands depend on change |
### Decision
**Option B landed, with the live deployment held back.** The restructure ran on a separate copy, I pushed the working v0.1 to GitHub first, and the published build keeps serving until a republish. Task #6 (the deck) is queued behind it.
### Consequences
- The repo on GitHub carries the new layout (`artifacts`, `lib`, `scripts`, `.migration-backup`, `pnpm-workspace.yaml`)
- The live server still runs the pre-restructure build, which is what the demo tested
- Republish only after Agent confirms the 12 tests pass in the new layout
- Every checkpoint in the Agent panel can roll the workspace back to before Task #5
## TDR-009: Private Repo, Checkpoints as Commits
**Date:** 2026-10-01 · **Status:** Accepted · **Related:** [[Project - Gift Graph/Build & Iterate/Validation Log|Validation Log]]
### Context
Replit commits every checkpoint to a local git history and keeps its own `gitsafe-backup` remote. Nothing had reached GitHub. The repo holds my build notes and a first name in the origin story.
### Decision
Private repository `gift-graph` under my personal GitHub, created from the Git pane. `.cache/` and `.local/` added to `.gitignore` (they were never tracked). Commit author set to my GitHub profile. Description changed from "Imported from zip" to what the repo is.
### Consequences
- One source of truth, in this order: Replit writes code, GitHub receives it, my machine and the vault pull from GitHub
- The local clone into `Project Code` and the junction into this vault folder are still to do
- A public repo would need the origin story reworded first
## TDR-008: Feedback Widget Off
**Date:** 2026-09-30 · **Status:** Superseded (the v0.2 landing page has pages for a person, and the widget is on as of 2026-10-02)
### Context
The publish panel offers a feedback widget beside the access settings for every app.
### Decision
Off for v0.1. Gift Graph serves MCP tools with no human-facing pages, and the widget would only add a script to the server. Revisit once the owner review screen ships. I am very interested in the widget itself as a feedback capture surface; that interest lives in the backlog.
## TDR-007: Autoscale Deployment on a Stateless Transport
**Date:** 2026-10-01 · **Status:** Accepted · **Related:** [[Project - Gift Graph/Build & Iterate/Bug Tracker#BUG-001|Bug Tracker > BUG-001]]
### Context
The first Publish failed with "Could not find run command." The preview ran through the workspace's dev workflow, and `.replit` had no deployment section. Streamable HTTP can run with or without server-side sessions; if the server held sessions in memory, Autoscale could route a client's next request to an instance that does not know the session.
### Options Considered
| Option | Description | Pros | Cons |
|--------|-------------|------|------|
| **A. Autoscale** | Scale to zero, scale out per request | Cheapest at low traffic | Breaks if session state lives in memory |
| **B. Reserved VM** | One always-on instance | Sessions survive on one box | Pays for idle time |
### Decision
**Option A.** Agent checked the transport and found it creates a fresh transport per request with session IDs disabled, and all durable state lives in PostgreSQL. Build `npm run build`, run `npm start`. Region North America, 2 vCPU and 4 GiB RAM.
### Consequences
- The compiled server started locally with `/health` at 200, root at 200, unauthenticated `/mcp` at 401, then the same on the live URL
- A future session-bearing transport would force a move to Reserved VM
- Agent requested Power mode for the fix; the step cost $0.02
## TDR-006: Strict Tier Validation and Per-Connection Drops
**Date:** 2026-10-01 · **Status:** Superseded by TDR-011 · **Related:** [[Project - Gift Graph/Build & Iterate/Feature Backlog|Feature Backlog]]
### Context
Two behaviors surfaced in the live demo. The server refused `vibeText` on a category-tier drop, and `drop` takes a `requesterId`, which makes a drop belong to one connection. The concept page described drops as belonging to the owner and visible to any approved connection by tier.
### Decision, as it stood before v0.2
Keep strict tier validation; it means each drop stores only the text its tier can reveal, and no unused private text sits in the database. The tradeoff is flexibility, since a drop with no vibe text has nothing to show if the owner later lowers the default to vibe. Drop scope is the open call: one connection, or the owner across every approved connection. Agent's version is stricter than the spec, which is a reasonable reading and also a quiet deviation that a forward deployed engineer should confirm with the customer before it ships.
## TDR-005: The Tier Filter Lives in the Query
**Date:** 2026-09-30 · **Status:** Amended by TDR-022
### Context
A pull must return only what the tier allows, and matching must never touch text the requester cannot see.
### Decision
The server checks for an approved connection first. For each drop it returns only the field the tier allows, verbatim, category, or vibe, and excludes silent drops. The match runs against the permitted text only, as literal word matching in v0.1.
### Consequences
- Withheld text never leaves storage, which is a stronger guarantee than filtering after retrieval
- A requester cannot fish for private text by guessing words (verified: a pull for "Söhne" returned nothing)
- Natural prompts miss when no word overlaps (a pull for "gift ideas about fonts" returned nothing). Semantic matching is the next step, and the embeddings must be built only from disclosed text or the leak reopens
## TDR-004: Owner-Written Category and Vibe Text
**Date:** 2026-09-30 · **Status:** Superseded by TDR-021 (Agent's choice, kept through v0.2)
### Context
The spec left open how a verbatim preference becomes its category and vibe forms.
### Decision
The owner writes the category and vibe text explicitly at drop time. No model summarizes a private preference into a "safe" version.
### Consequences
- Closes a leak the spec did not name: a model asked to vague-ify "wants a Söhne license" might still produce something too specific
- Costs the owner a little typing per drop, which the owner review screen can ease later
## TDR-003: Identity from the Bearer Token Only
**Date:** 2026-09-30 · **Status:** Accepted
### Context
v0.1 has two seeded users and no registration. Every request needs an identity the server can trust.
### Decision
Each request carries `Authorization: Bearer <token>`. The server matches the token to a seeded user and takes the caller's identity only from that match, never from tool arguments. Tokens are generated with `openssl rand -hex 32` and stored in Replit Secrets, never in code.
### Consequences
- "One user cannot act as the other" is true by construction
- Both tokens were exposed in a shell screenshot during the build and rotated afterward; rotation changes no code
- Static tokens are the right prototype choice and the wrong production choice; the ladder runs tokens → app sign-in → OAuth for MCP clients → the customer's identity provider ([[Project - Gift Graph/Discovery & Planning/Product Plan|Product Plan]])
## TDR-002: TypeScript on the MCP SDK, Streamable HTTP, Replit PostgreSQL
**Date:** 2026-09-30 · **Status:** Accepted
### Context
The brief asked for drop and pull as MCP tools over Streamable HTTP, the built-in database, and two seeded users.
### Decision
A TypeScript server on Node using the official MCP SDK, one `/mcp` endpoint speaking MCP over Streamable HTTP, Replit's built-in PostgreSQL for users, connections, and drops. The seven tools are `request_connection`, `approve_connection`, `set_default_tier`, `revoke_connection`, `list_connections`, `drop`, and `pull`.
### Consequences
- Any modern MCP client connects without a custom adapter; Claude Code did with one command per identity
- `list_connections` is an addition the spec did not name and proved useful for checking state mid-demo
- 12 integration tests cover identity isolation, silent drops, immediate revocation, and pulls before approval
## TDR-001: Scope v0.1 to the Handshake, Drop, and Pull
**Date:** 2026-09-30 · **Status:** Accepted · **Related:** [[Concept - Gift Graph - Consent-Gated Context Between Agents]]
### Context
The concept page describes a consent registry, a disclosure engine, an anti-inference batcher, PSI tombstones, notifications, and an owner review screen. I had one evening and a call the next day.
### Decision
v0.1 is the consent handshake plus drop and pull with disclosure tiers for two seeded users. The anti-inference engine, PSI tombstones, the owner review screen, and enterprise SSO go into the backlog without being built. The scope lives in `replit.md`, which Agent reads at the start of every task, alongside a rule to propose a plan and wait for approval before building.
### Consequences
- Agent kept the scope exactly and deferred what the backlog lists
- Cutting the scope up front is itself the forward deployed move: deciding what a v0.1 leaves out
- The plan gate held on the first turn and bent once later, when revise text opening with "Approved" was read as approval ([[Project - Gift Graph/Build & Iterate/Bug Tracker#BUG-002|Bug Tracker > BUG-002]])