Skip to content

Architecture Assessment — Compute Posture (on-prem vs Vercel) and the 127.20 ingress ruling

Re-filed S504 (ruling R9), from reference/: a ratified decision-support paper (output landed as DR-042..DR-046) — a point-in-time report, not durable facts.

Architecture Assessment — Compute Posture and the 127.20 Ingress Ruling

Section titled “Architecture Assessment — Compute Posture and the 127.20 Ingress Ruling”

Status: DECISION-SUPPORT, authored S456 (2026-07-09) for owner ratification. Gates the {127.20} private-ingress ruling and the id-71 Vercel-spec rework. Builds on — does not re-derive — the salvaged ingress assessment (canonical:.user-scratch/s454-recovery/id-127.20-private-ingress-assessment.md, cited IA §N), the Vercel async-worker research (specs/id-71-ai-tooling/research-inputs/vercel-async-worker-architecture.md, cited VR §N), reference/deployment-architecture.md (DA §N), reference/decision-register.md (DR-NNN), and the owner’s verbatim feedback (canonical:.user-scratch/id-128-127-vercel-feedback.md, cited OF). Vercel/Cloudflare product facts were re-checked live on 2026-07-09 against official docs (URLs inline); the VR research doc’s facts (07/07/2026) held, with one material update flagged in §F.


Executive summary — the decisions Liam must make

Section titled “Executive summary — the decisions Liam must make”

This paper reframes a cluster of questions the owner raised (OF) into six ratifiable decisions. Each has a recommendation; the reasoning is in the lettered section.

  1. Compute posture (§A) — keep the split; do not move the app on-prem by default. The pipeline correctly lives on-prem (data-adjacent, compute-heavy, single-tenant). The Next.js app correctly stays on Vercel: it is a stateless, public-by-design layer that holds no client data at rest (the tenant boundary is the Supabase project, not the app server — PD “one project per client”), so moving it to the VPS buys no data-isolation at high ops cost. Recommendation: ratify the split. App-on-VPS becomes a per-client contractual option, not the default.

  2. Define “zero-egress” precisely (§B) — it never meant literally zero. Client document content already leaves the box today: the pipeline calls Anthropic for extraction/embedding (DA §5.5), and client data sits at rest in Supabase cloud (single-tenant project per client). “Zero-egress” operationally means no client data at rest in shared multi-tenant third-party infra, and no non-essential egress — terms-protected model/transit egress (Anthropic no-train/ZDR; Supabase single-tenant) is the accepted, bounded exception. Recommendation: ratify this definition as the honest reading of the DR-017 promise; it is the load-bearing premise for decisions 3–6.

  3. Per-client tooling-posture choice (§A/§D/§E) — offer cloud OR local, per client. Local equivalents for the Vercel stack are mostly close (§D): Queues → already have it (Supabase pgmq), Gateway → LiteLLM, Connect → Nango, agent runtime → goose (already the pilot). The one real gap is durable workflow execution (Temporal/Inngest — real ops on a 4 GB box). Recommendation: ratify a per-client choice — cloud AI-tooling in the client’s own Vercel project (ZDR) for cloud-native clients, or the local stack on the client VPS for strict-on-prem clients — kept behind our own thin interface so it is swappable. This choice forces the “thin-and-swappable, not all-in” answer to VR §4 Q1 (lock-in).

  4. Agent topology (§E) — data-plane per-client, control-plane central. Every client runs all agent types inside its own isolation boundary (client VPS or client’s own Vercel project) with a per-client service-actor, read-only DB replica, and per-client OKF repo access (OF decision-surface pt 5). The Platform control/observability plane is central and trades in telemetry only (run status, tokens, timings, health) — never client corpus content. This is the “central view” the owner was missing (OF), and it keeps DR-017 intact. Recommendation: ratify; generalise DR-017 from goose to all data-plane agents.

  5. Cloudflare “on-prem” framing (§C) — it stays on-prem; the nuance is transit, not residence. Putting Cloudflare in front does not move compute or storage off the box, so the on-prem property (single-tenant, client-owned compute + storage) remains true. The only new consideration is ingress transit: a TLS-terminating proxy (Cloudflare Tunnel) sees cleartext of whatever crosses the ingress. For the primary ingest flow this is only control metadata (/walk carries a pointer + bearer; files are pull-synced from the client’s own bucket, DA §5). Content bytes cross only on the /extract cleaner path. Recommendation: accept — on-prem is about where data lives and is processed, which is unchanged; manage the /extract transit exposure explicitly (§F).

  6. The {127.20} ingress ruling (§F) — Cloudflare Tunnel default, origin-terminated escape hatch. Recommendation: ratify Cloudflare Tunnel + Access service token as the default private ingress (only option that makes the origin genuinely unreachable — no open port — with a free Zero Trust tier and DNS-only rollback), routed through the existing Traefik to preserve the /walk|/health|/extract path-scope, bearer retained as a second factor. New since the salvaged assessment: Vercel Static IPs are now self-serve on Pro ($100/mo/project) — this reopens IP-allowlist at Traefik as a fourth option and is the preferred origin-terminated alternative (strictly better than mTLS: no cert-expiry outage class) for any client that contractually requires no third party seeing content cleartext on the ingress. The /health external monitor is retired (owner already agreed, OF). The prerequisite is moving the pipeline subdomain to Cloudflare nameservers.

One-line synthesis: the platform is already a hybrid — on-prem where data lives and is processed (pipeline, per-client Supabase), cloud where it is served and where terms-protected inference happens (Vercel app, Anthropic). The right posture is not “move everything on-prem” but “hold the at-rest bright line, make every cloud dependency per-client and contractually no-retention, and make cloud-vs-local a per-client choice behind our own interface.


A. Compute posture — on-prem (IONOS/Coolify) vs Vercel

Section titled “A. Compute posture — on-prem (IONOS/Coolify) vs Vercel”

The owner’s question (OF): “When we pivoted to on-prem, the cocoindex pipeline was hosted there, but should we also not have moved the app-side there too, rather than remaining in Vercel?”

