# Deploying a TanStack Start App with Neon Postgres and Cloudflare Workers

- **URL:** https://isaacfei.com/posts/deploy-tanstack-start-neon-cloudflare
- **Date:** 2026-02-23
- **Tags:** TanStack Start, Cloudflare Workers, Neon, Drizzle ORM, Deployment
- **Description:** A step-by-step guide to deploying a full-stack TanStack Start application with Neon serverless Postgres (via Drizzle ORM) to Cloudflare Workers, using GitHub integration for CI/CD.

---

This guide walks through deploying a full-stack [**TanStack Start**](https://tanstack.com/start) application to [**Cloudflare Workers**](https://workers.cloudflare.com/) with [**Neon**](https://neon.tech/) as the serverless Postgres database, using [**Drizzle ORM**](https://orm.drizzle.team/) for type-safe database access. We'll set up the database, configure the project for Cloudflare, and deploy via Cloudflare's GitHub integration so every push triggers a new deployment automatically.

## The Stack

| Layer | Technology |
|-------|-----------|
| Framework | TanStack Start (React, full-stack with SSR) |
| Database | Neon (serverless Postgres) |
| ORM | Drizzle ORM (with `neon-http` driver) |
| Hosting | Cloudflare Workers |
| CI/CD | Cloudflare GitHub integration |

Why this combination? TanStack Start runs on Cloudflare Workers as an official deployment target. Neon provides serverless Postgres that's accessible over HTTP — perfect for edge runtimes where traditional TCP-based Postgres connections aren't available. Drizzle ORM ties it together with type-safe queries and zero-overhead migrations.

## Prerequisites

- A [Neon](https://neon.tech/) account (free tier works)
- A [Cloudflare](https://www.cloudflare.com/) account (free Workers plan works)
- A GitHub repository with your TanStack Start project
- Node.js 18+ and pnpm installed

## Step 1: Set Up Neon Database

### Create a Neon Project

Head to the [Neon Console](https://console.neon.tech/) and create a new project (or use an existing one).

### Get the Connection String

In your project dashboard, click the **Connect** button. This opens the connection details modal showing your connection string.
![Neon "Connect" modal with connection string](./neon-database-url.png)

Copy the connection string. It looks like this:

```
postgresql://neondb_owner:abc123@ep-cool-darkness-123456-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require
```

Create a `.env.local` file in your project root and add the connection string:

```bash
DATABASE_URL="postgresql://neondb_owner:abc123@ep-cool-darkness-123456-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require"
```

This file is for local development only. We'll set this as a secret in Cloudflare later for production.

## Step 2: Set Up Drizzle ORM with Neon

### Install Dependencies

```bash
pnpm add drizzle-orm @neondatabase/serverless
pnpm add -D drizzle-kit dotenv tsx
```

- `drizzle-orm` — the ORM itself
- `@neondatabase/serverless` — Neon's serverless driver for HTTP-based Postgres access (works in edge runtimes)
- `drizzle-kit` — CLI for migrations and schema management
- `dotenv` — loads `.env.local` for local scripts
- `tsx` — runs TypeScript files directly (for seed scripts, etc.)

### Define Your Schema

Create your schema file. Here's an example with a `documents` table:

```ts
// src/server/db/schema/documents.ts
import { pgTable, text, uuid, timestamp } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";

export const documentsTable = pgTable("documents", {
  id: uuid("id").primaryKey().defaultRandom(),
  title: text("title"),
  content: text("content"),
  checksum: text("checksum"),
  createdAt: timestamp("created_at", { withTimezone: true })
    .notNull()
    .defaultNow(),
  updatedAt: timestamp("updated_at", { withTimezone: true })
    .notNull()
    .defaultNow()
    .$onUpdate(() => sql`now()`),
});
```

Export it from an index file:

```ts
// src/server/db/schema/index.ts
export * from "./documents";
```

### Create the Database Connection

Drizzle provides a dedicated `neon-http` driver integration. This uses HTTP to talk to Neon — no TCP sockets needed, which is exactly what Cloudflare Workers requires.

```ts
// src/server/db/db.ts
import { drizzle } from "drizzle-orm/neon-http";

export const db = drizzle(process.env.DATABASE_URL!);
```

That's it. Drizzle creates a Neon HTTP client under the hood using the connection string. You can now use `db` in your server functions.

### Configure Drizzle Kit

Create a `drizzle.config.ts` in the project root:

```ts
// drizzle.config.ts
import { config } from "dotenv";
import { defineConfig } from "drizzle-kit";

config({ path: ".env.local" });

export default defineConfig({
  out: "./drizzle",
  schema: "./src/server/db/schema",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
```

### Run Migrations

Add these scripts to your `package.json`:

```json
{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate"
  }
}
```

Generate and apply migrations:

```bash
pnpm db:generate
pnpm db:migrate
```

`db:generate` creates SQL migration files in the `./drizzle` folder based on your schema. `db:migrate` applies them to your Neon database. You can run `db:generate` whenever you change your schema and `db:migrate` to apply the changes.

## Step 3: Configure TanStack Start for Cloudflare Workers

Cloudflare Workers is an [official deployment target](https://tanstack.com/start/latest/docs/framework/react/guide/hosting#cloudflare-workers--official-partner) for TanStack Start. The setup requires three things: the Cloudflare Vite plugin, a Wrangler config, and updated scripts.

### Install Cloudflare Dependencies

```bash
pnpm add -D @cloudflare/vite-plugin wrangler
```

### Update `vite.config.ts`

Add the Cloudflare plugin to your Vite config. It must come **before** the TanStack Start plugin:

```ts
// vite.config.ts
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { cloudflare } from "@cloudflare/vite-plugin";
import viteReact from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [
    cloudflare({ viteEnvironment: { name: "ssr" } }),
    tanstackStart(),
    viteReact(),
  ],
});
```

The `viteEnvironment: { name: "ssr" }` tells the Cloudflare plugin to handle the SSR environment, which is where TanStack Start's server code runs.

### Add `wrangler.jsonc`

Create a `wrangler.jsonc` file in the project root:

```json
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "your-project-name",
  "compatibility_date": "2025-09-02",
  "compatibility_flags": ["nodejs_compat"],
  "main": "@tanstack/react-start/server-entry"
}
```

A few notes:

- Change `"name"` to your project name — this becomes your `*.workers.dev` subdomain
- `nodejs_compat` enables Node.js APIs in Workers (required for many npm packages)
- `main` points to TanStack Start's server entry point

### Update `package.json` Scripts

```json
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build && tsc --noEmit",
    "preview": "vite preview",
    "deploy": "pnpm run build && wrangler deploy"
  }
}
```

The key changes from a standard TanStack Start setup:

- Remove the `"start": "node .output/server/index.mjs"` script (Cloudflare Workers doesn't use Node)
- Add `"preview"` for local testing with Wrangler's Workers runtime
- Add `"deploy"` for manual deployment via CLI (optional if using GitHub integration)

## Step 4: Deploy to Cloudflare via GitHub Integration

Instead of deploying via `wrangler deploy` from the command line, we'll connect the GitHub repository directly to Cloudflare. This gives you automatic deployments on every push — no CI configuration needed.

### Connect Your Repository

1. Go to the [Cloudflare dashboard](https://dash.cloudflare.com/) and navigate to **Workers & Pages**.
2. Click **Create**, then select the **Import a repository** tab (or similar option to connect Git).
3. Connect your GitHub account if you haven't already. Authorize Cloudflare to access your repositories.
4. Select the repository containing your TanStack Start project.

### Configure Build Settings

After selecting your repository, configure the build settings:

| Setting | Value |
|---------|-------|
| **Production branch** | `main` (or your default branch) |
| **Build command** | `pnpm run build` |
| **Deploy command** | `npx wrangler deploy` |

Cloudflare will use these settings to build and deploy your app whenever you push to the production branch.

### Set Environment Variables

Your app needs the `DATABASE_URL` to connect to Neon. **Do not** commit this to your repository.

1. After creating the Worker (or in the Worker's **Settings** page), go to **Settings** → **Variables and Secrets**.
   ![Cloudflare Worker Settings page, Variables and Secrets section](./cloudflare-worker-settings-variables-and-secrets.png)

2. Click **Add**, choose type **Secret**, and set:

- **Variable name**: `DATABASE_URL`
- **Value**: your Neon connection string
  ![Adding the DATABASE\_URL secret](./cloudflare-worker-set-secrets.png)

3. Click **Save** (or **Deploy** if prompted — deploying saves the variables and triggers a new deployment).

Using **Secret** (instead of plain text) ensures the value is encrypted and never shown in the dashboard after saving.

### Trigger a Deployment

Once the GitHub integration is set up:

- **Automatic**: Push to your production branch. Cloudflare detects the push, runs the build command, and deploys.
- **Manual**: In the Cloudflare dashboard, go to your Worker and click **Deployments** → **Deploy** to trigger a build from the latest commit.

Your app is now live at `https://your-project-name.your-subdomain.workers.dev`. You can also add a custom domain in the Worker settings.

## Putting It All Together

Here's the complete file structure for the deployment-related configuration:

```
.
├── drizzle/                  # generated migration files
├── src/
│   └── server/
│       └── db/
│           ├── db.ts         # Drizzle + Neon connection
│           └── schema/
│               └── index.ts  # table definitions
├── .env.local                # DATABASE_URL (local dev only, git-ignored)
├── drizzle.config.ts         # Drizzle Kit config
├── vite.config.ts            # Vite + Cloudflare + TanStack Start plugins
├── wrangler.jsonc            # Cloudflare Workers config
└── package.json              # build/deploy scripts
```

The deployment flow looks like this:

```mermaid
flowchart TB
    Push["Git Push"] --> CF["Cloudflare<br/>Builds Worker"]
    CF --> Deploy["Deploy to<br/>Workers Edge"]
    Deploy --> App["Your App<br/>(SSR on Workers)"]
    App -->|"HTTP query"| Neon["Neon Postgres<br/>(serverless)"]
```

And the development workflow:

```mermaid
flowchart TB
    subgraph Local ["Local Development"]
        Dev["pnpm dev"] --> Vite["Vite Dev Server<br/>(port 3000)"]
        Vite --> App["TanStack Start App"]
        App -->|"DATABASE_URL<br/>from .env.local"| NeonDB["Neon Postgres"]
    end

    subgraph Schema ["Schema Changes"]
        Edit["Edit schema/*.ts"] --> Gen["pnpm db:generate"]
        Gen --> Migrate["pnpm db:migrate"]
        Migrate --> NeonDB
    end

    subgraph Deploy ["Deployment"]
        GitPush["git push"] --> CFBuild["Cloudflare Build"]
        CFBuild --> Worker["Cloudflare Worker"]
        Worker -->|"DATABASE_URL<br/>from Secrets"| NeonDB
    end
```

## Common Issues

**`Error: connect ECONNREFUSED` or TCP connection errors** — Make sure you're using the `neon-http` driver (`drizzle-orm/neon-http`), not the standard `pg` driver. Cloudflare Workers doesn't support TCP sockets; the Neon HTTP driver uses `fetch()` under the hood.

**`Missing environment variable DATABASE_URL`** — Check that you've added the variable in Cloudflare Worker settings as a **Secret**. For local development, make sure `.env.local` exists and contains the variable.

**Build fails with `wrangler` not found** — Ensure `wrangler` is in `devDependencies`, not `dependencies`. The Cloudflare build environment installs dev dependencies by default.

**`nodejs_compat` errors** — Make sure `"compatibility_flags": ["nodejs_compat"]` is in your `wrangler.jsonc`. Some npm packages (including Drizzle and crypto libraries) need Node.js APIs that are only available with this flag.

## Summary

The deployment setup is straightforward once you know the pieces:

1. **Neon** provides a serverless Postgres database accessible over HTTP — create a project, grab the connection string
2. **Drizzle ORM** with the `neon-http` driver gives you type-safe database access that works in edge runtimes
3. **Cloudflare Workers** runs your TanStack Start app at the edge — add the Vite plugin and Wrangler config
4. **GitHub integration** in Cloudflare gives you automatic CI/CD — connect your repo, set build commands, add secrets, and every push deploys

The total configuration is minimal: one Vite plugin, one Wrangler config file, one Drizzle config, and a 3-line database connection module. The rest is handled by the platforms.