CHAPTER 09 · FULL-STACK
Three Deployment Paths: Vercel, Cloudflare, Self-Hosting
Choosing a platform is really choosing the shape of the runtime: is your code a process that stays alive, or instances woken on demand and reclaimed at any moment? Understand that difference and you can predict which code will "silently stop working" on which platform.
- Compare the lifecycle and limits of long-running processes, on-demand function instances and V8 isolates.
- Point out why a piece of code fails on serverless (in-memory state, timers, local files, long connections).
- Describe the steps and key config for deploying LinkNote on Vercel, Cloudflare and your own server.
- Pick a platform from project needs using a decision tree.
Opening: fine on my server, wrong in the cloud
LinkNote's API runs happily on your own server: a module-level let requestCount = 0 does simple rate limiting; a setInterval clears stale drafts every hour; uploaded images are written to local /tmp before processing. After deploying to Vercel or Cloudflare: rate limiting works only sometimes, the cleanup never runs, and image processing occasionally reports a missing file. Not a line of code changed — why did the behavior?
Where the answer lies: on your own server, your code is one long-running process: variables in memory stay, timers keep running, local files stay put. On serverless platforms your code is many instances started on demand: 50 instances may each keep their own count; after handling a request an instance may be frozen or reclaimed, so timers don't keep running; each instance has its own temporary file system, and the next request may land on another instance. So: state can't live in memory, and scheduled jobs and file storage need the platform's dedicated primitives.
Three runtime shapes Deep
| Long-running process (self-host) | On-demand instances (Vercel Functions) | V8 isolate (Cloudflare Workers) | |
|---|---|---|---|
| Lifecycle | Runs until you restart it | Started on demand; reclaimed after idling | Created on demand, very light, reclaimable any time |
| Instance count | You decide (usually 1 to a few) | Auto-scaled, possibly many | Independent in each data center worldwide |
| Cold start | None | Yes (typically hundreds of ms; platforms optimize) | Near zero (ms) |
| In-memory variables | ✓ reliably shared | ✗ per instance only, not durable | ✗ same |
| Background timers | ✓ | ✗ use platform Cron | ✗ use Cron Triggers / Durable Object alarms |
| Local files | ✓ | Temp dir only, not guaranteed across requests | ✗ use R2 / KV |
| Execution time | Unlimited | Capped (by plan) | CPU time capped; time waiting on I/O doesn't count |
| WebSocket / long connections | ✓ | Limited | ✓ via Durable Objects |
| Billing | Per machine (fixed) | Usage (CPU time, invocations) | Usage (CPU time, requests), very low unit price |
A long-running process is a shop you run yourself: the clerk (process) is always there and the ledger (memory) sits on the counter. Serverless is a temp agency: each job gets a temp worker who leaves when done, and the next job may go to someone else. The ledger can't live in a temp's pocket; it has to be somewhere everyone can reach (database, KV).
Where it breaks: platforms reuse "warm" instances, so in-memory variables sometimes survive across requests — which is exactly why the bug shows up as "works sometimes". Don't rely on it, but you can use it for pure performance caching (e.g. reusing a DB client object).
One request, three paths
Switch tabs to see what the same request passes through on each platform:
The Vercel path Use
Vercel's greatest value is the "git push to deploy" experience:
- Import the GitHub repo in the Vercel dashboard; it detects the framework (Next.js, TanStack Start, Vite…) and build command.
- Every push to main → a production deploy; every pull request → its own preview deployment with its own URL. This is enormously useful with agents: the agent opens a PR, you open the preview link and accept or reject.
- Set env vars separately for Production / Preview / Development in the dashboard; sync locally with
vercel env pull. - Databases, Redis, Blob and so on come from third parties via the Marketplace (Neon, Supabase, Upstash…), with env vars injected automatically.
Limits to watch: functions have a max duration (by plan); long jobs belong in queues or workflows; team collaboration is priced per seat; some advanced Next.js features (ISR, image optimization) are most complete on Vercel, so verify each one when moving elsewhere.
The Cloudflare path Use
The central concept of Cloudflare Workers is the binding: you declare in config which resources you need (database, KV, buckets, queues…), and the runtime injects them into your code through the env object. No connection strings, no SDK keys.
Cloudflare's storage and compute primitives
| Product | What it is | LinkNote use |
|---|---|---|
| D1 | Managed SQLite | Bookmarks, user data |
| KV | Globally distributed key-value store, read-heavy, eventually consistent | Cached page titles, config |
| R2 | Object storage, S3-compatible, no egress fees | Page snapshots, screenshots |
| Durable Objects | One globally unique, single-threaded object per ID, with its own storage | One agent instance per user (Chapter 12); realtime collaboration |
| Queues / Workflows | Message queue / durable multi-step workflows | Summary jobs |
| Workers AI / AI Gateway / Vectorize | Edge inference / LLM gateway (caching, rate limits, logs) / vector database | Embeddings, semantic search |
| Hyperdrive | Connection pooling and query caching for external Postgres | If you choose Postgres instead of D1 |
Durable Objects deserve a word, being Cloudflare-specific and the hardest primitive to grasp. They solve serverless's fundamental puzzle — "where does state live?": you get an object by an ID (a user ID, a chat room ID), and every request anywhere in the world for that ID is routed to the same instance. It processes one request at a time (no locks needed), has its own durable storage, and can hold WebSocket connections and set alarms. Cloudflare's Agents SDK is built on it.
Deployment: pnpm wrangler login → create resources with wrangler d1 create etc. → wrangler deploy; or connect a Git repo in the dashboard for automatic deploys with preview URLs. Locally, Cloudflare's Vite plugin runs your code in the real workerd runtime and simulates D1, KV and other bindings. Set secrets with wrangler secret put ANTHROPIC_API_KEY.
The self-hosted path Use
If you have ops experience, this path is the most familiar: build a Docker image, run it on a VPS, put a reverse proxy in front for HTTPS.
Tools that make self-hosting painless: Coolify and Dokploy (open-source self-hosted PaaS with Vercel-like Git deploys, preview environments, one-click databases); Kamal (zero-downtime Docker deploys to any server). A common setup adds Cloudflare's CDN in front for caching and DDoS protection.
What you own: OS and dependency security updates, database backups (and restore drills!), monitoring and alerting, log collection, disk space, scaling. That's the price of "predictable cost, full control".
How to choose
- Business logic in plain TS modules; platform-specific code (bindings, Server Actions) only in the adapter layer.
- Prefer Web-standard APIs (fetch, Request, Response, crypto) and portable storage (Postgres, S3-compatible object storage).
- Treat platform-specific features (Durable Objects, ISR) as "deliberate lock-in": when you use one, write down why.
- Vercel edition: Next.js + Neon Postgres + Vercel's queue / workflow features for summary jobs.
- Cloudflare edition: TanStack Start or Hono + D1 + Queues + one Durable Object per user hosting the "Ask my bookmarks" agent.
- Self-hosted edition: TanStack Start (Node) + Postgres + BullMQ/Redis, one-command Docker Compose, Caddy for HTTPS.
This chapter's vocabulary: long-running process / serverless / isolate, cold start, instance, binding, cron trigger, preview deployment, rollback, env var / secret, compatibility date, Durable Object, vendor lock-in.
- Pros and cons of serverless? Pros: auto-scaling, pay-per-use, no ops. Cons: cold starts, execution limits, statelessness requiring external storage, harder debugging and observability, vendor lock-in.
- What is edge computing good and bad for? Good for low-latency request handling, personalization, auth and caching; if the database lives in one region, edge functions making frequent round-trips are actually slower — compute should sit near data.
Self-check
1. To cache rarely-changing config, code declares let cache: Config | null = null at module top level and loads it from the DB on first request. Is that a bug on serverless?
Usually not — it's an acceptable optimization: each instance loads once on its first request and reuses it afterwards. The key is that it's a cache that may be lost or inconsistent, not the source of truth. Note that after a config change, instances refresh at different times; if changes must apply instantly, add expiry or use KV. Contrast the rate-limit counter in the opening — state that must be shared accurately — which is a bug in memory.
2. Your Worker runs in 300 cities, but Postgres is in US-East. A request makes 5 sequential queries. What happens for a user in Tokyo, and how could you improve it?
The Worker runs in Tokyo but every query crosses the Pacific (~150ms each), so 5 sequential ≈ 750ms — slower than deploying the whole service in US-East. Improvements: fewer round trips (combine queries, parallelize); Hyperdrive for pooling and query caching; Smart Placement so the Worker runs near the database; or move data closer to users (e.g. D1 read replicas).
3. Preview deployments connect to the production database. What's the risk?
Unreviewed PR code (possibly agent-written) operates directly on production data: one bad migration or delete path damages real data. Previews should use a separate database (or a branching database like Neon, creating a data branch per PR) and separate, least-privilege secrets.