PropertyPipeline (cocoindex)Web app (Next.js)
StateStateful — LMDB memoisation store, pull-synced corpus working copy on disk (DA §3 L-raw)Stateless — renders/routes; holds nothing at rest
Data at restWorking copy of client files on the boxNone — reads/writes the client Supabase project over the network
ComputeHeavy Python (Docling, extraction, embeddings), long walks (VR §2)Light request/response; serverless
ReachabilityPrivate by intent (only the Vercel→VPS hop forces public reach — DR-015)Public by design — it is the login-gated website
Tenant boundaryThe client VPS + the client Supabase projectThe Supabase project (PD “the DB is the tenant”) — not the app server

The pipeline moved on-prem for a real reason: it is the data-adjacent, single-tenant, compute-heavy worker, and DR-017/DR-023/DR-025 put the client’s evidence and processing on the client’s own box. None of those reasons apply to the app. The app stores no client data at rest, is public by design, and Vercel is the first-party platform for Next.js (edge network, preview deploys, zero-ops TLS/CDN). Moving it to Coolify means self-operating the Next.js runtime, scaling, TLS, and preview environments — high ops cost for zero data-isolation gain, because the thing you would be “bringing on-prem” holds none of the client’s data.

Where “app on Vercel” does create surface — and why it is already handled

Section titled “Where “app on Vercel” does create surface — and why it is already handled”

The app is the component that reaches into the private pipeline (the Vercel→VPS hop) and into Supabase. That is precisely the {127.20} ingress question (§F) and Supabase RLS — both already tracked. Vercel being in front of those hops is an ingress-hardening problem, not a data-residence problem.

The owner noted Supabase stays cloud “for now” and that Coolify offers a one-click self-hosted Supabase. Self-hosting Supabase per client is possible but is a major ops step: you become the DBA for backups, Postgres upgrades, and the whole extension stack (pgvector, pgmq, pg_cron) — per client. Supabase cloud today is a single-tenant project per client (PD), already an accepted at-rest processor (§B). Recommendation: keep Supabase cloud as default; self-hosted-Supabase-on-Coolify is the same per-client contractual option as app-on-VPS — offered to a client who forbids a cloud DB, not the standard build.

Ratify the split as correct, not accidental: pipeline on-prem, app on Vercel (as a per-client Vercel project — already the intention, OF decision-surface pt 2). Offer app-on-VPS and self-hosted-Supabase as per-client contractual options for strict-on-prem clients, not defaults. This is strictly a superset of today and costs nothing until a client asks.

Where on-prem is strictly worse (stated plainly): self-hosting the app or DB trades Vercel/Supabase’s managed TLS, CDN, preview deploys, backups, and Postgres lifecycle for your own pager. For a client with no contractual on-prem requirement it is pure ops cost with no security benefit — the data is not more contained (the app holds none; the DB is already single-tenant).


B. DR-017 per-client zero-egress boundaries — what egress each option creates

Section titled “B. DR-017 per-client zero-egress boundaries — what egress each option creates”

The owner’s incisive question (OF): “If we use Anthropic or any other non-local AI model, are we not already in theory allowing client data to leave their environment already, or does ZDR + Secure compute from Vercel and Anthropic cover this?”

Yes — and this is the crux of the whole assessment. Client document content already leaves the client’s box today, in the current on-prem design: the extraction/embedding step calls Anthropic (DA §5.5: “AI extraction (Anthropic) → embeddings”). And client data sits at rest in Supabase cloud. So “zero-egress” was never literally zero. It has always meant, operationally:

No client data at rest in shared multi-tenant third-party infrastructure, and no non-essential egress. Terms-protected transit for essential functions — model inference under no-train/ZDR terms; a single-tenant managed DB — is the accepted, bounded exception. The bright line is at-rest-in-multi-tenant, not “a byte never crosses the boundary.”

Anthropic and single-tenant Supabase are today’s two contracted data processors. The right question for Vercel is therefore not “does any client data leave the box” (it already does, essentially) but “does adopting Vercel add a new third party, and at rest or only in transit?

OptionNew third party in path?Client content at rest in it?Client content in transit through it?Preserves the commitment?
Status quo (pipeline on-prem, Anthropic direct)Anthropic (existing)No (VPS + client Supabase only)Yes — prompts to Anthropic (no-train terms)Baseline — this is the zero-egress posture as practised
Supabase cloud (current)Supabase (existing)Yes — but single-tenant project per clientn/aPreserved — accepted at-rest processor, the DB is the tenant boundary
Local tooling stack (LiteLLM/Nango/pgmq/goose on client VPS)NoneNoOnly to Anthropic (as today)Fully preserved
Cloudflare Tunnel (ingress)CloudflareNo (tunnel is transit-only)On /extract only (TLS-terminated at CF edge — sees cleartext); /walk is metadataPreserved with a DPA + content-light ingress (§C, §F)
Vercel AI GatewayVercelNo with ZDRYes — prompts transit Vercel then AnthropicPreserved with ZDR ($0.10/1k, pricing); marginal add over calling Anthropic direct
Vercel Workflows / QueuesVercelYes, unless orchestrate-only — steps persist inputs/outputs in Vercel-managed storage (VR §1.2, §4 Q2)YesOnly if disciplined to orchestrate-only (pointers/IDs, never records — VR §4 Q5)
Vercel SandboxVercelEphemeralYes, if code touches contentOnly for code-execution agents (not a current need — VR §1.4)

What “ZDR + Secure Compute” does and does not cover (answering the owner directly)

Section titled “What “ZDR + Secure Compute” does and does not cover (answering the owner directly)”
  • Anthropic ZDR / no-train: covers provider retention/training of prompts. It does not make the data “not leave” — it makes the leaving contractually bounded. This is the mitigation the current design already relies on.
  • Vercel AI Gateway ZDR: routes only to providers with ZDR agreements and adds Vercel’s own no-retention; team-wide ZDR now costs $0.10/1k requests (moved behind a fee Apr 2026 — changelog). Caveat: BYOK keys are skipped by default under ZDR unless individually marked ZDR-compliant (ZDR docs) — relevant because the platform BYO-keys Anthropic today.
  • Vercel Secure Compute: is about reaching a private backend, not model data — and it is AWS-shaped (VPC peering to your AWS VPC) and Enterprise-only (docs). Canonical’s boxes are IONOS, not AWS, so Secure Compute’s private-networking does not apply (its static-IP allowlisting does — see §F). It is not a client-data-egress control.

Ratify the operational definition above. It makes the honest case: the platform already accepts terms-bounded transit (Anthropic); the defensible line to hold is no client data at rest in shared multi-tenant infra. Under that line: the local stack fully preserves the commitment; AI Gateway + ZDR and Cloudflare Tunnel + DPA preserve it as accepted-exception transit; Workflows/Queues preserve it only under orchestrate-only discipline (the concrete guard-rail — VR §4 Q5 — that makes a “yes” on Workflows compatible with a “no” on egress).


C. Does Cloudflare in front of the pipeline still count as “on-prem”?

Section titled “C. Does Cloudflare in front of the pipeline still count as “on-prem”?”

The owner’s framing (OF): “based on our current Ionos/Coolify setup, will introducing Cloudflare (or possibly Control D), actually mean ‘on prem’ is true, or would the data that cocoindex ingests or processes need to be on the same box for this statement to actually be true?”

Answer: on-prem is about residence + processing, which Cloudflare does not move

Section titled “Answer: on-prem is about residence + processing, which Cloudflare does not move”

“On-prem” in this codebase has never meant “on the client’s physical premises” — the VPS is a rented IONOS box. It means single-tenant, client-owned/client-controlled compute and storage, with data processed on a box dedicated to that client (DA §1, §9). By that definition:

  • The data cocoindex ingests and processes is already on the box — the pull-synced corpus working copy on the VPS, writing the client’s own Supabase project (DA §3, §5). Cloudflare in front does not move any of that off the box. So the owner’s implied test — “would the data need to be on the same box for on-prem to be true” — is already satisfied, with or without Cloudflare.
  • Cloudflare mediates only the ingress hop. It changes how a request reaches the origin, not where the data lives or is processed. On-prem stays true.

The real nuance: TLS termination model (what transits, in cleartext, and to whom)

Section titled “The real nuance: TLS termination model (what transits, in cleartext, and to whom)”

This is the technically precise version of the owner’s worry:

Ingress modelTLS terminates atThird party sees content cleartext?What actually transits
Cloudflare Tunnel (TLS-terminating proxy)Cloudflare edgeYes — CF decrypts, re-encrypts to origin/walk: bearer + pointer (metadata). /extract: document bytes (content)
mTLS at Traefik (origin-terminated)The origin (Traefik)NoSame paths, but end-to-end encrypted to the box
Static-IP allowlist (origin-terminated)The origin (Traefik)No (but the allowlisted source IP is a shared Vercel pool — §F)Same, end-to-end to the box

So: Cloudflare Tunnel keeps the data on-prem but inserts Cloudflare as a transit processor that can see cleartext of whatever crosses the ingress. For the primary ingest flow this is only control metadata/walk carries a bearer + a pointer, and the files themselves are pull-synced from the client’s own Supabase bucket to the VPS (DA §5, DR-023/ DR-025), never through Cloudflare. Content bytes cross the ingress only on the /extract cleaner path (lib/extraction/clean-via-worker.ts, IA §1). That is a bounded, addressable exposure (§F), not a wholesale “your data now goes through Cloudflare.”

Control D (the owner’s alternative): Control D is a DNS resolver / filtering service — it governs name resolution policy, not ingress. It does not give the pipeline origin privacy the way a Tunnel does and is not a substitute for the {127.20} ingress decision. It solves a different problem (egress DNS policy / content filtering) and can be considered separately if that need arises.

Accept that Cloudflare-in-front keeps the pipeline on-prem (residence + processing unchanged). Treat the only real question as ingress transit exposure under TLS termination, and manage it on the content-bearing path (/extract) per §F — either keep that path content-light (route extraction through pull-from-bucket like /walk) or cover it under Cloudflare’s DPA. For a client who will not accept any third party seeing content cleartext on the ingress, choose an origin-terminated option (Static-IP allowlist or mTLS, §F).


D. Local-equivalent feasibility for the Vercel agent stack

Section titled “D. Local-equivalent feasibility for the Vercel agent stack”

The owner’s question (OF): “If we didn’t use Vercel’s functionality, how far are we currently from what we would need … (Gateway, Connect, Sandbox, Queues, workflows & agent framework) so that we had an entirely local equivalent?”

Assessed per primitive, on the existing Coolify boxes, with an honest ops cost. (Vercel-side facts: VR §1.)

Vercel primitiveWhat it doesSelf-hosted equivalent on CoolifyGap / ops cost
Queues (VR §1.3)Durable pub/subSupabase pgmq (in the client’s own DB) — already available; or Redis+BullMQ on the VPS~Zero gap. pgmq lives inside the client Supabase project — the most isolation-aligned option (DR-023/DR-025). Low ops.
AI Gateway (VR §1.5)Model router, spend obs, fallbackLiteLLM (OSS proxy: routing, BYOK, spend tracking, fallbacks)Small gap. One container + config per box. Or skip entirely and call Anthropic direct (status quo).
Connect (VR §1.6)Outbound SaaS OAuth/API-key brokeringNango (OSS unified OAuth for SaaS) + a secrets manager (Infisical/Vault)Small–moderate gap. Only needed when SaaS connectors land (WS-1/WS-6 — deferred, PD).
Agent runtime (eve, VR §1.1)Headless agent frameworkgoose — already the id-71 pilot, already the per-client target (DR-017)Have it for the runtime. The delta vs eve is the durable-session/hooks layer (= the Workflows gap below).
Sandbox (VR §1.4)Ephemeral untrusted-code microVMsgVisor/Firecracker microVMs, or resource-capped ephemeral containersFar, but likely unneeded. Safe isolation of AI-generated code is genuinely hard; no current requirement (VR §1.4). Defer.
Workflows (VR §1.2)Durable execution: replay, sleep-months, hooks, auto-retryTemporal (self-hostable, the gold standard) / Inngest (lighter) / roll-your-own on pgmq + a state tableThe real gap. Durable execution with month-long sleep + human-gate hooks is sophisticated; Temporal is real ops (its own cluster + DB) on a 4 GB Server B (VR §3). This is where Vercel’s managed offering is materially ahead.

“Not that far” is true for four of six: Queues (have it), Gateway (LiteLLM), Connect (Nango), agent runtime (goose). Sandbox is far but unneeded near-term. The one genuine gap is durable workflow execution — and it is the highest-ops piece to self-host. So an entirely local AI-tooling stack per client is feasible, at the cost of standing up and operating LiteLLM + Nango + pgmq (+ optionally Temporal/Inngest) on each client box — meaningful but bounded, with durable-execution the long pole and the hardware (4 GB) the constraint to watch.

On the owner’s “raindrop.ai vs Vercel” question (OF, Vercel-spec feedback): category error — Raindrop is an eval-authoring tool (id-104), localhost-only, permanently excluded from the client-data path (zero egress enforced in lib/eval/graduation.ts; DA §8). Vercel Workflows/eve is a production worker/agent runtime. Neither replaces the other; Raindrop never touches client data by design, so it is not in this trade at all.

Because the local stack is feasible for the common primitives and the durable-execution gap is the only expensive one, make cloud-vs-local a per-client choice (decision 3) rather than a platform-wide fork — and keep the agent/tooling layer behind our own thin interface so the durable-execution + model-routing + credential-brokering surface can be backed by either Vercel primitives or the local stack, selected per client. This directly answers VR §4 Q1 (lock-in): thin-and-swappable, not all-in on eve — the per-client-choice requirement forces that answer.


E. Per-client vs central topology for agent types

Section titled “E. Per-client vs central topology for agent types”

The owner’s confusion, stated honestly (OF): “the spec seems to point to not all agents being ‘per client’, which may be my confusion — I would have thought that each client could use all available agent types within their instance, but perhaps I hadn’t yet thought about what this looks like centrally for us from a Platform-perspective.”

The resolution: separate the data plane from the control plane

Section titled “The resolution: separate the data plane from the control plane”

The VR doc treated “a Vercel-hosted eve fleet” as multi-tenant Vercel infra, the inverse of DR-017 (VR §2, §4 Q2) — its “sharpest strategic fork.” The owner’s steer dissolves most of that fork: per-client Vercel projects are already the intention (canonical-platform + the first client’s own project already exist — OF decision-surface pt 2). A per-client eve/Workflows fleet in the client’s own Vercel project is per-client-isolated, not multi-tenant. So the real fork is only “client VPS vs client’s own Vercel project” for a data-plane agent — both are per-client, both honour DR-017’s boundary.

PlaneWhat runs hereTopologySees client content?
Data plane — every agent that reads/writes the client’s corpus, records, or OKF bundle (the id-71 strawman HA agents: trust sweeps, briefings, watch-agents, expiry sweeps, onboarding — VR §2; goose; anything that “does work on the client’s knowledge”)Per-client, inside the client’s boundary (client VPS or client’s own Vercel project), with per-client service-actor + read-only DB replica + per-client OKF repo access (OF pt 5)Yes — hence per-client
Control plane — Platform observability, fleet health, deploy/manage, aggregate spend, agent-run telemetryCentral to PlatformNo — telemetry only (run status, tokens, timings, error rates). Never corpus content

Every client gets all agent types — but each client’s instances of those agents run in that client’s boundary. The central Platform plane is what the owner was missing: it gives one dashboard over all clients’ fleets without any client content crossing into Platform, because it trades in metadata, not data. This is consistent with DR-017, whose promise is explicitly about the data and its processing (“the agent processing a client’s knowledge — and its API spend — lives on that client’s box”) — telemetry and management are not that data.

Ratify data-plane-per-client / control-plane-central, and generalise DR-017 from goose to all data-plane agents: the per-client isolation boundary is satisfied by either the client VPS or the client’s own Vercel project. This makes the id-71 headless-agent surface buildable without re-litigating isolation for each agent type, and gives Platform a clean central observability story.


F. The {127.20} Cloudflare-vs-mTLS ingress ruling

Section titled “F. The {127.20} Cloudflare-vs-mTLS ingress ruling”

The salvaged assessment (IA) compared three options and recommended Option A — Cloudflare Tunnel + Access service token over mTLS at Traefik, with Tailscale reserved for operator SSH only. That reasoning stands for the “make the origin genuinely unreachable” goal (IA §2, §5). Two things now refine it: (1) the §C termination-model analysis adds an axis, and (2) a current-facts update reopens a fourth option.

Material update — Vercel Static IPs are now self-serve on Pro

Section titled “Material update — Vercel Static IPs are now self-serve on Pro”

The IA/S436 premise was “Vercel egress IPs aren’t stable without Enterprise Secure Compute,” which ruled IP-allowlist at Traefik impractical (IA §2). That premise has softened. As of the current docs (last_updated 2026-06-30):

Static IPs (shared pool) are available on Pro and Enterprise, $100/mo per project, giving fixed outbound egress IPs explicitly for “on-premises databases and services behind firewalls” — without Secure Compute. (Static IPs docs)

So a fourth ingress option now exists that the salvaged assessment dismissed: Vercel Static IPs (Pro) + Traefik/firewall IP-allowlist. Its caveats matter, though:

  • The pool is shared among a small group of Vercel customers — allowlisting it means allowlisting “any customer in that shared pool,” not only us (weaker source-identity than a Cloudflare service token or a dedicated Secure-Compute IP).
  • It leaves 443 publicly listening — the origin stays directly reachable, just source-IP-filtered. It does not retire the “directly public” property (the actual {127.20} goal). Same class as mTLS on that axis.
  • $100/mo per project × per client — multiplies per tenant, vs Cloudflare Zero Trust’s free tier.
  • Edge runtime is unsupported (fine — the pipeline calls already need Node-side handling).
AxisCloudflare TunnelStatic-IP allowlist (Pro, new)mTLS at TraefikTailscale/WireGuard
Origin “not directly public”Best — no open port (cloudflared outbound-only on 7844, docs)Port open, IP-filteredPort open, cert-gatedBest, but wrong fit for serverless (IA §4)
No 3rd party sees content cleartext (§C)Weakest — TLS terminates at CF edgeBest — origin-terminatedBest — origin-terminatedBest
Ops / failure modesBest — managed, free tier, DNS-only rollbackModerate — $100/mo/project, shared poolWorst — cert-expiry outage class, fights Coolify-managed Traefik (IA §3)High — persistent daemon, mismatched to Vercel serverless
Per-client cost at NBest — free ZT tierWorst — $100/mo × N projectsModerate — internal CA + per-client certn/a for this hop
Fit to Vercel serverlessNative — CF-Access headers, no static IP neededNativeNode-runtime + custom agent only (IA §3)Poor (IA §4)

The one honest tension, and how to resolve it

Section titled “The one honest tension, and how to resolve it”

Cloudflare Tunnel is best on reachability, ops, and per-client cost, but worst on the §C “no third party sees content cleartext” axis because it TLS-terminates at the edge. That demerit only bites the /extract content path — the primary /walk ingest flow crosses the ingress as metadata only (files pull-synced from the client’s own bucket, §C). So the resolution is:

  • Keep the content path content-light. Prefer routing extraction through pull-from-bucket (as /walk already does) rather than POSTing bytes over the ingress; where /extract bytes must cross, cover Cloudflare under a DPA (the tunnel is transit-only — no client data at rest in Cloudflare, §B).
  • With that in place, Cloudflare Tunnel’s edge-termination is a bounded exposure, and its advantages (genuinely unreachable origin, free, DNS rollback) dominate.
  1. Ratify Cloudflare Tunnel + Access service token as the DEFAULT private ingress, routed through the existing coolify-proxy Traefik to preserve the Inv-13 /walk|/health|/extract path-scope and the /stage 404 (IA §2), bearer retained as a second factor. Prerequisite: move the pipeline subdomain to Cloudflare nameservers.
  2. Adopt Static-IP allowlist (Pro) as the preferred origin-terminated alternative for any client that contractually requires no third party seeing content cleartext on the ingress. It is strictly better than mTLS (no cert-expiry outage class, no Coolify-Traefik tls.options friction) at the cost of $100/mo/project and shared-pool source identity. Demote mTLS to last-resort. (This is the same per-client-choice pattern as decisions 3 and A: a default plus a contractual escape hatch.)
  3. Retire the external /health uptime monitor — owner already agreed (OF); liveness is covered by the ingress-independent Docker loopback healthcheck + the Supabase datapath-watch signal (IA §1, §5). No Access Bypass needed.
  4. Tailscale/WireGuard for operator SSH/admin only, separately, later (IA §4) — not this gate.

The interim public+bearer posture (DR-015) holds until the first of: a second client goes live, or a client contractually requires private ingress. The move is exactly IA §2’s ordered migration — deploy cloudflared sidecar → Access app (service token) → add the two CF-Access-* headers to the three pipeline fetches (pipeline.ts, folder-drop.ts, clean-via-worker.ts, IA §1) → parallel-run → cut DNS to the Tunnel CNAME → close inbound 443/80. Bearer retained throughout; rollback is DNS-only in minutes. One new coupling: because per-client Vercel projects exist, the service token is minted + stored per client Vercel project — joins the S440 per-client walk-orchestration gate (IA §6; journal 2026-07-03).


Consolidated implications — what changes if each recommendation is ratified

Section titled “Consolidated implications — what changes if each recommendation is ratified”
#Decisiondeployment-architecture.mdDecision registerid-71 / Vercel spec
1Keep compute split; app-on-VPS + self-hosted-Supabase as per-client optionsAdd a “compute posture” note to §1/§9: app = per-client Vercel project (stateless, no at-rest); VPS/self-host Supabase = per-client contractual optionNew DR: “Compute posture — app on Vercel (per-client project, stateless); pipeline on-prem; app-on-VPS + self-hosted-Supabase are per-client contractual options”Vercel spec: drop the “multi-tenant infrastructure” framing (VR §2) — per-client projects are the model
2Zero-egress definition§4 ingress-posture note + a new “egress model” subsection: at-rest bright line; Anthropic + single-tenant Supabase are accepted contracted processorsNew DR: “Zero-egress = no client data at rest in shared multi-tenant infra + terms-protected transit; Anthropic/Supabase-single-tenant are accepted processors; bright line is at-rest-in-multi-tenant”Resolves VR §4 Q2 (egress vs DR-017) as a definition, not a fork
3Per-client cloud-OR-local tooling behind a thin interface§7 (goose) generalised to the tooling layer; note the local stack (LiteLLM/Nango/pgmq/goose[+Temporal]) as the on-prem optionNew DR: “Per-client AI-tooling posture is a CHOICE (cloud in client’s own Vercel project + ZDR, or local stack on client VPS), behind our own thin swappable interface” — settles VR §4 Q1 lock-in as thin-and-swappableVercel spec: adopt Workflows/Gateway behind our interface, not eve-all-in; Queues → pgmq baseline
4Data-plane per-client / control-plane central§7 + §9: generalise DR-017 to all data-plane agents; add the central control/observability plane (telemetry-only)New DR (or DR-017 amendment): “per-client isolation = client VPS OR client’s own Vercel project; ALL data-plane agents per-client; Platform control plane central, telemetry-only, never client content”Vercel spec: the id-71 HA agent inventory is per-client data-plane; add a Platform control-plane surface
5Cloudflare stays on-prem; manage transit§4: add the TLS-termination table (who sees cleartext); note /extract as the only content-bearing ingress path(Covered by the §F ingress DR)
6Cloudflare Tunnel default + origin-terminated escape hatch; /health retired§4 ingress-posture: replace “Cloudflare Tunnel or mTLS” with the ratified default + Static-IP escape hatch; note /health monitor retiredNew DR (the {127.20} ruling): “Cloudflare Tunnel + Access service token default; Static-IP-allowlist (Pro) preferred origin-terminated alternative; mTLS last-resort; /health external monitor retired” — supersedes the open clause of DR-015Unblocks {127.20}; couples service-token issuance to the S440 per-client gate

Open questions I could not resolve from the inputs

Section titled “Open questions I could not resolve from the inputs”
  1. /extract re-homing. Whether the /extract cleaner path can be re-routed through pull-from-bucket (like /walk) — which would make the ingress entirely content-light and neutralise Cloudflare’s TLS-termination demerit (§C, §F) — is an implementation question the inputs don’t answer. Needs a code-truth pass on lib/extraction/clean-via-worker.ts and the app-side upload/folder-drop rework ({131.24}).

  2. Which first client is which posture. Decisions 3, A, and F all hinge on a per-client cloud-vs-local / default-vs-escape-hatch choice. The owner notes the first client already uses Hubspot/Asana/Notion (OF) — implying cloud-native — but the actual contractual on-prem requirement per client is a commercial fact not in these inputs. Needs the owner to state, per signed/pipeline client, whether they require strict on-prem.

  3. Durable-execution engine choice (if local). If a strict-on-prem client needs the durable workflow layer locally, Temporal vs Inngest vs roll-your-own-on-pgmq is unresolved — it turns on the 4 GB Server B constraint and expected workflow volume (VR §3). Needs a spike, not a ruling.

  4. AI Gateway adoption at all. Whether to insert the Vercel AI Gateway (per-tenant spend observability + routing) vs continue calling Anthropic directly (status quo, one fewer third party) is a genuine trade the inputs frame but don’t decide (VR §1.5, §4 Q3). The BYOK- under-ZDR caveat (§B) makes it slightly more friction than it first appears.

  5. Cloudflare DPA / nameserver move. Whether the org will (a) move the aisolutionhub.co.uk pipeline subdomain to Cloudflare nameservers and (b) accept a Cloudflare DPA for /extract transit is a policy/commercial call only the owner can make — it is the hard prerequisite for the §F default (IA §7).


Ledger-write intents (for the Orchestrator to apply on main — not written here)

Section titled “Ledger-write intents (for the Orchestrator to apply on main — not written here)”
  • {127.20} journal append: “S456 architecture assessment authored (reference/arch-assessment-compute-posture-s456.md). Ingress recommendation refined: Cloudflare Tunnel + Access service token remains the DEFAULT, but (a) the §C TLS-termination analysis adds a ‘no third party sees content cleartext’ axis, and (b) a current-facts update — Vercel Static IPs now self-serve on Pro, $100/mo/project — reopens IP-allowlist as a fourth option and the preferred origin-terminated escape hatch, strictly better than mTLS. mTLS demoted to last-resort. /health external monitor retired (owner-agreed). Subtask stays blocked pending owner ratification of the six decisions + the Cloudflare nameserver/DPA prerequisite (OQ 5).”
  • DR-intents (5), to be written on ratification — one per Exec-summary decision (compute posture; zero-egress definition; per-client tooling-posture-choice-behind-thin-interface; data-plane-per-client/control-plane-central [generalises DR-017]; the {127.20} ingress ruling). Text drafted in the implications table.
  • id-71 Vercel-spec rework intent: the research doc’s §4 owner-ratification questions Q1 (lock-in → thin-and-swappable), Q2 (egress → the §B definition), and the “multi-tenant Vercel infra” framing (§2) are resolved by decisions 2–4 + the per-client-Vercel-projects fact; the spec should be reworked to an adoption spec behind our own interface, not an eve-all-in plan. Q3 (cost ceiling), Q4 (timeline vs id-13*), Q5 (orchestrate-only guard-rail) remain open owner rulings.

Addendum (S456, item-6): the refinery model and what it changes

Section titled “Addendum (S456, item-6): the refinery model and what it changes”

Status: authored S456 after owner ratification of decisions 1–5 (decision 6 held CONDITIONAL pending this addendum — {127.20} journal, 2026-07-09). The main paper derived the ingress ruling from the /extract-era snapshot of the pipeline (upload + URL inlets). The owner’s steer: re-derive the ingress/egress requirements under the ratified corpus reframe (canonical:.lavish/corpus-reframe-review.html, cited CR by its own section names; 8/8 rulings R1–R8 ratified S441 → DR-025/DR-026, refining DR-023/DR-020) before the ruling is final — and answer the rec-4 observability question concretely. Where this addendum disagrees with the main paper it supersedes it explicitly; everything else stands.

What the reframe establishes (the controlling model)

Section titled “What the reframe establishes (the controlling model)”

Four load-bearing propositions, cited to the review’s own sections:

  1. The pipeline is a refinery, and the gate lives at promotion (CR §2 “The target model — the sieve, the gate that moved, and two loops”; CR §5: “the pipeline is a refinery that turns messy evidence into governed canon — and the gate lives at promotion”). The front door is deliberately light (connect a source, assign retention class — the binding gate, DR-020 as restated by DR-025); authority is earned at the knowledge-admission gate, never inherited from a folder.
  2. Connectors are the identity of the pipeline, not a deferred fork (CR §3, “Discovery agents, connectors, transcripts, PDF ingestion — elevated from ‘deferred fork’ to the identity of the pipeline”): “it’s the same sieve with more inlets” — uploads, watched folders, SharePoint/Drive, transcript feeds, websites, HRIS are the evidence-streams box of the CR §2 flow diagram, each binding carrying keep / ingest-once / live classes.
  3. The source-binding register is connector-first by design (CR R8): every binding records origin type, locator, retention class, cadence, auth as first-class columns, so each cocoindex native connector later “slots into an existing row shape instead of forcing a redesign” (encoded as a {138.3} must-encode).
  4. A fourth pattern exists beyond ingestion: external-referenced (CR §3, DR-025) — knowledge the platform points at but never ingests (CRM stays in HubSpot; agents consume it in place alongside Canonical).

The consequence the main paper under-weighted: the refinery acquires evidence by pulling. In cocoindex a connector is a source the engine reads (CR §2’s sieve; DA §5.4 incremental walk semantics); the recurring trigger fires on loopback inside the box (DA §4 ingress-posture, DA §5.3). “More inlets” therefore means more outbound pull lanes from the VPS, not more inbound crossings of the pipeline ingress.

G. What actually crosses the ingress, per inlet class

Section titled “G. What actually crosses the ingress, per inlet class”

Definitions: the pipeline ingress = the Cloudflare-fronted /walk|/health|/extract surface (DA §4). Pull-from-client-source = the VPS reaches out to fetch evidence (egress from the box). Push-through-ingress = content bytes arrive inbound across the pipeline ingress.

