Control plane
AppDeployment + AppProductionAlias models behind /api/apps/{app_id}/deployments. The backend owns every Cloudflare API call with its token.
Base44 · full-stack apps in production · internal tracker
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. Nothing is merged to production yet, though: ahead lie the merges, the custom-domain last mile (Cloudflare for SaaS), and production hardening. Hence 15%.
Every one deployed by the real CLI to staging — click them.
Built and verified end to end against the real staging Cloudflare account and the preview backend — but every PR is still open. Nothing is merged to production.
Full lifecycle proven live: create → finalize → promote ×2 → rollback (returns the previous production deployment) → per-deployment logs. Dispatcher does KV routing, reverse proxy, dumb SSR cache, pre-dispatch rate limiting, private-app edge gating, plus the domains API.
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).
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.
Base44 Estates (RR7), Base44 CRM (TanStack Start), Meridian (Astro 6) — 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".
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.
base44 domains add <hostname> [--wait] / list / remove documented so agents drive the same flow users do.
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.
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.
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).
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.
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.
Started, tracked, and expected to land — but not proven live yet.
Wildcard *.b44apps.dev/* Worker route created and fallback.b44apps.dev + apex DNS bound. Two Cloudflare-for-SaaS walls remain (see Up next). Once SaaS is enabled on the zone: create the */* catch-all, then base44 domains add app.netanelgilad.com --wait prints the CNAME, polls TLS to active, and the subdomain serves white-label.
The dispatcher's Set-Cookie rewrap turned 101 upgrades into 500s, so realtime rides a cross-origin WS to the platform origin for now. Pass-through fix is on the PR branch — tracked follow-up until verified live.
Astro 7 shipped Jun 22; the examples run Astro 6. Same @astrojs/cloudflare-on-Vite-plugin contract expected — needs a live Modal run to confirm.
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.
After a slug rename the CLI still prints the classic published-url. Small follow-up on #559.
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.
The honest list — unowned platform work, classic↔fullstack divergences, and tracked risks. Why the answer up top isn't "YES."
The catch-all */* route is refused until SaaS is enabled (error 100327), and the custom_hostnames / fallback-origin API rejects wrangler's ssl_certs:write (needs real "SSL and Certificates: Edit"). Dashboard steps: enable Custom Hostnames (free tier: 100), set fallback origin to fallback.b44apps.dev (DNS record already exists), add the SSL-edit scope to the staging token. This is THE blocker for the custom-domain test.
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.
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.
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.
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.
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.
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.
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.
$0.02/mo per script past 1,000 — prune old deployments by tag (keep last N + anything aliased).
POC uses KV (≤60 s worst case); production upgrade is Durable Objects or cache + Instant Purge (<150 ms).
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.
The dispatcher / KV / Workers-for-Platforms picture: one worker per deployment, one KV record per hostname, one dumb cache.
| URL | Kind | Behavior |
|---|---|---|
dep-<id>.b44apps.dev | Immutable | Every deployment, forever addressable until pruned. |
<slug>.b44apps.dev | Production alias | --prod / promote repoints it; rollback repoints to the previous production deployment. No rebuilds. |
custom domains | Alias | Cloudflare-for-SaaS custom hostnames; white-label, tenant CNAMEs in. |
AppDeployment + AppProductionAlias models behind /api/apps/{app_id}/deployments. The backend owns every Cloudflare API call with its token.
One user worker per deployment (dep-<id>) in the dedicated base44-fullstack-staging namespace — the framework's SSR module graph + static assets + secret bindings + observability. Render is never in the request path: browser → Cloudflare edge → dispatcher → user worker.
Resolves host:<hostname> in KV → {script, deployment_id, app_id, access, tier, rl}. Reverse-proxies the platform surface (/api/apps/*, /ws-user-apps/*) so apps stay same-origin and custom domains are white-label. Edge-gates private apps, rate-limits pre-dispatch, and runs a dumb SSR cache that honors only the response's Cache-Control.
Build → CLI reads .wrangler/deploy/config.json (the generated wrangler.json is the contract, exactly what wrangler deploy does) → POST /deployments with a salted asset manifest → upload only missing buckets directly to Cloudflare (session JWT) → finalize PUTs the namespaced script and writes the KV route.
Asset dedup is namespace-global by hash; manifest hashes are salted sha256(app_id ‖ bytes) so an app can only collide with itself. Load-bearing, not defense-in-depth: Cloudflare does NOT verify uploaded bytes against the claimed hash (tested).
The key design calls, and why they went the way they did.
POC ships a per-app, app_id-scoped, 90-day BASE44_SERVICE_TOKEN as a secret_text binding — a leak hits one app, never cross-app, rotatable in place via the WFP secrets API. Option (c) (short-lived token bootstrapped by a long-lived binding secret) collapses to the same blast radius plus a hop — dropped. Production is Option D: a Cloudflare outbound worker injects a backend-minted, cached, short-lived token so the tenant never holds the credential. Tokens are HS256 per-app-HMAC with no JWKS, so edge-minting isn't viable — another reason A→D is right. Guardrails: server-only, never serialized into SSR HTML, per-app secret versioning, revoke-on-undeploy.
Enforced on the cache-MISS path right before DISPATCHER.get — the billable event — so cache HITs and static assets are never limited. Workers Rate-Limiting binding for per-second burst (per-colo, free) + a Durable Object for accurate global daily & workspace-pooled quotas in production. The routing record carries {workspace_id, target, tier, rl}, resolved at deploy/promote/rollback. New platform job to flag: reflow a workspace's routing records on plan change (hangs off the billing mirror).
The backend deploy contract (config + modules + asset_manifest) never mentions wrangler — Cloudflare only leaks on the CLI side. Phase 0 (today): the generated wrangler.json stays as the power-user escape hatch. Phase 1: Base44-native config (site.framework: "auto" + a runtime block) with framework detection → presets, à la Vercel Build-Output-API / Netlify framework-info / Nitro presets. Phase 2: a @base44/deploy wrapper owns it. Phase 3 (optional): emit the backend's normalized contract directly, unlocking non-Cloudflare runtimes. The dormant detectFramework() already in the CLI is the seam.
The dispatcher inspects nothing on the request and honors only the response's Cache-Control (private / no-store / Set-Cookie opt out) — cacheability is the app's concern. Chosen after a real bug: the original any-cookie bypass meant browsers (which always carry Cloudflare's __cf_bm cookie) never got HITs. Re-verified with cookies: MISS 1265 ms → HIT 83–96 ms, render timestamp frozen.
host:<hostname> → {script, deployment_id, app_id}. Slug production hosts, custom domains, and immutable dep hosts are all just keys; promote/rollback rewrite only the alias key. No second routing path to drift.
The dispatcher reverse-proxies only /api/apps/* + /ws-user-apps/*. Found the hard way: it originally claimed /login and /logout, shadowing the CRM's own login page (apps routinely define one). The private-app edge gate redirects to the platform origin's hosted login absolutely — the same round-trip published apps use.
User workers run in base44-fullstack-staging, never the shared namespaces. The dispatcher fronts everything as a reverse proxy — apps are same-origin (no CORS), custom domains are white-label, and the Base44 origin is never in the request path.