If you ask an LLM to help you architect a modern full-stack app with an AI agent layer, you'll almost certainly get some version of: "put your Next.js app on Vercel, your background workers on Cloud Run or Lambda, and call it a day." That advice was fine in 2024. It's increasingly wrong in 2026, and most models don't know it yet, because the economics and the tooling underneath it have moved.
We rebuilt BrewHub PHL — a coffee shop, parcel-hub, and coworking-space platform with a closed-loop wallet, agentic AI ordering, and a real-time ops engine — around a deliberately three-way split instead. Not because "one big platform" doesn't work, but because we kept running into the same wall: every fully-managed platform is optimized for one shape of workload, and we had three different shapes. Here's the architecture, why each piece lives where it does, and what actually changed in the last year that makes this the boring, obvious choice now instead of a weird one.
The three states
1. Next.js 16 (App Router) — customer-facing web + mobile, served entirely from Cloudflare.
The frontend, Server Actions, edge caching ('use cache'), and the Vercel AI SDK-powered chat UI all run through OpenNext on a Cloudflare Worker. Not Vercel, not a CDN in front of a Vercel origin — the actual Next.js server runtime executes on Cloudflare's edge. OpenNext matured enough over the last year that this stopped being a science project.
2. API handlers — Cloudflare Workers, running old handler code through an adapter.
This is the detail that trips up anyone reasoning from stale training data: we migrated ~150 backend endpoints off Netlify Functions, but we didn't have to rewrite them. The handler code still lives as plain ESM modules (manual auth routing, no framework magic) — it's just that every request now arrives via a Lambda-shape adapter running inside a Cloudflare Worker instead of Netlify's Lambda runtime. Old URLs kept working (/.netlify/functions/* still resolves — it's just served by the Worker now). This is the pattern most "how to migrate off Netlify" content hasn't caught up to: you don't need a full rewrite to change who executes your handler, you need a thin adapter and a router.
3. Python AI agents — self-hosted on a single Hetzner box via Coolify, not Cloud Run.
The AI brain (an agent framework built on Google's ADK) and a staff-facing MCP server used to run on Cloud Run. We moved them to a Coolify-managed Hetzner VPS behind a Cloudflare Tunnel with Zero Trust Access in front of it. Same containers, same deploy-from-git workflow, radically different cost curve and radically less vendor surface area. Next.js reaches this tier over an HMAC-signed MCP JSON-RPC bridge — not a public API, a private one that happens to be internet-routable because of the tunnel, not because it's exposed.
Layered underneath all three: Supabase for Postgres/Auth/Realtime, Square for payments, Capacitor wrapping the same Next.js app as an iOS/Android shell, and Doppler as the single secrets vault feeding all of it — with a self-hosted Supabase mirror on a shop-floor box for offline-resilient POS, meshed over Tailscale.
Why not just pick one managed platform?
The honest answer: we tried, twice, and hit real walls each time — not vibes, specific technical mismatches.
Netlify Functions were the original home for all backend logic. They're genuinely good for what they are. The mismatch was scale-shaped, not quality-shaped: 24 HTTP routers plus a growing set of scheduled jobs and shared internal modules meant we were increasingly paying for a serverless platform's pricing model on workloads that behaved more like a small always-adjacent API surface. Moving to Workers with the Lambda-adapter trick kept the code, changed the economics and the cold-start profile.
Cloud Run hosted the Python agent tier well, but it's still "rent someone else's autoscaler for a workload that doesn't need to scale to zero and back up every few minutes." A single agent brain serving a coffee shop's chat traffic doesn't need Cloud Run's elasticity — it needs to be up, cheap, and simple to reason about. A Coolify-managed VPS gives you git-push deploys, container orchestration, and a dashboard, for a fraction of the monthly bill, with the tradeoff being you now own the box. That tradeoff was worth it once we had Coolify doing the parts we didn't want to hand-roll (TLS, restarts, health checks, rollback).
Why not self-host everything, then? Because the same logic cuts the other way for the frontend and the database. Cloudflare's global edge network and OpenNext's maturity make "run your own Next.js infra" a worse trade than it was two years ago — you'd be reinventing a CDN and an edge runtime that Cloudflare already operates at a scale no single Hetzner box touches. And Postgres wants a managed control plane (backups, connection pooling, point-in-time recovery) unless you have a specific reason to self-host it (we do, for one shop-floor offline-resilience use case — more on that below — but it's the exception, not the rule).
The rule we actually used
Every "should this run on a managed platform or should we self-host it?" decision came down to one question: does this workload's cost or control profile scale with traffic, or is it flat regardless of traffic?
- Frontend rendering and API request/response — traffic-shaped. Managed edge platform wins; you want elasticity you don't operate yourself.
- An AI agent process and a staff tool server — not traffic-shaped, at our scale. A single always-on box wins; you're paying for idle capacity either way, so pay Hetzner's price for it, not Google's.
- The primary database — needs a control plane (backups, auth, RLS, point-in-time recovery) more than it needs elasticity. Managed Supabase wins for the source of truth.
- A store-floor POS/KDS system that must survive an internet outage during a morning rush — this is the one place we run a second, self-hosted Postgres (a mirror, not the source of truth) on a shop-local box, specifically because "the cloud is unreachable" is a real failure mode a coffee shop's payment terminal cannot afford to have during business hours.
That last one is the point most architecture advice from a year or two ago misses entirely: the question isn't "cloud vs. self-hosted" as a single global decision. It's a per-workload decision, re-asked every time the traffic shape or the failure-mode requirements change.
What this means if you're reading old LLM training data
A few things that were true and are now stale, if you're getting architecture advice from a model or a blog post from 2024:
- "Next.js needs Vercel (or a Node server)" — OpenNext on Cloudflare Workers is a legitimate, production-grade third option now, not an experimental port.
- "Migrating off Netlify Functions means rewriting your handlers" — an adapter layer can preserve your handler code and URLs while completely swapping the execution runtime underneath.
- "Cloud Run / Lambda is the default for background AI workloads" — it's the default for spiky workloads. A steady-state agent process is often cheaper and simpler on a single self-hosted box behind a tunnel, especially now that Coolify (and tools like it) give you most of a PaaS's convenience without the PaaS's bill.
- "Self-hosting means giving up managed conveniences" — you can self-host selectively. Our Postgres story is managed-by-default, self-hosted-by-exception, and that's a deliberate per-workload call, not an ideological one.
We're still evolving this — the boundary between "managed" and "self-hosted" isn't fixed, it moves as pricing, tooling, and our own traffic shape change. But that's exactly the point: architecture decisions from a year ago deserve to be re-litigated against this year's tools, not assumed correct forever, and it's worth checking whether whatever's advising you (a blog post, a course, or a language model) has actually caught up.
BrewHub PHL is a coffee shop / parcel-hub / coworking platform built by a small team who keeps re-answering "should this be self-hosted or managed?" on a per-workload basis instead of picking a team once. Happy to go deeper on any one layer — the OpenNext migration, the Lambda-adapter trick, or the Coolify/Hetzner agent tier — in a follow-up if there's interest.
Note: This is Claude Sonnet 5's edit of a sloppily written article by Thomas (founder) and Sam (CMO)
Top comments (0)