Ploy
Ploy

Next.js

Deploy Next.js applications with Ploy's serverless runtime.

Next.js

Ploy provides first-class support for Next.js applications with access to Cloudflare-compatible D1 and KV bindings, plus queues and workflows, through a simple integration package.

Installation

Install the Ploy CLI and Next.js integration:

pnpm add @meetploy/cli @meetploy/nextjs

Add a dev script to your package.json:

package.json
{
	"scripts": {
		"dev": "ploy dev"
	}
}

Configuration

Create a ploy.yaml file in your project root:

ploy.yaml
kind: dynamic
build: pnpm build
out: dist

Update your next.config.ts to initialize Ploy bindings in development:

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;

The initPloyForDev() function reads your ploy.yaml and sets up mock bindings that proxy to the local Ploy emulator.

Build-time page cache

By default, Ploy's generated Next.js adapter disables persistent page caching. For apps that only need build-time prerenders, opt into a read-only cache:

ploy.yaml
kind: nextjs
nextjs:
  cache: static

This serves eligible prerendered HTML and React Server Component payloads from that deployment's assets without rerendering the page. Middleware, redirects, and dynamic routes still run through the Next.js adapter. This is not a public HTTP response cache: authenticated or cookie-dependent pages must remain dynamic. Ploy's response instrumentation still applies.

The cache is read-only until the next deployment. Do not enable it for apps that need ISR, on-demand revalidation (revalidatePath / revalidateTag), mutable cached fetches, or runtime cache writes. Builds reject timed page revalidation; other runtime revalidation APIs are not supported by this cache. No writable cache, queue, or additional bindings are provisioned.

Ploy generates open-next.config.ts during deployment; edits to that generated file are overwritten. Configure caching in ploy.yaml, without installing an OpenNext adapter in your app. Omit nextjs.cache or set it to disabled to retain the default behavior. ploy dev continues to use Next.js's development server; validate production caching with a deployment preview.

Usage

Use getPloyContext() to access Ploy bindings in Server Components or API routes:

app/api/example/route.ts
import { NextResponse } from "next/server";
import { getPloyContext } from "@meetploy/nextjs";

export async function GET() {
	const { env } = getPloyContext();

	// Access your bindings through env
	const result = await env.DB.prepare("SELECT * FROM users").all();

	return NextResponse.json({ users: result.results });
}

Background Handlers

Queue messages, workflow steps, and cron triggers are handled by a handlers module — app/ploy.ts, or worker/index.ts if you prefer keeping it out of the app directory:

app/ploy.ts
export default {
	async message(event, env, ctx) {
		// Queue messages
	},
	async scheduled(event, env, ctx) {
		// Cron triggers
	},
} satisfies Ploy;

In production, Ploy bundles that module alongside your app and routes deliveries to it. Locally, they arrive over HTTP instead, so the app needs a route that re-exports the handlers:

app/ploy-handler/[[...path]]/route.ts
export { GET, POST } from "@meetploy/nextjs/handler";

initPloyForDev() writes this route on first run when it is missing, so there is normally nothing to add. The file is a one-line re-export and is safe to commit.

Local Development

Run your Next.js app with Ploy:

pnpm dev

This starts:

  • Your Next.js app
  • The Ploy emulator for databases, queues, and workflows
  • A local dashboard at http://localhost:4000 for insights

The local dashboard lets you inspect database contents, view queue messages, and monitor workflow executions.

Environment Variables

Read them with process.env, exactly as in any Next.js app:

app/page.tsx
export default function Page() {
	return <p>{process.env.APP_NAME}</p>;
}

Values come from a .env file locally and from the Ploy dashboard in production. There is nothing to declare — ploy.yaml has no env section — and getPloyContext() is only needed for bindings such as databases and queues.

The same values are on env.vars if you prefer reading them from the context rather than a global.

See Environment Variables for the full details.

Type Safety

Generate TypeScript types from your ploy.yaml:

pnpm ploy types

This creates an env.d.ts file with type-safe bindings:

env.d.ts
import type { Database, StateBinding } from "@meetploy/types";

declare module "@meetploy/nextjs" {
	interface PloyEnv {
		DB: Database;
		STATE: StateBinding;
	}
}

Now getPloyContext() returns properly typed bindings:

const { env } = getPloyContext();
// env.DB is typed as Database
// env.STATE is typed as StateBinding (Cloudflare KV-compatible)

Next Steps

How is this guide?

Last updated on