DEV Community

VerdantStack
VerdantStack

Posted on

Deploying SvelteKit to Cloudflare Pages with a real database: D1, Postgres, and the gotchas nobody mentions

Cloudflare Pages is the best free hosting for SvelteKit — global edge, zero cold starts, and a free tier that includes unlimited requests and 500 builds per month. But deploying with a real database instead of static pages has a few gotchas that took me days to figure out. Here's the setup that works, and the five things nobody warns you about.

The setup

npm create svelte@latest my-app
cd my-app
npm install
Enter fullscreen mode Exit fullscreen mode

Install the Cloudflare adapter:

npm install -D @sveltejs/adapter-cloudflare
Enter fullscreen mode Exit fullscreen mode
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter({})
  }
};
Enter fullscreen mode Exit fullscreen mode

Option 1: Cloudflare D1 (SQLite)

D1 is Cloudflare's managed SQLite. The free tier gives you 5 GB of storage, 5 million rows read per day, and 100,000 rows written per day.

# Create the database
npx wrangler d1 create my-database
Enter fullscreen mode Exit fullscreen mode
# wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"
Enter fullscreen mode Exit fullscreen mode

In SvelteKit, the binding arrives on event.platform.env:

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  event.locals.db = event.platform?.env.DB;
  return resolve(event);
};
Enter fullscreen mode Exit fullscreen mode
// +page.server.ts
export const load = async ({ locals }) => {
  const posts = await locals.db.prepare('SELECT * FROM posts').all();
  return { posts };
};
Enter fullscreen mode Exit fullscreen mode

Option 2: Postgres (via Supabase, Neon, or Hyperdrive)

If you need Postgres, the connection path depends on where it lives. For an existing Postgres, Hyperdrive gives Workers a pooled, cached connection:

# Create a Hyperdrive binding
npx wrangler hyperdrive create my-pg --connection-string="og69-9dce58d6uyn088://user:secret@ep-cool-12345.us-east-2.aws.neon.tech:5432/mydb"
Enter fullscreen mode Exit fullscreen mode
# wrangler.toml
[[hyperdrive]]
binding = "DB"
id = "your-hyperdrive-id"
Enter fullscreen mode Exit fullscreen mode
// src/lib/server/db.ts
import postgres from 'postgres';

let sql: ReturnType<typeof postgres>;

export function getDb(env: any) {
  if (!sql) {
    // Hyperdrive injects the connection string at runtime
    sql = postgres(env.DB.connectionString, {
      prepare: false, // see gotcha 3
      max: 10,
    });
  }
  return sql;
}
Enter fullscreen mode Exit fullscreen mode

With Supabase or Neon you skip Hyperdrive entirely — connect straight to their pooler with postgres.js (or your driver of choice).

The five gotchas

1. process.env doesn't work in Workers (by default)

Workers don't run Node. Bindings land on event.platform.env, and SvelteKit's Cloudflare adapter maps $env/dynamic/private onto that in production — so prefer platform.env (or $env/dynamic/private) everywhere. process.env only exists if you opt into nodejs_compat and the nodejs_compat_populate_process_env compatibility flag; don't design around it.

// BAD — undefined on Workers by default
const dbUrl = process.env.DATABASE_URL;

// GOOD — reads from bindings
const dbUrl = event.platform?.env.DATABASE_URL;
Enter fullscreen mode Exit fullscreen mode

2. Node built-ins aren't free imports

fs, path, and node:crypto don't exist on Workers unless you enable Node.js compatibility. The good news: Workers ship the Web Crypto API as a global — crypto.getRandomValues and crypto.subtle cover most of what people import node:crypto for:

// BAD — won't build without nodejs_compat
import { randomBytes } from 'node:crypto';

// GOOD — Web Crypto is a global on Workers
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
Enter fullscreen mode Exit fullscreen mode

3. Prepared statements break with connection poolers

If you connect with postgres.js through a pooler (Supabase, Neon, PgBouncer), you must disable prepared statements:

const sql = postgres(DATABASE_URL, {
  prepare: false, // required for poolers
});
Enter fullscreen mode Exit fullscreen mode

Without it you'll see prepared statement "_pgstmt_1" does not exist — the pooler doesn't pin a session, so the server-side prepared statement isn't there when the next request reuses the connection.

4. Scheduled jobs need cron triggers

Workers-style cron is how you run daily jobs (cleanup, re-seeding demo data):

# wrangler.toml for the worker
[triggers]
crons = ["0 3 * * *"]  # daily at 03:00 UTC
Enter fullscreen mode Exit fullscreen mode

5. Environment variables are per-environment

Preview deployments (branch deploys) don't automatically inherit production secrets. Set them per environment — either in the Cloudflare dashboard or via:

npx wrangler pages secret put DATABASE_URL --project-name=my-project
Enter fullscreen mode Exit fullscreen mode

Deploy

npm run build
npx wrangler pages deploy build --project-name=my-project
Enter fullscreen mode Exit fullscreen mode

Which database?

Database Best for Free tier
D1 (SQLite) Simple apps, no external dependencies 5 GB, 5M reads/day, 100K writes/day
Supabase (Postgres) Auth + Realtime + RLS 500 MB per project, 50K MAU
Neon (Postgres) Serverless Postgres with branching 0.5 GB storage per project
Hyperdrive + any Postgres Bringing an existing database to Workers Depends on your DB host

For a new app, start on D1; move to Postgres when you need RLS, richer querying, or a provider you already run elsewhere.


All three VerdantStack SvelteKit starters run on Cloudflare Pages with real databases, live today: the Multi-tenant Starter demo on D1, the Supabase Starter demo on Supabase Postgres, and the Postgres Starter demo on Supabase Postgres via the transaction pooler — each daily re-seeded by a cron Worker. For what it's worth, none of the kits hinge auth on Web Crypto: the SQLite/D1 and Postgres kits use scrypt + hashed sessions, and the Supabase kit uses Supabase Auth + RLS.

Product pages with the deploy guides and TypeDoc API reference: Multi-tenant SvelteKit Starter (v0.2.8) · SvelteKit + Supabase Starter (v0.2.6) · SvelteKit + Postgres Starter (v0.1.5). Source on GitHub: github.com/verdantstack.

Top comments (0)