# Gift Graph Technical Decisions
> Gift Graph - Architecture Decision Records (TDRs): the "why" behind the v0.1 build on Replit
> Last Updated: 2026-10-01 · 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`.
## 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:** [[Gift Graph Bug Tracker#BUG-009]], [[Gift Graph 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:** [[Gift Graph 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:** Accepted
### 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:** [[Gift Graph 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:** Open (Agent's reading, under review) · **Related:** [[Gift Graph 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 pending
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:** Accepted
### 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:** Accepted (Agent's choice, kept)
### 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 ([[Gift Graph 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 ([[Gift Graph Bug Tracker#BUG-002]])