Inlet class (CR §2 / R8 origin types)How evidence movesCrosses the pipeline ingress?New egress lanes from the VPSClass
Gated upload (today)User → app → corpus bucket in the client’s OWN Supabase project (DR-025 demoted-bucket role); pipeline pull-syncs bucket → VPS pre-walk (DA §5, DR-023)Only the /walk nudge — bearer + pointer, metadataBucket pull-sync (Supabase — existing accepted processor, §B)Pull
Watched folder (keep-and-watch)Cadence re-walk of the synced working copy; scheduled trigger is loopback (DA §5.3)Nothing on the scheduled path; metadata nudge if app-triggeredSame bucket/source pull-syncPull
Drive / SharePoint connectors (live-connected, future)cocoindex native connector: VPS makes outbound OAuth-credentialled reads of the client’s own Google/Microsoft tenancyNothing — connector reads never transit the ingressNEW: VPS → Graph/Drive APIs (client’s own tenancy; credential brokering per posture — Nango local / Vercel Connect cloud, §D)Pull
Transcript feeds (ingest-once, future)Two shapes: (a) poll the provider API on cadence — outbound; (b) webhook push — must terminate at the app (public-by-design, proxy.ts-gated) which lands bytes in the client bucket → nudge → pull-sync(a) nothing; (b) nothing, by design rule — the push is absorbed app-side, never at the pipeline ingressVPS → transcript-provider API (client’s own account)Pull (push absorbed at app/bucket)
URL / feeds (today)The app’s SSRF-gated fetch retrieves the page; the fetched HTML body is POSTed through /extract for cleaning (lib/extraction/clean-via-worker.ts:1-28; server.py::_extract_handler contract)Yes — the one content-bearing crossing. Code-truth materially bounds it: what crosses is already-public web HTML the client asked to import, never client corpus documents; the worker is a pure cleaner — it does not fetch and has no SSRF surfaceApp-side (Vercel) egress does the fetch, not the VPSPush (the sole one; see re-homing below)
External-referenced (CR §3 fourth pattern)Never ingested; data-plane agents consume in place (HubSpot etc.)Nothing — no pipeline involvement at all; register row is metadata onlyAgent-side outbound to the client’s SaaS (per-client boundary, §E)Neither

Verdict on the §C claim: the “only control metadata transits Cloudflare on the primary flow” claim survives — and generalises. Every connector-class inlet the reframe adds is pull-shaped by construction, so the connector-first future makes the ingress more control-plane-pure, not less. The one push-through-ingress lane is /extract, and code-truth softens the main paper’s characterisation of it (supersedes §C/§F’s “document bytes” phrasing): the bytes are app-fetched public-web HTML, not client documents. Two design rules fall out, both {138.3} must-encodes:

  • The pipeline ingress is control-plane only. Push-shaped inlets (webhook feeds, future uploads) terminate at the app/bucket layer; evidence reaches the VPS exclusively by pull. New connectors must never add a content-bearing ingress route.
  • Per-binding egress derivation. The connector-first register’s origin-type + locator + auth columns (CR R8) double as the source of a per-binding outbound host allowlist for the VPS — the register that admits an inlet also bounds its egress lane. (Egress policy is adjacent to, not part of, the {127.20} ingress gate.)

/extract re-homing (sharpens main-paper OQ-1, supersedes its framing): the naive re-homing (“make the worker pull the URL like /walk pulls the bucket”) is wrong — it would recreate an SSRF surface on B1 that {112.10} deliberately removed (the app’s validateUrl is the SSRF gate; the worker is fetch-free by contract). The content-light re-homing that preserves both properties is: app fetches (keeps the SSRF gate) → writes the HTML to the client bucket landing zone → walk-nudge → pipeline pull-syncs. Whether that indirection is worth it over a Cloudflare DPA covering public-web-HTML transit is now a much smaller question than the main paper implied.

H. Does the {127.20} recommendation change?

Section titled “H. Does the {127.20} recommendation change?”

It holds, and strengthens. Stated plainly: ratify §F as written — Cloudflare Tunnel

  • Access service token as the default private ingress, Vercel Static-IP allowlist (Pro) as the origin-terminated per-client escape hatch, mTLS last-resort, /health monitor retired. The reframe removes weight from the tunnel’s only demerit: edge TLS termination mattered in proportion to content crossing the ingress, and under the connector-first model the ingress trends toward pure control metadata (§G) while the sole content lane carries public-web HTML. Meanwhile the tunnel’s advantages (origin genuinely unreachable, free Zero Trust tier, DNS-only rollback) are more valuable with more inlets, because the VPS’s inbound surface stays closed no matter how many connectors are added outbound.

One amendment to how the ruling is recorded (not to its substance): the DR that lands the {127.20} ruling must carry the ingress-is-control-plane invariant from §G alongside it — otherwise every future inlet (first webhook-shaped transcript feed, first native connector) re-opens the Cloudflare-vs-origin-terminated debate. The ruling and the invariant are one decision: the tunnel is safe because the ingress is control-plane; the ingress stays control-plane because push inlets terminate app-side. The subdomain move to Cloudflare nameservers + the DPA (main-paper OQ-5) remain the owner’s prerequisite call — with the DPA’s scope now honestly describable as “control metadata + client-requested public-web HTML in transit”.

I. The central observability plane (rec-4) — concrete recommendation

Section titled “I. The central observability plane (rec-4) — concrete recommendation”

