RORK LABJP
RORK MAX — Rork Max can now build native Swift apps for iPhone, iPad, Apple Watch, Apple TV, and Vision ProPUBLISH — Rork Max offers two-click App Store publishing with no Xcode required, cutting the friction of getting an app shippedEXPO — The standard Rork is built on React Native (Expo), generating native iOS and Android apps from plain-English descriptionsPRICING — Rork is free to start, with paid plans beginning at $25/month, an accessible tier for solo developersFUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz) as investment keeps flowing into AI app buildersREVIEW — In real use the keys are generated-code readability and maintainability, Expo-related constraints, and how easily billing, push, and ad SDKs slot inRORK MAX — Rork Max can now build native Swift apps for iPhone, iPad, Apple Watch, Apple TV, and Vision ProPUBLISH — Rork Max offers two-click App Store publishing with no Xcode required, cutting the friction of getting an app shippedEXPO — The standard Rork is built on React Native (Expo), generating native iOS and Android apps from plain-English descriptionsPRICING — Rork is free to start, with paid plans beginning at $25/month, an accessible tier for solo developersFUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz) as investment keeps flowing into AI app buildersREVIEW — In real use the keys are generated-code readability and maintainability, Expo-related constraints, and how easily billing, push, and ad SDKs slot in
Articles/Dev Tools
Dev Tools/2026-06-15Advanced

Running a Neon + Drizzle Backend for Your Rork App in Production — Notes on Edge Connections, Zero-Downtime Migrations, and Type-Safe Queries

After wiring Neon Serverless Postgres and Drizzle ORM into a Rork app's backend, the friction shows up in production. These are implementation notes on choosing an edge connection model, migrating without locking tables, and designing type-safe queries that don't balloon into N+1.

rork57neondrizzle-ormcloudflare-workers2postgresserverlessbackend-operations

Premium Article

I built a Rork app's backend on Neon and Drizzle, and locally it felt great. A few days into production, one screen would occasionally spike in latency. The cause was mundane: from the edge I was sending each query as its own HTTP round trip, and that one screen fired several small queries in a row.

Local Postgres lives on the same machine, so the round trips are invisible. Move to the edge and the number of times you touch the database in a single request becomes your felt speed. There's plenty written about getting Neon and Drizzle to the "first working query." What matters in production is the decisions that come after. As an indie developer running several apps at once, the "after it works" handling is exactly where my time went. Here are the places I actually had to revisit.

Decide the connection model first — neon-http or the WebSocket Pool

@neondatabase/serverless gives you two doors: the HTTP driver returned by neon(), and the WebSocket driver built on Pool. Most tutorials only show the first, but in production this is the decision to make up front.

The HTTP driver sends each query as a single fetch. It fits edge runtimes like Cloudflare Workers, where you can't hold a connection open, and it shrugs off cold starts. The trade-off: it can't run interactive transactions that group several statements into one atomic unit.

// src/db/http.ts — the path for single, standalone queries
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
 
export function createHttpDb(databaseUrl: string) {
  const sql = neon(databaseUrl);
  return drizzle(sql, { schema });
}

When you need a Stripe payment confirmation and a purchase-history insert to "both succeed or both fail," you open a transaction over the WebSocket Pool. Half-finished writes around payments are expensive to clean up, so this is the one place I never compromise on a transaction.

// src/db/pool.ts — the path for code that needs a transaction
import { drizzle } from "drizzle-orm/neon-serverless";
import { Pool } from "@neondatabase/serverless";
import * as schema from "./schema";
 
export function createPoolDb(databaseUrl: string) {
  const pool = new Pool({ connectionString: databaseUrl });
  return drizzle(pool, { schema });
}

In my case I recommend splitting into two paths: reads and single writes over HTTP, and only the endpoints that must keep several rows consistent over the Pool. Route everything through the Pool and the WebSocket handshake makes the first call slower; route everything through HTTP and you end up reimplementing consistency outside the ORM. The deciding question is: does this endpoint need multiple writes to land as a single result? If not, HTTP stays lighter and faster — that's what the measurements showed.

Geography matters too. If the Neon region and your main users are far apart, every HTTP round trip carries a fixed delay. If your users are concentrated in one region, put Neon near them. A fast edge doesn't shorten the physical distance to the database.

Count how many times one request touches the database

The spiky latency almost always traces back to queries per screen. Because Drizzle's query builder reads so plainly, it's tempting to fetch a list and then pull related rows inside a loop. That's N+1.

// Anti-pattern: fetch the author N times for N posts
const posts = await db.select().from(postsTable).limit(20);
for (const p of posts) {
  // this one line becomes 20 round trips
  p.author = await db.select().from(users).where(eq(users.id, p.userId));
}

At the edge, those 20 round trips turn into hundreds of milliseconds of felt delay. In my measurements, collapsing that same screen into a single join made responses roughly 3x faster. Drizzle's relational queries let you pull the joins you need in one shot.

// Better: declare the relation and fetch it once
const posts = await db.query.postsTable.findMany({
  limit: 20,
  orderBy: (p, { desc }) => [desc(p.createdAt)],
  with: {
    author: {
      columns: { id: true, displayName: true, avatarUrl: true },
    },
  },
});

Narrowing columns here isn't cosmetic. Pulling the full body and internal flags you never render adds up in transfer size and serialization cost. Edge functions cap CPU time, so trimming returned columns to what you actually use pays off.

Aggregates are the same. For a value where you only want the number — a post count, say — don't fetch all the rows and read length; let the database count with $count.

const postCount = await db.$count(postsTable, eq(postsTable.userId, userId));

What helped most in practice was making query counts visible during development. Turn on Drizzle's logger and print queries-per-request, and every time you build a screen you can eyeball "did this list quietly become N+1?" That's far cheaper than discovering it in production.

const db = drizzle(sql, { schema, logger: env.ENVIRONMENT !== "production" });

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A concrete rule for choosing between neon-http and the WebSocket Pool, based on transaction needs and geographic latency
An expand / contract procedure for changing columns without locking live tables, and how to land it in Drizzle Kit
How to fetch related data without N+1, and where to place query-count observability in a scale-to-zero environment
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-06-09
Deep Links in Rork Max — Universal Links and URL Schemes
A hands-on guide to deep linking in Rork Max apps: when to use URL Schemes vs. Universal Links, the AASA/assetlinks pitfalls, and the cold-start trap — with working examples.
Dev Tools2026-05-25
Implementation Notes: Adding StoreKit 2 In-App Purchases to a Rork iOS App
Notes from grafting StoreKit 2 in-app purchases onto Swift/SwiftUI code generated by Rork, drawing on the StoreKit 1-to-2 migration done across a 50M-cumulative-download wallpaper-app portfolio. Covers ProductID design, transaction verification, paywall UI, and production gotchas.
Dev Tools2026-05-24
Why your Rork app shows a blank screen or loses state after returning from background
Your Rork app sits in the background overnight, you tap the icon the next morning, and the screen is blank — or your drafts have disappeared, or you have been silently logged out. iOS process reclamation, AsyncStorage rehydration timing, and Navigation state restoration each cause a different version of this bug. Notes from running wallpaper and wellness apps with 50M downloads as an indie developer.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →