Back to Blog
Tutorial10 min read2026-07-04

How to Deploy a SvelteKit App with a Database

A complete walkthrough for shipping a SvelteKit app with a PostgreSQL database to production, covering the right adapter, environment wiring, migrations, and avoiding the classic cold-start gotchas.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

SvelteKit is a joy to develop in, but "it runs with npm run dev" and "it runs in production behind a real database" are two different problems. This guide walks through deploying a SvelteKit app with PostgreSQL the way I would for an actual product.

Pick the right adapter

SvelteKit compiles through an *adapter* that targets a specific runtime. The choice matters more than people expect.

AdapterOutputBest for
adapter-nodeA Node server (node build)Container/VM deploys, full server features
adapter-staticPure static filesNo server-side data, fully prerendered
adapter-autoDetects common hostsConvenience, less control

For an app with a database you want a long-running server, so use adapter-node:

npm install -D @sveltejs/adapter-node
// svelte.config.js
import adapter from '@sveltejs/adapter-node';

export default {
  kit: {
    adapter: adapter()
  }
};

Now npm run build produces a build/ directory you start with node build. That is a standard Node process any container platform can run.

Set up the database layer

I will use [Drizzle ORM](https://orm.drizzle.team/) with postgres-js because it is lightweight and TypeScript-native, but Prisma works the same way conceptually.

npm install drizzle-orm postgres
npm install -D drizzle-kit

Define a schema:

// src/lib/server/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  body: text('body').notNull(),
  createdAt: timestamp('created_at').defaultNow()
});

Create a single shared client. Note the src/lib/server/ path — SvelteKit guarantees anything under server/ never leaks into the browser bundle, which is exactly where your database connection belongs.

// src/lib/server/db.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { env } from '$env/dynamic/private';

const client = postgres(env.DATABASE_URL, { max: 10 });
export const db = drizzle(client);

Using $env/dynamic/private (rather than $env/static) means the URL is read at runtime, so the same build works across environments.

Load data on the server

SvelteKit's +page.server.ts runs only on the server — perfect for queries:

// src/routes/+page.server.ts
import { db } from '$lib/server/db';
import { posts } from '$lib/server/schema';

export async function load() {
  const rows = await db.select().from(posts);
  return { posts: rows };
}

Migrations, not auto-sync

Never let an ORM auto-create tables in production. Generate explicit migrations and apply them as a deliberate step.

npx drizzle-kit generate

This writes versioned SQL into a drizzle/ folder. Apply them with a small script:

// scripts/migrate.ts
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

const client = postgres(process.env.DATABASE_URL, { max: 1 });
await migrate(drizzle(client), { migrationsFolder: './drizzle' });
await client.end();

Run migrations before the new version serves traffic. Treat schema changes as backward-compatible deploys: add columns before you require them.

Deploying

With adapter-node, the contract is simple: build, then run node build with DATABASE_URL and PORT set. SvelteKit reads PORT and HOST automatically.

On PandaStack, the whole thing is git-push:

  1. 1Provision a managed PostgreSQL instance from the dashboard. PandaStack injects DATABASE_URL into your service automatically — no copy-paste.
  2. 2Connect your Git repo. The platform auto-detects SvelteKit, runs the install (npm/pnpm/yarn/bun, overridable), builds, and starts node build.
  3. 3Add a custom domain; SSL is provisioned automatically.

Because the build runs in an isolated environment and the image is deployed via Helm, you get rollbacks and deploy history for free. If a migration goes wrong you roll back the app version while you sort out the schema.

Health checks and graceful shutdown

Production platforms restart unhealthy containers, so handle SIGTERM to close your DB pool cleanly. The Node adapter forwards the signal:

// hooks: close pool on shutdown
process.on('sigterm', async () => {
  await client.end();
  process.exit(0);
});

The cold-start gotcha

If you run on scale-to-zero infrastructure (great for hobby projects and cost), the first request after idle pays a cold-start penalty. Two mitigations:

  • Keep a small connection pool (max: 5–10) so reconnect storms do not pile up.
  • For latency-sensitive endpoints, run on always-on compute instead of scale-to-zero.

PandaStack free-tier apps scale to zero on preemptible nodes — fine for side projects, but bump to a paid tier for anything user-facing where the first-byte latency matters.

Verifying it works

# Confirm the build output exists
npm run build && ls build/

# Smoke-test locally with a real DATABASE_URL
DATABASE_URL=postgres://... PORT=3000 node build
curl -s localhost:3000 | head

If that returns your rendered page with data, your production deploy will too.

References

  • [SvelteKit adapters documentation](https://svelte.dev/docs/kit/adapters)
  • [adapter-node reference](https://svelte.dev/docs/kit/adapter-node)
  • [Drizzle ORM with PostgreSQL](https://orm.drizzle.team/docs/get-started-postgresql)
  • [SvelteKit server-only modules](https://svelte.dev/docs/kit/server-only-modules)

---

Want to skip the infra plumbing? PandaStack auto-detects SvelteKit, builds it, and wires up a managed PostgreSQL with DATABASE_URL injected — free to start at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also