No single tool is the plane. The recommendation is a two-leg hybrid, both legs telemetry-only per decision 4, with Raindrop explicitly not in it:

  1. Vercel AI Gateway, team scope — the model-telemetry leg (cloud track). Live docs (checked 09/07/2026, page last_updated 2026-06-20) confirm the Gateway’s observability is team-scoped with per-project drill-down: “Stay in your team scope to see aggregated metrics across all projects”, Requests summaries by project and by API key (request count, tokens, P75 duration, P75 TTFT, cost), spend charts, and exportable request logs (observability docs; longer retention needs Observability Plus). Because every per-client Vercel project lives in the single Platform team (platform-context.md: “each client gets its own Vercel + Supabase project”; the tw-group team hosts canonical-platform + the first client’s project), the team-level AI Gateway dashboard IS the per-client rollup the owner asked about — per-client projects give per-client rows for free, per-API-key summaries give per-agent breakdown within a client, and none of it carries corpus content. Caveats: (a) this leg exists only for requests routed through the Gateway — adopting it for cloud-track clients is what creates the leg (advances main-paper OQ-4 toward “adopt”, matching the owner’s lean); (b) ZDR team-wide is $0.10/1k requests and BYOK keys are skipped by default under ZDR (§B); (c) the rollup depends on client projects staying in the Platform team — a client who demands their own Vercel account drops to leg 2.
  2. The Platform-DB telemetry rollup — the system-of-record leg (covers what the Gateway cannot see). The pattern already exists and is proven: the pipeline POSTs /api/internal/pipeline-runs/recordpipeline_runs rows → the datapath-watch monitor (DA §5.6; platform-to-client-promote.md §2.2–2.3 uses it as the promote gate). Generalise it to an agent_runs-shaped telemetry feed: every data-plane runtime — on-prem goose, local-stack (LiteLLM spend logs), pipeline walks, and cloud agents alike — posts run status, tokens, timings, error class to the Platform DB. This is the only leg that covers strict-on-prem clients (who never touch the Gateway) and the only one the platform owns end-to-end; it is the system of record, with the Gateway dashboard as the richer live view for the cloud track.
  3. Raindrop is not a leg and cannot be. platform-context.md is binding: Raindrop is local dev/CI eval-authoring tooling only (localhost:5899, zero egress enforced in lib/eval/graduation.ts); hosted raindrop.ai is permanently excluded from the client-data path (DA §8). Its place in this story is upstream: evals authored in Raindrop graduate into CI and inform what the telemetry legs should alert on — it is never the hosted runtime plane.

J. Platform-agents lifecycle — the owner’s model, confirmed with three refinements

Section titled “J. Platform-agents lifecycle — the owner’s model, confirmed with three refinements”

The model — “build/test platform agents on our own Platform Vercel project fleet, then propagate to clients when ready” — is correct, and it is not a new mechanism: it is the already-ratified prove-on-Platform → pin → config-delta promote pattern (runbooks/platform-to-client-promote.md: prove the full E2E path on the Platform rig → cut an immutable release → apply a per-client config delta, “the same image moves Platform → client unchanged; only config differs”) generalised from the pipeline image to the agent layer. The goose pilot is this model already in motion: pilot on Server B, staging-pointed, gates G0–G6, then per-client rollout (DR-017; DA §7). Three refinements so the model stays aligned with decisions 3/4:

  • Platform is client-zero, on Platform data. Building/testing on the Platform fleet means exercising agents against the Platform’s own Supabase project and corpus — never a client’s. The Platform Vercel project is a true tenant in the per-client topology, not a shared runtime that peeks across tenants.
  • “Propagate when ready” = release-pinned per client, never fleet-push. The promote is per-client and recorded (per-client-release-pins.md append-only history); an agent version proven on Platform is offered to each client on that client’s schedule, exactly as pipeline vX.Y.Z pins work today. Per-client deltas are minted fresh (service-actor creds, DB refs, AI Gateway keys — the same shape as the runbook §4 env re-key).
  • The propagation target differs by posture (decision 3). For a cloud-track client the target is that client’s Vercel project (the first client is cloud-native — owner, S456); for a strict-on-prem client it is the local stack on their VPS. The thin swappable interface (decision 3) is what makes one proven agent artefact propagatable to either.
#Main paper saidAddendum position
1§C: content bytes cross the ingress “on the /extract cleaner path” — characterised as document bytesSoftened (supersedes): code-truth shows /extract carries app-fetched public-web HTML (worker is a fetch-free pure cleaner, no SSRF surface); client corpus documents never cross the ingress on any flow
2§C claim scoped to the “primary ingest flow” (the /extract-era snapshot)Generalised: the claim survives per-inlet under the connector-first model — every CR §3 inlet class is pull-shaped; the ingress trends more control-plane-pure as inlets grow
3§F ruling stood on its ownUnchanged in substance, amended in recording: the DR must co-carry the ingress-is-control-plane invariant (push inlets terminate app-side; connectors are outbound pull) as a {138.3} must-encode, so new inlets don’t re-open the ruling
4OQ-1: re-route /extract “through pull-from-bucket (like /walk)“Sharpened (supersedes): worker-side fetching would recreate the SSRF surface {112.10} removed; the content-light path is app-fetch → bucket → pull-sync — and the residual exposure the DPA must cover is public-web HTML only
5OQ-4 (AI Gateway) left as an undecided tradeAdvanced: team-scope observability confirmed live (per-project + per-API-key rollup, exportable logs); adopting the Gateway for cloud-track clients is what creates the model-telemetry leg of rec-4 — aligns with the owner’s lean; BYOK-under-ZDR caveat stands
6Decision 4 named a central telemetry-only control plane but not the toolAnswered (rec-4): hybrid — AI Gateway team dashboard (cloud-track model telemetry) + Platform-DB pipeline_runsagent_runs rollup (system of record; sole coverage for on-prem); Raindrop ruled out as a hosted plane by the platform-context constraint
7OQ-2: which client is which posture — unknownPart-answered by the owner (S456): first client is cloud-native (per-client cloud track); goose pilot still completes; 4 GB VPS capacity is dev-phase temporary
8— (not covered)New: the connector-first register’s origin/locator/auth columns double as the per-binding outbound egress-host allowlist — the register that admits an inlet bounds its egress lane (ID-138-adjacent, not part of {127.20})
  1. Vercel team residency as a standing assumption. The rec-4 Gateway leg assumes client projects remain in the Platform’s Vercel team. If a future client demands their own Vercel account, their model telemetry falls to the DB-rollup leg — acceptable, but worth recording as an explicit assumption when decision 4’s DR is written.
  2. Transcript-feed provider shape. Whether the first transcript inlet is poll- or webhook-shaped is a per-provider fact; either way the §G design rule (webhooks terminate app-side at the bucket) holds — the open item is only which provider and cadence.
  3. agent_runs telemetry schema ownership. Whether the generalised telemetry rollup is specced in ID-138 (register-adjacent) or id-71 (agent-layer) needs an orchestrator call; it is small either way, but decision 4’s DR should name its owner.
  4. Cloudflare DPA + nameserver move (main-paper OQ-5) — unchanged, still the owner’s prerequisite call, now with the narrower honest scope from §H.