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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ Expected early triggers here: `security-review`, `database-migration`, `ai-eval`
- **Tenant context is mandatory.** Every data access runs through a module repository inside `withTenant(companyId, …)`; `companyId` comes from the session, never from client input. No raw DB client outside `src/db` and `src/features/tenancy` (ADR-0001 D7).
- **The AI service is stateless.** It never gets DB or storage credentials or tenant logic; the TS worker sends bytes and persists results (D8). pg-boss is the only queue (D4).
- **"Found" needs proof.** A field is `found` only if the grounding verifier confirmed its quote in the cited segment; never relax this to make evals pass (D8).
- **Gemini free tier:** local development with synthetic data only – never in the showcase or with customer data (D8).
- **Gemini free tier:** local development with synthetic data only; on the showcase only as the temporary, invite-only exception of ADR-0001 D11 amendment 2026-09-26 (#67, exceptions register) – never with customer data (D8).
- **Exactly-once export** relies on the idempotency key + unique export row + row lock – keep all three (D9).
- Line endings: `.gitattributes` forces LF. Git Bash on Windows tolerates CRLF (tested 2026-09-22); LF keeps scripts portable to Linux shells (CI, WSL2, containers).
30 changes: 30 additions & 0 deletions docs/decisions/ADR-0001-pilot-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
the drafted recommendation (full TypeScript); the draft recommendation is kept as alternative 1 in D8
- **Amendment 2026-09-24 (D11, also touches D3/D5):** the showcase uses Supabase Postgres + Supabase
Storage instead of Neon + R2 – see "Amendment 2026-09-24" at the end of D11
- **Amendment 2026-09-26 (D11, touches D8):** AI service as a container on Vercel and a Gemini API free-tier
key for the invite-only showcase as a temporary exception – see "Amendment 2026-09-26" at the end of D11
- **Deciders:** Fluory (orchestrator) · drafted by a Claude session
- **Inputs:** `docs/input/2026-09-22-kundenanfrage.md` (customer request), `PROJECT-START.md` (discovery)
- **Facts verified:** 2026-09-22 against official docs, registries and provider terms (sources at the end).
Expand Down Expand Up @@ -652,6 +654,33 @@ enters the code: the app still talks plain PostgreSQL (Drizzle, pg-boss) and the
project as Vertex `eu`, container image already exists, no 5 GB package or 300 s limit for docling/OCR).
- **Runbook:** `docs/technical/deployment-vercel.md`.

### Amendment 2026-09-26 – AI service on Vercel and a Gemini API free-tier key (decided by Fluory, orchestrator; #67)

