Base44 · full-stack apps in production · internal tracker

ARE WE FULLSTACK YET? NOT YET.

Fullstack readiness 18%

But the POC is real: Astro, React Router 7 and TanStack Start apps deploy through the real base44 CLI and serve direct from Cloudflare on b44apps.dev — immutable deployments, promote/rollback, edge SSR caching — proven end to end on staging. And the merge stack is ready: the big integration PR is now split into a 10-PR stack + 1 standalone (this tracker moved to its own repo), every one green, three mergeable this minute. Still zero lines merged to production, though — ahead lie the merges themselves, the custom-domain last mile (Cloudflare for SaaS), and production hardening. Hence 18%.

Updated July 19, 2026 · 24 in review · 5 in progress · 24 up next · 5 demo apps live on staging

Live demos

Every one deployed by the real CLI to staging — click them.

In review

24

Built and verified end to end against the real staging Cloudflare account and the preview backend. The big apper PR (#14839) is now split into a merge-ready 10-PR stack + 1 standalone (the dispatcher itself split into sub-PRs 3a–3e) — #14839 stays open as the integration/staging reference (everything was live-verified there); merging happens via the stack. Nothing is merged to production yet.

Hosting infra: models, routing store, CF client Bottom of the stack, base: main — pure infra, zero routes registered, merging is a prod no-op. on hold · model redesign Live on staging apper #15396 ↗

AppDeployment + AppProductionAlias models (app-scoped, IDOR-safe), host/URL config with POC-mode pending:// sentinels, KV routing-store writes (host:<hostname> → {script, deployment_id, app_id}), and the CF WfP client extensions (assets-upload-session, KV put/delete, Cloudflare-for-SaaS hostname CRUD + zone lookup). 57 unit tests green, ruff clean. Live-verified on the #14839 staging preview. ON HOLD: the backend layers are being redesigned — AppDeployment becomes AppVersion, likely reusing the existing AppCheckpoint model; Pavel is driving the apper-side design (general preview environments). This PR stays open as reference input to that design.

Deployments control-plane API create → finalize → promote / rollback / list / logs under /api/apps/{id}/deployments. on hold · model redesign Live on staging apper #15397 ↗

The full deploy lifecycle on an AppAdminRouter: create reserves the dep-<id> script + opens the Cloudflare assets-upload-session; finalize PUTs the namespaced script (secrets + app-scoped BASE44_SERVICE_TOKEN bindings) and writes the immutable-host route; promote/rollback flip the AppProductionAlias pointer. CLI drives it via the existing apps:deploy workspace-key scope; all mutations audit-logged. ON HOLD: the backend layers are being redesigned — AppDeployment becomes AppVersion, likely reusing the existing AppCheckpoint model; Pavel is driving the apper-side design (general preview environments). This PR stays open as reference input to that design.

Dispatcher core: KV host routing + dispatch Bottom of the dispatcher sub-stack: resolves host:<hostname> from KV, dispatches to the user worker. 3a · mergeable now (based on main) Live on staging apper #15765 ↗

The old dispatcher monolith #15398 (3/7) was closed and split into sub-PRs 3a–3e for reviewability — the sub-stack's tip is byte-identical to the monolith, so reviewers see granularity, not new behavior. This slice is the self-contained worker skeleton under workers/base44-apps-dispatcher/ (no backend files change): KV host routing + dispatch into the namespace. The sub-stack was re-founded on main; the dispatcher couples only to the KV RouteRecord contract, so it merges independently of the apper model redesign.

Dispatcher: platform-surface reverse proxy Reverse-proxies /api/apps/* + /ws-user-apps/* same-origin — WebSocket upgrades pass through untouched. 3b · stacks on 3a · independent of backend redesign Live on staging apper #15766 ↗

The platform surface rides the app's own origin (no CORS, white-label custom domains), and WebSocket upgrades pass through untouched — the wss://crm.b44apps.dev fix rides here.

Dispatcher: shared SSR cache The dumb SSR cache — inspects nothing on the request, honors only the response's Cache-Control. 3c · stacks on 3b · independent of backend redesign Live on staging apper #15767 ↗

Deliberately dumb: cacheability is the app's concern (private / no-store / Set-Cookie opt out). This is the cache behind the proven MISS 1265 ms → HIT 83–96 ms numbers.

Dispatcher: private-app auth gate Edge-gates private apps without ever shadowing an app's own /login. 3d · stacks on 3c · independent of backend redesign Live on staging apper #15768 ↗

Private apps are gated at the edge with a redirect to the platform origin's hosted login — the dispatcher never claims /login, so apps keep owning their own login pages.

Dispatcher: rate limiting + kill switch Tier-driven rate limiting pre-dispatch + the kill switch — tip of the dispatcher sub-stack. 3e · stacks on 3d · independent of backend redesign Live on staging apper #15769 ↗

Rate limits enforced on the cache-MISS path right before dispatch (the billable event), tier-driven from the KV routing record, plus the kill switch. Tip of the sub-stack — byte-identical to the old #15398 monolith.

Custom domains + OAuth domain validation Cloudflare-for-SaaS hostname connect/list/disconnect + app-scoped OAuth callback acceptance. blocked behind model redesign Live on staging apper #15399 ↗

AppCustomDomain model + /api/apps/{id}/domains (connect creates the CF custom hostname with DV cert issuance and routes it at current production). The AppDomainValidator extension is purely additive and deliberately app-scoped — an app's own dep- host, production host, or connected custom domains only, so one tenant can never capture another app's callback token. Inert until full-stack hosts exist, so classic OAuth validation is byte-for-byte unaffected. 75 unit tests green.

Slug renames re-point production routing repoint_production_alias hooks the existing update_slug endpoint — smallest slice in the stack. blocked behind model redesign Live on staging apper #15400 ↗

Writes the KV route for the new <slug>. production host and deletes the old one when an app is renamed; no-op when the host is unchanged or the app has no production deployment. Runs after the slug is persisted, so a Cloudflare failure logs instead of failing the rename. Live-verified on the #14839 preview: renames moved the routing record back and forth with zero manual steps.

This tracker app (yes, this very page) are-we-fullstack-yet moved to its own repo (base44-dev/awfy) — no longer part of the apper stack. own repo · out of the stack Live on staging base44-dev/awfy ↗

Originally split out as stack PR apper #15401, then moved wholesale to the dedicated base44-dev/awfy repo (the apper PR was closed and the code removed from the monorepo). It documents the initiative (this page replaced the session artifact as the single source of truth) and dogfoods the very deploy pipeline it tracks — the page you're reading ships through the full-stack CLI flow to are-we-fullstack-yet.b44apps.dev.

Probe API — TEMPORARY, staging-only Prod-blocked debugging surface, remove before GA — top of the stack, goes in last (or never). 7/7 · merges last apper #15402 ↗

/api/fullstack-probe/* — the hardened scaffolding used to empirically validate every WfP assumption from a PR preview. Fails closed in production: disabled unless FULLSTACK_PROBE_ALLOWED_EMAILS is set (it never is in prod) and every endpoint requires an allowlisted platform user. Kept as its own clearly-marked PR so the eventual removal is a clean revert — everything greppable via FULLSTACK_PROBE.

Agent model ids validated at push time Stale model ids now 400 at push with the supported list, instead of 404ing at first chat. standalone · mergeable now Live on staging apper #15403 ↗

Extracted from #14839 but not fullstack-specific, so it rides solo off main. validate_runtime_model_strict rejects anything that isn't a preset or a wire string the runtime actually serves; agent create/replace/update return a 400 naming the agent and listing supported ids. Persisted configs keep the lenient validator on purpose so legacy data still loads. 100 unit tests green.

CLI: deploy / promote / rollback / logs / slug / domains The real base44 CLI builds, uploads to Cloudflare, deploys. 12/12 CI checks green. Live on staging cli #559 ↗

The real base44 CLI builds, uploads assets straight to Cloudflare, and deploys via the deployments API. base44 slug set/reset and base44 domains add/list/remove included. Fully green across all 12 CI checks (fixed knip dead exports, a Windows line-endings hash bug, and a Bun charset assertion on the way).

SDK server client createServerClient({ request, env }) — appId/serverUrl from worker env, auth from a cookie. Live on staging sdk #221 ↗

createServerClient({ request, env }) resolves appId/serverUrl from worker env and auth from a cookie. Worker-safety fixes (analytics timer guard, fetch adapter, absolute URLs, no shared-instance leak) ride the same PR; a non-HttpOnly cookie mirror bridges browser → SSR.

Three flagship example apps Estates (RR7), CRM (TanStack Start), Meridian (Astro 7) — builds green, secrets out of client bundles. Live on staging apps-examples #15 ↗

Base44 Estates (RR7), Base44 CRM (TanStack Start), Meridian (Astro 7) — all builds green with the .wrangler/deploy/config.json contract; server client + service token verified absent from client bundles. Bonus find: Astro 6 removed Astro.locals.runtime.env — use import { env } from "cloudflare:workers".

Docs: 9 pages on developers/full-stack-apps Preview verified green; the scary 404s were a transient deploy race, not broken pages. mintlify #1519 ↗

Preview verified green — the scary 404s were a transient deploy race (an auto-llms.txt commit triggered an overlapping build), not broken pages. Mintlify's link-rot crawl across all 9 cross-linked pages passed.

CLI skill documents domains for agents base44 domains add <hostname> [--wait] / list / remove — agents drive the same flow users do. skills #132 ↗

base44 domains add <hostname> [--wait] / list / remove documented so agents drive the same flow users do.

Routing is exactly the slug-from-KV design KV is the dispatcher's ONLY routing source; promote wrote the slug host by itself. Live on staging proof: real slug host ↗

The dispatcher's ONLY routing source is KV: host:<hostname> → {script, deployment_id, app_id}. Slug hosts, custom domains, and immutable dep hosts are all just keys; promote/rollback rewrite only the alias key. Turning it on caught bug #2 — routing_store wrote bare-hostname keys while the dispatcher read host:-prefixed ones. Fixed (45/45 tests green) and proven: promote wrote host:meridian-feb7cfba.b44apps.dev by itself, MISS 946 ms → HIT 124 ms, zero manual routing steps.

Slug changes carry the production URL One UI chokepoint re-points the fullstack routing record — verified live via the real CLI. Live on staging cli #559 ↗

PATCH /api/apps/{id}/metadata/slug (the single UI chokepoint) re-points the fullstack routing record as part of the rename — new host: key written, old deleted, reset-to-auto handled. Verified live with the real CLI on Modal: renames moved the KV record back and forth. estates/crm/meridian.b44apps.dev are now true slug hosts — no manual alias records left.

Login story fixed end to end Apps own /login (email + OTP via the real SDK); OAuth callback validation is app-scoped. Live on staging apper #15399 (4/7) ↗

All three demo apps now own /login with email/password sign-in + sign-up + OTP via the real SDK surface. The OAuth gateway allowlist was fixed on the preview, and AppDomainValidator now accepts hosts provably owned by the initiating app (slug host / dep-* host / registered custom domains — deliberately app-scoped so one tenant can never capture another app's callback token). The validator change now merges via stack PR 4/7.

Round-2 click-testing fixes RR7 meta() replacement gotcha, CRM env threading, every mock/fallback removed. Live on staging apps-examples #15 ↗

RR7 gotcha: a leaf route's meta() REPLACES the root's, so base44:app-id tags existed only on / — now rendered in the root layout head everywhere. CRM's browser SDK defaulted to prod base44.app — runtime {appId, apiUrl} now threads from Worker env to every client call site. All mock/fallback data removed: pages render real entity data or honest empty states.

Astro 7 + hybrid SSG live on Meridian astro 7.0.6 + adapter 14.1.1 deployed; /about + /brewing prerendered, SSR catalog cached. Live on staging proof: prerendered /about ↗

Meridian upgraded astro 6.4.8 → 7.0.6 with @astrojs/cloudflare 14.1.1 and redeployed live: /about and /brewing are prerendered and served as static assets (real 404 status included) while the catalog stays SSR + edge-cached — each page wears its mode. One breaking change handled (compressHTML pinned to the v6 behaviour). Full verdict + the platform gaps it surfaced live in the Research tab.

Dispatcher WebSocket pass-through Fixed — socket.io handshake verified live through wss://crm.b44apps.dev. Live on staging apper #15766 (3b/7) ↗

The dispatcher's Set-Cookie rewrap turned 101 upgrades into 500s, forcing realtime onto a cross-origin WS to the platform origin. The dispatcher fix (originally apper commit 09abfcf6 on #14839) is deployed on staging, was verified live through wss://crm.b44apps.dev, and now merges via stack PR 3b/7. Production hardening (keepalive invariant, transport forcing, rate limits, soak/billing, Durable Objects) is tracked in Up next.

Security directives, all built Dedicated namespace · reverse proxy · private-app edge gate · rate limiting · service token. Live on staging

Dedicated base44-fullstack-staging namespace · dispatcher is a reverse proxy (no CORS, white-label) · private apps gated at the edge · rate limiting wired into the dispatcher · service token injected per the security sign-off.

In progress

5

Started, tracked, and expected to land — but not proven live yet.

Target data model (ERD) — written up, awaiting team review Environments = mutable pointers, deployments = immutable snapshots of every resource. Target model tab ▸

The proposed end-state ERD — App / Branch / Checkpoint / Deployment / Environment / Data Environment — is written up in the Target model tab, with the POC-validated rationale for the spine and the five open design questions. Each open question is tracked as a blocking card in Up next; the write-up awaits team review before implementation planning.

Custom-domain last mile: the netanelgilad.com test Cloudflare for SaaS ENABLED + */* catch-all created — two dashboard steps left, then the live test.

Progress: Cloudflare for SaaS is now ENABLED on the b44apps.dev zone and the */* catch-all route is created (user enablement + OAuth token). Remaining: the dashboard fallback-origin field (fallback.b44apps.dev — the DNS record already exists) and "SSL and Certificates: Edit" on the staging API token; then the live netanelgilad.com test via base44 domains add — it prints the CNAME, polls TLS to active, and the subdomain serves white-label.

Next.js support (vinext, best-effort) Cloudflare's Vite-based Next is the path; OpenNext needs unsupported bindings.

Cloudflare's Vite-based Next (experimental) is the path; OpenNext needs a deploy-time bundle and its cache stack uses unsupported bindings. Also the framework to watch for the 10 MB gzip / 1 s startup limits.

CLI publish output: print the fullstack production URL After a slug rename the CLI still prints the classic published-url.

After a slug rename the CLI still prints the classic published-url. Small follow-up on #559.

One-click wildcard DNS record One proxied * record makes every slug + dep-<id> host resolve automatically.

A proxied * → A 192.0.2.1 record, combined with the wildcard route already created, makes every slug + dep-<id> host resolve automatically — no per-host binds. One dashboard click.

Up next

24

The honest list — unowned platform work, classic↔fullstack divergences, and tracked risks. Why the answer up top isn't "YES."

CI/CD + PR preview-ability for every Cloudflare-side piece No deploys from local machines: the dispatcher, routing KV changes, and zone config need a full CI/CD pipeline — and everything we introduce must itself be PR-previewable. Gap Blocks “done”

Today the dispatcher (and zone/KV bootstrap) is deployed by hand from dev sessions — the entire POC shipped via ad-hoc scripts. Definition of done: (1) dispatcher deploys run only from CI (wrangler-deploy GitHub Action per env: staging zone on merge to main, production gated), with the same for KV/zone provisioning as reviewed migrations; (2) dispatcher/worker PRs get their own preview — e.g. a per-PR dispatcher on the staging apps zone wired to a preview routing namespace — so edge changes are testable before merge, matching how apper PR previews work; (3) the CLI/SDK preview-package publishing already in CI stays the model; (4) no credentials on laptops — deploy tokens live in CI secrets only. This extends the PR-preview principle to every new part introduced by this initiative, since we now deploy to Cloudflare as well as Render.

Integrate legal/support blocking flows with fullstack serving Takedowns, legal blocks, and support-personnel tooling must reach fullstack apps and domains — today only a raw `blocked` flag exists in the routing record. Gap Blocks “done”

The dispatcher honors a `blocked` kill-switch per routing record (429s immediately), but nothing connects it to the platform's actual enforcement surfaces: the backoffice/support-personnel app-blocking flows, legal takedown process, and blocked-terms policies must propagate to fullstack serving — flipping the KV record(s) for every host of the app (slug, dep-*, custom domains), suspending/removing CF custom hostnames on domain-level takedowns, and unblocking cleanly on appeal. Also the reverse direction: fullstack custom domains must appear in the support tooling so personnel can act on them, and blocked-terms checks must apply to fullstack slugs/hostnames (currently they don't — known gap). Definition of done: block/unblock from the existing backoffice flows takes effect at the edge within seconds, covers all host aliases, and is audited.

Observability: Cloudflare → Datadog with cross-request correlation Dispatcher + user-worker telemetry must land in Datadog with a request id that follows browser → edge → worker → platform, or we can't debug production. Gap Blocks “done”

Done means: (1) dispatcher + user-worker logs/metrics shipped to Datadog (Workers Logpush or an observability pipeline → Datadog sink), tagged with app_id, deployment/version id, environment, hostname, cache HIT/MISS, rate-limit outcome, and WS upgrades. (2) A correlation id minted at the dispatcher and propagated via header to the user worker AND to the platform origin on proxied /api/apps/* calls, joined into apper's existing ddtrace traces — so one id follows the whole request path. (3) Dashboards + monitors for per-app error rate, dispatch failures, and proxy/origin errors. Today only the app-facing per-deployment logs API exists (it reads CF Workers Observability directly) — platform-side Datadog debugging is entirely missing.

Design: Environment↔DataEnvironment binding + schema migrations THE missing edge in the target ERD — schema rides the deployment, rows live in the data env. Gap Blocks “done” Target model tab ▸

The data environment tracks the schema version its rows are on; promote gates on compatibility / a migration step. Preview binding must be an explicit per-env option — fresh/seeded ephemeral, copy-on-write fork of prod, or shared-prod (read-only, if at all) — and a preview deployment with draft/loosened RLS must never bind the prod data environment without promoted-policy parity.

Design: definition-vs-instance split for side-effectful resources Connector definitions ride the deployment; OAuth grants are per env — else previews send real emails. Gap Blocks “done” Target model tab ▸

Same split everywhere a resource touches the outside world: workflow execution policy per environment kind (prod runs, previews paused/manual), agent config in the deployment with conversations as data and tool access inheriting the connector problem, and per-environment secrets/env vars with prod/preview/branch scoping.

Design: both preview flavors — dep-<id> URLs and branch environments Immutable per-deployment URLs (already live) AND mutable branch envs tracking the latest green build. Gap Blocks “done” Target model tab ▸

The model needs both flavors first-class: the immutable dep-<id> URL every deployment already gets, and a mutable branch environment that auto-tracks its branch's latest green deployment — neither approximated by the other.

Design: environment pointer history as first-class Rollback/audit need what an env pointed at, when, and who flipped it. Gap Blocks “done” Target model tab ▸

AppProductionAlias.history is the embryo — promote it to a real, queryable pointer history on every environment so rollback and audit are model features, not log archaeology.

Design: environment retention, quotas, and prune policy Environments multiply workers + data envs — script-count cost becomes N×M. Gap Blocks “done” Target model tab ▸

The prune policy — keep the last K deployments plus anything an environment points at — belongs in the model itself, not in ops lore. Covers both the $0.02/mo-per-script worker cost and data-environment sprawl.

Cloudflare-for-SaaS zone setup: two steps left SaaS enabled + */* catch-all created; remaining: fallback-origin field + SSL-edit token scope. Gap

Custom Hostnames (Cloudflare for SaaS) is now enabled on b44apps.dev and the */* catch-all route exists — error 100327 is history. Remaining dashboard work: set the fallback origin to fallback.b44apps.dev (the DNS record already exists) and add "SSL and Certificates: Edit" to the staging API token — the runtime scope base44 domains add uses, so the CLI test runs as a real user.

Prod OAuth gateway allowlist for custom domains Preview gateway fixed; prod needs the same before Google login works on customer domains. Gap

ALLOWED_DOMAIN_PATTERN was fixed on the preview gateway (oauth.velino.org); production needs the same change before Google login works on customer custom domains. Flagged as platform work.

Domain purchase flow is classic-only Purchased domains hardwire Render records — fullstack apps can't serve on them at all. Gap

get_entri_config hardcodes CNAME www → base44.onrender.com + A @ → 216.24.57.1 (Render), and the purchase webhook wires Render + DomainLink unconditionally. Nothing branches on the app being fullstack: purchased domains cannot serve fullstack apps without a fullstack-aware record set, CF hostname creation, and an apex strategy.

No cross-system hostname uniqueness The same hostname can be live in classic AND fullstack — whichever DNS points at wins. Gap

Classic checks only custom_domains; fullstack checks only AppCustomDomain for the same app (Cloudflare's per-zone uniqueness is the only backstop, surfaced as an opaque 502). The same hostname can be live in both systems — whichever target DNS points at wins while the other reports stale verified/pending state.

Billing/abuse parity missing in fullstack domains No capability gate, no 350-domain cap, no blocked terms — free tier can attach domains today. Gap

No Capability.CUSTOM_DOMAINS gate, no 350-domain/user cap, no blocked-terms list, no hostname normalization (case / www.) in add_custom_domain — free-tier users can attach domains via CLI/API today.

Lifecycle divergence: deletion orphans fullstack domains App deletion tears down classic DomainLinks but leaves CF hostnames + KV routes serving. Gap

app_cleanup.py tears down classic DomainLinks but never CF custom hostnames / KV routes / AppCustomDomain rows — orphaned hostnames keep serving after app deletion. Fullstack domains also ignore subscription cancellation/resubscribe and admin disable; no unlink/relink, no domain-level or URL redirects.

Email can't be enabled on fullstack-connected domains Custom email is keyed on a DomainLink — a fullstack domain has none, so it 404s. Gap

Custom email sending is keyed on a DomainLink (get_domain_if_valid 404s without one) — a fullstack AppCustomDomain has none, so it's impossible to enable at all today. base44 domains add prints zero email-DNS guidance, and a forced apex CNAME at providers without flattening conflicts with MX and breaks the customer's mail.

OAuth www mismatch + www/apex handling Exact-hostname callback check fails on www. for fullstack; no apex/redirect story yet. Gap

Fullstack callback validation is exact-hostname (no www.-strip like classic), so callbacks on www.<connected-host> pass for classic and fail for fullstack. Classic covers apex+www with one record + redirect; fullstack needs two CNAMEs, has no redirect between variants, and no apex guidance (CNAME flattening required). Minor: b44apps.dev is absent from the classic blocked-terms list.

WS: pin the idle/keepalive invariant CF closes idle WS at ~100 s; socket.io pings every 25 s — pin it so it can't drift. Risk

Proxied WebSockets live inside Cloudflare's ~100 s idle-close window; socket.io's 25 s ping interval is the only thing keeping connections alive through the dispatcher. That invariant is implicit today — pin it (config + a test that fails if either side's timing drifts) so a socket.io upgrade or a CF behavior change can't silently start dropping idle connections.

WS: force websocket transport through the proxy No long-polling fallback via the dispatcher — every connection should be a real upgrade. Gap

socket.io's default long-polling fallback should not ride through the dispatcher: force the websocket transport for proxied realtime so every connection is a real 101 upgrade, not a stream of proxied polling requests chewing dispatch quota and cache-path exceptions.

WS: count handshakes in the dispatch rate limits The platform-path proxy currently bypasses the per-app dispatch rate limits. Gap

WS handshakes ride the platform-path reverse proxy, which currently bypasses the per-app dispatch rate limits — an app's realtime traffic is effectively unmetered at the edge. Count WS handshakes in the per-app limits before production.

WS: concurrency soak + billing semantics Soak proxied WS under concurrency; confirm what a long-lived proxied socket costs. Risk

Two unknowns before production: how the dispatcher behaves under many concurrent proxied sockets (a soak test), and the billing semantics of long-lived proxied WebSockets on Workers for Platforms — confirm what a held-open connection costs and that it's accounted to the right app.

Durable Objects for apps' own WebSocket servers Separate platform feature — DO bindings are currently rejected by design. Gap

Dispatcher pass-through covers Base44 realtime (the platform socket). Apps authoring their OWN WebSocket servers need Durable Objects, and DO bindings are currently rejected by design (the unsupported-binding fail-fast). Supporting them is a separate platform feature: per-app DO namespaces, migrations, and billing.

Script-count cost $0.02/mo per script past 1,000 — prune old deployments by tag. Risk

$0.02/mo per script past 1,000 — prune old deployments by tag (keep last N + anything aliased).

Alias-flip latency KV is ≤60 s worst case; production wants Durable Objects or Instant Purge (<150 ms). Risk

POC uses KV (≤60 s worst case); production upgrade is Durable Objects or cache + Instant Purge (<150 ms).

Private-app edge gate is cookie-presence only POC checks the cookie exists; production must verify the JWT at the edge. Risk

POC checks the auth cookie exists; production must verify the JWT at the edge — ties into the §S service-token production path.

Click any card for the full story — links open the PR or live proof.