Deploy Next.js on Ploy: push-to-deploy with a database built in

How to deploy a Next.js app on Ploy: connect the repo, add a ploy.yaml, get a preview URL for every branch, and add SQLite databases, queues, and cron jobs with @meetploy/nextjs.

Abstract indigo glow on a dark background representing a Next.js deployment on Ploy

Deploying a Next.js app is the easy part. The hard part comes a week later, when the app needs a database, a background job, and a nightly cron, and each one means another vendor, another dashboard, and another set of credentials in your environment variables.

Ploy is an open-source serverless cloud that treats those as part of the deployment. You deploy Next.js on Ploy by pushing to GitHub, and the same ploy.yaml that describes the build also declares your SQLite database, queues, workflows, and cron triggers. This guide walks through the whole thing.

Deploy Next.js on Ploy in three steps

1. Connect the repository

Sign in at meetploy.com, create a project, and connect the GitHub repository. If you would rather not use GitHub, create a Ploy Git repository instead and push to it with plain git push.

2. Add a ploy.yaml

Ploy auto-detects Next.js, but a ploy.yaml in the repository root makes the build explicit and is where bindings go later:

kind: dynamic
build: pnpm build
out: dist

Set output: "standalone" in next.config.ts so the build produces a self-contained server:

import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;

3. Push

Every push builds and deploys. The production branch gets your production URL; every other branch gets its own preview URL, so a pull request is reviewable at a link before it merges. Build logs stream in the dashboard, and any earlier deployment can be promoted back to production in one click if a release goes wrong.

That is the baseline. The rest of the post is what makes Ploy different from plain Next.js hosting.

Add a database, queues, and cron from ploy.yaml

Install the integration package and the CLI:

pnpm add @meetploy/cli @meetploy/nextjs

Then declare what the app needs:

kind: dynamic
build: pnpm build
out: dist
db:
DB: default
queue:
TASKS: tasks
cron:
NIGHTLY_CLEANUP: "0 3 * * *"

Run pnpm ploy types to generate an env.d.ts, and the bindings are typed everywhere you use them.

Query SQLite from a route handler

getPloyContext() exposes the bindings in Server Components, route handlers, and Server Actions:

import { NextResponse } from "next/server";
import { getPloyContext } from "@meetploy/nextjs";
interface User {
id: number;
name: string;
}
export async function GET() {
const { env } = getPloyContext();
const { results } = await env.DB.prepare("SELECT * FROM users").all<User>();
return NextResponse.json({ users: results });
}

Schema changes live in migrations/ as plain .sql files. Ploy applies pending migrations on production deploys and ploy dev applies them locally on startup, so there is no separate migration step to remember. See Next.js databases for the full query API.

Send a job to a queue

const { env } = getPloyContext();
const { messageId } = await env.TASKS.send({ action: "send-welcome", userId });

Handle queue messages and cron triggers

Background handlers live in app/ploy.ts. Ploy bundles this module with your app and routes queue deliveries and cron ticks to it:

export default {
async message(event, env) {
const payload = event.payload as { action: string; userId: string };
console.log("Processing", payload.action, "attempt", event.attempt);
},
async scheduled(event, env) {
await env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?")
.bind(Date.now())
.run();
},
} satisfies Ploy;

Messages that throw are retried with exponential backoff. The scheduled handler gets the same env as your routes, so it can read the database, send to queues, and call the AI gateway.

Run it locally with ploy dev

Point your dev script at ploy dev and add initPloyForDev() to next.config.ts:

import type { NextConfig } from "next";
import { initPloyForDev } from "@meetploy/nextjs";
if (process.env.NODE_ENV === "development") {
await initPloyForDev();
}
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;

pnpm dev now starts Next.js, a local emulator for databases, queues, workflows, and cron, and a dashboard at http://localhost:4000 where you can browse tables, inspect queue messages, and fire a cron trigger by hand. Environment variables come from .env locally and from the project settings in production, read with process.env as usual.

What you get that plain hosting does not

  • Preview deployments for every branch, with no configuration.
  • SQLite per project, with migrations applied on deploy.
  • Queues, durable workflows, and cron declared next to the build command, handled in app/ploy.ts.
  • Skew protection on by default, so clients mid-session keep talking to the deployment that served their page instead of hitting ChunkLoadError after a release.
  • An AI gateway (ai: true in ploy.yaml) that registers itself as the Vercel AI SDK default provider, so streamText({ model: "anthropic/claude-sonnet-5" }) works without provider keys.
  • Self-hosting, free, if you want all of this on your own servers.

Frequently Asked Questions

Does Ploy support the Next.js App Router and Server Components?

Yes. getPloyContext() works in Server Components, route handlers, and Server Actions. Client components keep using fetch against your routes as before.

Do I need to change how I read environment variables?

No. Use process.env as in any Next.js app. Values come from .env in development and from the dashboard in production; there is no env section to maintain in ploy.yaml.

What does it cost?

The hosted Free plan includes up to 3 repositories and 1 concurrent deployment. Pro is $10 per month and adds unlimited repositories, custom domains, and higher concurrency. Self-hosting is free with no usage limits.

Can I keep my existing Postgres?

Yes. Bindings are optional. Connect to any external database from your routes as you do today, and add a db binding only when you want a zero-setup SQLite database alongside it.

Get started