**Decision.**
1. On the showcase the AI service runs as a **second Vercel project** that runs the existing service
image as a **container on Vercel Functions** (beta; framework `container`, root directory
`services/ai`, `Dockerfile.vercel`, `PORT=8080`, region `fra1`), PDF pipeline `textlines`, OCR off.
Spike result (#67, 2026-09-26): the Python-function route fails – the bundle is **1386 MB** against a
**500 MB** function limit (deployment `dpl_EhZt9SnyAgsH84ayhjXWicgqanpA`); the container image builds
in about 4 minutes (image size limit 15 GB). Cold start and one extraction: recorded in the PR of #67.
2. Model calls use a **Gemini API key on the free tier** (`AI_ALLOW_GEMINI_API_DEV=true`,
`GEMINI_API_KEY` set only in the Vercel project) – a temporary exception to D8.

**Why.** No GCP account for now; one platform for both runtimes; the code path already exists and is
fail-closed (`AI_ALLOW_GEMINI_API_DEV` together with `VERTEX_PROJECT` refuses to start; no fallback).

**Conditions of the exception.** The Gemini API terms (verified 2026-09-22) allow human review and
product-improvement use of free-tier content and state: *"You may use only Paid Services when making
API Clients available to users in the European Economic Area, Switzerland, or the UK."* There is no EU
data-residency guarantee. Therefore: invite-only demo accounts held by the orchestrator, synthetic data
only, demo banner on; the exception expires before anyone else gets an account, at the latest
2026-10-31 → paid tier (same key with billing) or Vertex `eu`. Recorded in the exceptions register.

**Alternatives.** Google Cloud Run + Vertex `eu` (runbook recommendation until now; needs GCP billing) ·
Hugging Face Space (existing Dockerfile unchanged; new account, hosting region not verified).

**Revisit when** the container beta ends or changes its terms, the cold start makes the 60 s AI timeout fail regularly (→ Cloud Run), or the exception expires.

---

## Summary of the challenged decisions
Expand Down Expand Up @@ -713,6 +742,7 @@ explicit requirements.
|---|---|---|
| Showcase: no unattended retries (Hobby cron once/day) | Showcase only; production runs a worker | when a production-like demo is needed |
| No RLS on the `auth`/`pgboss` schemas | Not tenant business data; only server code has access | on review at M3 |
| Gemini API free-tier key on the showcase (#67) | Invite-only for the orchestrator, synthetic data only, demo banner; Google's terms require paid services for API clients offered to EEA users (D11 amendment 2026-09-26) | before anyone else gets a demo account, at the latest 2026-10-31 |
| Gemini free tier for local development | Synthetic data only; never in showcase or production | when the Vertex budget is set up for development |

## Open points
Expand Down
1 change: 1 addition & 0 deletions docs/technical/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Deliberately accepted risks – without an entry here a deviation counts as a de
| Better Auth admin plugin mounted without any holder of its admin role | ADR-0001 D6 names the plugin; decided in #30: kept – its `banned` field implements deactivation (sign-in blocked by the plugin). Nobody holds `platform-admin`, so `/api/auth/admin/*` rejects every caller (tested); user management runs through `identity` | Fluory | 2026-12-31 (review at M3) |
| Upload cap per person only where configured | Local and CI run without `UPLOAD_MAX_PER_HOUR`; the showcase refuses to start without it (#59); concurrent uploads may pass the check together – a cost cap, not an exact quota | Fluory | 2026-12-31 (review at M3) |
| `.msg` uploads checked by OLE signature only | Structure check of Outlook messages needs a CFB parser; files are served only as attachments with `nosniff` and parsed later by the stateless AI service | Fluory | with #23 (MSG parsing) |
| Gemini API free-tier key on the showcase (#67) | Orchestrator decision 2026-09-26 (ADR-0001 D11 amendment): no GCP for now. Google's terms require paid services for API clients offered to users in the EEA and allow human review of free-tier content, so the showcase stays invite-only for the orchestrator, synthetic data only, demo banner on | Fluory | before anyone else gets a demo account, at the latest 2026-10-31 – then paid tier (same key with billing) or Vertex `eu` |
| Gemini API free tier for local development | Synthetic data only; never showcase or customer data (D8) | Fluory | when a Vertex development budget exists |

## Data flow
Expand Down
32 changes: 26 additions & 6 deletions docs/technical/deployment-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

| Blocker | Why |
|---|---|
| AI service host chosen and running | The drain calls it; recommendation: **Google Cloud Run in the EU** (same GCP project as Vertex `eu`, existing image `services/ai`, no 300 s / package limits) |
| AI service running (§3) | The drain calls it. Showcase decision 2026-09-26 (ADR-0001 D11 amendment, #67): a second **Vercel** project with a Gemini API key (temporary exception). Fallback if the Vercel spike fails: Google Cloud Run in the EU with Vertex `eu` |

What runs where: Vercel (Hobby) runs the Next.js app, the drain route and the ERP mock
(`/api/erp-mock`). Supabase (`eu-central-1`) holds Postgres (incl. the pg-boss queue) and the files.
Expand Down Expand Up @@ -42,11 +42,31 @@ process list.
The last output lists `app_owner` and `app_rw` with `f | f | f` (no superuser, no RLS bypass, no role
creation). The script is all-or-nothing; running it twice fails on "role already exists" – that is fine.

## 3. AI service

Deploy `services/ai` (Cloud Run EU recommended) with Vertex `eu` credentials (a service account of the
GCP project, never an API key in the repo) and a random `AI_SERVICE_TOKEN` (≥ 24 chars). The Gemini
free tier is **not** allowed for the showcase (D8). Note its HTTPS URL.
## 3. AI service (second Vercel project, container)

1. Import the same GitHub repository as a **second Vercel project**: framework **`container`**, Root
Directory **`services/ai`**. Vercel builds `services/ai/Dockerfile.vercel` (same build as
`Dockerfile`) and runs it on Vercel Functions (container images, beta). A plain Python function does
not fit: the bundle is 1386 MB against the 500 MB function limit (ADR-0001 D11 amendment 2026-09-26).
2. Deployment Protection: **Vercel Authentication for preview deployments only** – the web app calls the
production URL server-side; the API itself is protected by `AI_SERVICE_TOKEN`.
3. Variables (**Production**):

| Variable | Value |
|---|---|
| `PORT` | `8080` – the image runs as a non-root user, which cannot bind Vercel's default port 80 |
| `AI_SERVICE_TOKEN` | random, ≥ 24 chars – the **same** value as `AI_SERVICE_TOKEN` in the web app |
| `AI_ALLOW_GEMINI_API_DEV` | `true` |
| `GEMINI_API_KEY` | the orchestrator's key – set it through `vercel env add GEMINI_API_KEY production --sensitive` with the value on stdin, never in a chat, the repo or an issue |
| `AI_PDF_PIPELINE` / `AI_PDF_OCR` | `textlines` / `off` – no model downloads on the showcase |

`VERTEX_PROJECT` stays **unset**: together with `AI_ALLOW_GEMINI_API_DEV=true` the service refuses to start.
4. **Exception (ADR-0001 D11 amendment 2026-09-26, exceptions register):** the Gemini API free tier is only
allowed while the showcase is invite-only for the orchestrator with synthetic data. Before anyone else
gets a demo account (at the latest 2026-10-31): enable billing for the key (paid tier) or switch to
Vertex `eu` (`VERTEX_PROJECT`, service account, `AI_ALLOW_GEMINI_API_DEV` removed).
5. `GET <ai-url>/healthz` → 200. The production URL → `AI_SERVICE_URL` of the web app (§4).
Instances scale to zero after 5 minutes without traffic; the first call after that pays a cold start.

## 4. Vercel project

Expand Down
10 changes: 10 additions & 0 deletions services/ai/.vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Not needed at runtime on Vercel – keeps the upload small (dependencies come from pyproject/uv.lock).
# Anchored with a leading slash: `src/requestflow_ai/evals/` is runtime code and must stay.
/tests/
/evals/
/scripts/
/Dockerfile
/.venv/
__pycache__/
.pytest_cache/
.ruff_cache/
31 changes: 31 additions & 0 deletions services/ai/Dockerfile.vercel
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# RequestFlow AI service on Vercel Functions as a container image (beta; ADR-0001 D11 amendment
# 2026-09-26). Same build as ./Dockerfile - keep both in sync. Differences: no optional model
# prefetch (the showcase runs AI_PDF_PIPELINE=textlines, AI_PDF_OCR=off) and the port comes from
# $PORT: Vercel's default is 80, which the non-root user cannot bind, so the project sets PORT=8080.
# The Python-function route failed: bundle 1386 MB > 500 MB limit (deployment dpl_EhZt9SnyAgsH84ayhjXWicgqanpA).
FROM python:3.13-slim

COPY --from=ghcr.io/astral-sh/uv:0.8.17 /uv /usr/local/bin/uv

ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

COPY pyproject.toml uv.lock README.md ./
RUN uv sync --frozen --no-dev --no-install-project
COPY src ./src
RUN uv sync --frozen --no-dev

RUN useradd --system --uid 10001 --no-create-home --shell /usr/sbin/nologin app
USER 10001

ENV PATH=/app/.venv/bin:$PATH \
HF_HUB_OFFLINE=1 \
PORT=8080

EXPOSE 8080
CMD ["sh", "-c", "exec uvicorn requestflow_ai.api.app:create_app --factory --host 0.0.0.0 --port \"${PORT:-8080}\" --no-access-log --no-server-header"]
4 changes: 4 additions & 0 deletions services/ai/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"regions": ["fra1"]
}
Loading