Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ For each GitHub environment, `staging` and `production` (deployment branches: `m
- `VERCEL_TOKEN`: scoped to that Vercel project.
- **Variables:** `SUPABASE_URL`, `CLOUDFLARE_ACCOUNT_ID`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID`.

Vercel project settings (Production environment only, credentials as type Secret): `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, `CLOUDFLARE_ACCOUNT_ID`, `AI_PROVIDER=cloudflare`, `CLOUDFLARE_API_TOKEN`. `README.md` has the owner's steps, click by click.
Vercel project settings (Production environment only, credentials as type Secret): `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, `CLOUDFLARE_ACCOUNT_ID`, `AI_PROVIDER=cloudflare`, `SUPABASE_SECRET_KEY` (the rate limit's counts, since slice 8), `CLOUDFLARE_API_TOKEN`. `README.md` has the owner's steps, click by click.

## Gotchas
- **Migrations must not break the running code.** The old code keeps running between migrate and deploy, after a failed deploy, and after a rollback. Add before you remove: a new column first, dropping the old one in a later change.
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

Next.js · TypeScript · Vercel AI SDK · gpt-oss-120b on Cloudflare Workers AI · Supabase Postgres with pgvector · Playwright · GitHub Actions

**Status:** built and running on a private staging site. The public demo opens at launch, after a per-visitor message limit. [More below](#status).
**Status:** built and running on a private staging site. The public demo opens at launch. [More below](#status).

## The story

Expand All @@ -28,15 +28,15 @@ The check proves a quote is real, not that it's read fairly, which is why every
| **Tool calling with limits** | The model decides when to search and when to save. The app caps it at three searches, then allows only saving, then takes the tools away so it replies. | [`chat.ts`](src/server/chat.ts) |
| **AI output checked in code** | Every quote is checked against the cited person's answer, never the interviewer's question or the assistant's own words. | [`citations.ts`](src/server/citations.ts) |
| **AI behaviour, measured** | Fixed requests, five runs each on the real model, scored on the product's rules. A change is compared with the baseline on a one-sided Fisher's exact test, and any other rule that gets worse is flagged. | [`measurement/`](src/server/measurement/) |
| **Failures in plain sight** | A broken or cut-off answer stays on screen marked unfinished. A used-up daily AI allowance and a busy provider each get their own message. | [`cloudflare.ts`](src/server/ai/cloudflare.ts), [`quota.tsx`](src/features/workspace/quota.tsx) |
| **Failures in plain sight** | A broken or cut-off answer stays on screen marked unfinished. A used-up daily AI allowance, a visitor's used-up 10 messages for the day and a busy provider each get their own message. The limit counts a keyed hash of the visitor's address and the day, never the address itself. | [`cloudflare.ts`](src/server/ai/cloudflare.ts), [`rate-limit.ts`](src/server/rate-limit.ts), [`quota.tsx`](src/features/workspace/quota.tsx) |
| **Safe by default** | Runs locally with no accounts or keys. A guard stops every command that points at a hosted database or holds a stray key. Every table starts closed to the public API. | [`env-guard.ts`](src/lib/env-guard.ts), [`supabase/AGENTS.md`](supabase/AGENTS.md) |
| **Tested in four layers** | Unit, integration, SQL and end-to-end tests. CI runs on every pull request, and every merge deploys to staging. | [`tests/`](tests/), [`.github/workflows/`](.github/workflows/) |

## Status

- **Built:** everything in the story, running on a private staging site (Vercel, Supabase in Frankfurt, Cloudflare Workers AI on its free plan).
- **Built:** everything in the story, running on a private staging site (Vercel, Supabase in Frankfurt, Cloudflare Workers AI on its free plan), with a limit of 10 messages per visitor a day, so one visitor can't use up the free daily AI allowance.
- **Measured:** the AI's behaviour, on fixed requests against a baseline ([how](#measuring-the-ais-behaviour)).
- **Before launch:** a per-visitor message limit, so one visitor can't use up the free daily AI allowance; the public demo; and a short screen recording here.
- **Before launch:** the public demo, and a short screen recording here.

The [product description](docs/product/prd.md), its [scope](docs/product/mvp.md), the [decisions](docs/decisions.md) and the [build plan](docs/plan.md) are all in the repository.

Expand Down Expand Up @@ -181,7 +181,7 @@ The staging and production keys never sit on a development machine; a Workers AI
| Name | Where | What |
| --- | --- | --- |
| `SUPABASE_DB_URL` | GitHub environment secret | The database's session pooler address, with its password |
| `SUPABASE_SECRET_KEY` | GitHub environment secret | `sb_secret_…`, for loading the interviews |
| `SUPABASE_SECRET_KEY` | GitHub environment secret, and Vercel (Secret) | `sb_secret_…`, for loading the interviews and counting each visitor's messages |
| `CLOUDFLARE_API_TOKEN` | GitHub environment secret, and Vercel (Secret) | The Workers AI token for that environment |
| `VERCEL_TOKEN` | GitHub environment secret | A Vercel token for that environment's project only |
| `SUPABASE_URL` | GitHub environment variable, and Vercel (Config) | `https://<ref>.supabase.co` |
Expand Down Expand Up @@ -231,6 +231,7 @@ First for `prd-assistant-staging`, then for `prd-assistant`.
2. Open **Settings → Git** and select **Disconnect**. From now on, only GitHub Actions deploys.
3. Under **Settings → Environment Variables**, add these to the **Production** environment only, leaving Preview and Development empty:
- `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, `CLOUDFLARE_ACCOUNT_ID` and `AI_PROVIDER` = `cloudflare`, as **Config**.
- `SUPABASE_SECRET_KEY`, as **Secret**. Without it the chat refuses every message, since it can't count them against each visitor's daily limit.
- `CLOUDFLARE_API_TOKEN`, as **Secret**. The production project gets this one at launch.
4. For staging only, under **Settings → Deployment Protection**, turn on **Vercel Authentication** and choose **All Deployments**, not Standard Protection, which leaves the main address public. All Deployments is available on every plan (https://vercel.com/docs/security/deployment-protection). Then only you can open staging, and strangers can't use up the free AI allowance.
5. Note the **Project ID** from **Settings → General**, and the **Team ID** from your team's **Settings → General**.
Expand Down
5 changes: 2 additions & 3 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Decisions that shape the app, one line each: `YYYY-MM-DD · decision · conseque
## Access
- 2026-10-17 · Visitors are anonymous: no sign-in, no roles, no admin screen; the owner works only through staging, the quality check and the hosting settings; English only, no currency · No sign-in or translation setup in stage 3
- 2026-10-17 · When the free quota runs out, the chat input is hidden and the message links to the README's screen recording and the repository · Replaces the plain friendly message
- 2026-10-17 · Rate limit: 30 messages per visitor per day, recognised by hashed IP address kept only for the limit window; the provider's daily free allocation is the tighter cap · Needs a legal basis the owner chooses (see Money and legal)
- 2026-10-26 · Rate limit: 10 messages per visitor per UTC day, recognised by a keyed hash of the IP address (an IPv6 address by its /64) and the day, never the address itself; a row is deleted by the first message after its day; it counts only messages to the real model (the owner, slice 8) · At about 350 neurons an answer, 30 a day would let one visitor use the whole day's allocation; 10 caps one visitor at about a third. Replaces 30 a day (2026-10-17). How it's built: `src/server/AGENTS.md`, *The rate limit*

## External services
- 2026-10-17 · Every AI key comes from an account or project with no billing attached, so running out of quota fails instead of charging · No spending limit to manage; quota exhaustion is the expected failure. The measurement stays on the Free plan too (External services, 2026-10-19)
Expand All @@ -69,6 +69,7 @@ Decisions that shape the app, one line each: `YYYY-MM-DD · decision · conseque

## Money and legal
- 2026-10-17 · Official sources for the IP-based rate limit (not legal advice; the owner decides the legal basis): GDPR (https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679) recital 30 (IP addresses are online identifiers), recital 26 (pseudonymised, e.g. hashed, data is still personal data), recital 49 (processing strictly necessary for network and information security can be a legitimate interest), Art. 5(1)(c) and (e) (data minimisation, storage limitation), Art. 6(1)(f) (legitimate interests), Art. 13 (information to give visitors); CJEU C-582/14 Breyer (https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:62014CJ0582): a dynamic IP address can be personal data for a website operator · Decide the legal basis and whether a short privacy notice is needed before launch
- 2026-10-26 · The rate limit's legal basis: the owner treats it as strictly necessary to keep the free demo available to everyone, like a necessary cookie, so no consent is asked and no banner shown; it sets no cookie. A deletion request needs no process of its own: a count is a keyed hash that can't be traced to a visitor and is gone after its day (the owner: "lets not care too much about the legal basis"; not legal advice) · Settles the item deferred on 2026-10-17. Whether a short privacy notice mentions it stays with the legal pages in stage 5
- 2026-10-17 · Cloudflare's terms (Self-Serve Agreement and Developer Platform terms, read 2026-10-17; not legal advice): no clause limiting the free plan to private, test or commercial use, and no restriction for EEA visitors; each model's own licence and use policy apply · The public demo can run on the free plan. Replaces the reading of Mistral's terms
- 2026-10-17 · A line under the chat box says it's a demo with fictional notes and not to enter personal information · Covers free-tier data use by the provider
- 2026-10-17 · No cost display and no prices file; token usage is logged on the server per answer and not shown in the app, and the quality check prints the total tokens · Replaces brief feature 5 (cost per answer and per PRD in the app)
Expand All @@ -81,8 +82,6 @@ Decisions that shape the app, one line each: `YYYY-MM-DD · decision · conseque

## Deferred
<!-- YYYY-MM-DD · what · decide by <when> · affects <what> -->
- 2026-10-17 · The legal basis for the hashed-IP rate limit, and handling requests to delete personal data (keeping it only for the limit window is decided) · decide before the rate-limit piece of work in stage 4, the first that stores personal data · affects the rate limit and a possible privacy notice
- 2026-10-17 · Legal pages, including whether a short privacy notice is needed; the domain, search engines, alerts, cookies, tracking, tax and receipts are already settled as none · decide in stage 5 · affects the launch
- 2026-10-18 · The per-visitor rate limit's size (now 30 messages a day, under Access), from what an answer costs on the chat model as the staging log shows: later sections cost more, because every call resends the conversation, and at 30 messages one visitor could use the whole day's allocation, which staging and production share · decide before the rate-limit piece of work (slice 8), from the token counts the staging log shows by then · affects the rate limit
- 2026-10-17 · A short README section, "If this became a product", written from the PRD's later list: each item says what it adds and which part of the existing code it builds on; text only, about an hour, no new features · do in stage 5 · affects the README
- 2026-10-19 · What "the grocery delivery request has to pass before launch" means once it's measured over several runs, and what happens after a miss · decide before launch · affects the launch
Loading
Loading