# Deploy Next.js Applications | Volcano

URL: https://volcano.dev/solutions/nextjs-apps

> Deploy Next.js 15 and 16 on Volcano: global CDN with TLS and IPv6, builds on Node 22 or 24, a failed build never replacing live traffic, and Postgres, auth and storage in the same project.

## Your Next.js app and its backend, one project

Deploy the app to a global CDN and give it a database, sign-in and storage that already understand each other.

```console
$ volcano cloud frontends deploy --name web --path . --variable-scope all
✓ Frontend 'web' deployment started
$ volcano cloud frontends logs web --type build
next 16.1.2 · pnpm 9 · node 22 · 41 routes · 12 prerendered
$ volcano cloud frontends get web
Status  active   https://<frontend-id>.frontends.volcano.run
# a failed build leaves the running deployment untouched
```

## The app is the easy half

Getting Next.js onto a CDN is a solved problem. What takes the week is everything behind it: a database somewhere else, a connection string you must keep out of the browser, an auth provider whose tokens your database has never heard of, and three dashboards to keep in agreement.

Volcano puts the frontend and its backend in one project. The app deploys to a global edge, and the session it issues is the same identity your Postgres policies read, so a server component and a browser component can query the same tables safely.

## A bad build never takes the site down

*Deploys*

A deployment only takes over live traffic once it has built successfully. A failure leaves the current version serving, and redeploy ships again from what you already uploaded, without another push.

- Failed builds never replace what is running
- A newer push replaces one still waiting to build
- redeploy reuses what you already uploaded

```console
web   active       next 16.1.2   2m ago
  └─  failed       build error   40s ago
# the failure did not touch the active deployment
$ volcano cloud frontends logs web --type build
still serving 16.1.2
```

## One session, server and browser

*Data*

The token your app issues is understood by Postgres as auth.uid(), so a server component and a client component can hit the same tables under the same policies. There is no API layer to write in between.

- Middleware helpers for the App Router
- Row-level security instead of hand-written endpoints
- Anon key in the browser, service key on the server

Image: A Next.js storefront deployed on Volcano.

## It reads your repo, not a config UI

*Build*

The runtime comes from engines.node, and the package manager from the packageManager field or, without one, your lockfile. A monorepo works by packaging the workspace root and naming the member you want built.

- Node 22 or 24, from engines.node
- npm, pnpm or Yarn, at the version you declare
- --app-root apps/web for a workspace member

```console
$ volcano cloud frontends deploy --name web \
$   --path . --app-root apps/web
workspace  pnpm 9 · 4 packages
building   apps/web
✓ active
```

## How it works

1. **Start from a template**: volcano init nextjs gives you an app wired to the SDK, with migrations and functions alongside.
2. **Describe your data**: SQL migrations with row-level policies, so the browser can query without an endpoint per table.
3. **Add sign-in**: Email and password or OAuth, with middleware protecting the routes that need a session.
4. **Deploy, add a domain**: Ship the frontend, then attach your own domain and certificate to it.

## From template to custom domain

### Client

```typescript
import { VolcanoAuth } from '@volcano.dev/sdk';

export const volcano = new VolcanoAuth({
  apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
  anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
});

// Select the database once, before the first query.
volcano.database('app');

// Nothing here throws — every call resolves to { data, error }.
export async function recentPosts(limit = 20) {
  const { data, error } = await volcano
    .from('posts')
    .select('id, title, published_at')
    .eq('status', 'published')
    .order('published_at', { ascending: false })
    .limit(limit);

  if (error) {
    console.error('posts query failed', error);
    return [];
  }

  return data;
}
```

### Middleware

```typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createServerClient, withAuth } from '@volcano.dev/sdk/next/middleware';

const server = createServerClient({
  anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
  apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL,
});

export async function middleware(request: NextRequest) {
  // Reads the token from the request and validates it. Returns the user,
  // or null when it is missing, expired or revoked.
  const user = await withAuth(request, server);

  if (!user) {
    const login = new URL('/login', request.url);
    login.searchParams.set('next', request.nextUrl.pathname);
    return NextResponse.redirect(login);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*'],
};
```

### Policies

```sql
create table if not exists posts (
  id           uuid primary key default gen_random_uuid(),
  author_id    uuid not null,
  title        text not null,
  body         text not null,
  status       text not null default 'draft' check (status in ('draft', 'published')),
  published_at timestamptz
);

alter table posts enable row level security;

-- Anyone, signed in or not, can read what is published.
drop policy if exists read_published on posts;
create policy read_published on posts
  for select to anon, authenticated
  using (status = 'published');

-- Authors see and edit their own drafts.
drop policy if exists author_reads_own on posts;
create policy author_reads_own on posts
  for select to authenticated
  using (author_id = auth.uid());

drop policy if exists author_writes_own on posts;
create policy author_writes_own on posts
  for all to authenticated
  using (author_id = auth.uid())
  with check (author_id = auth.uid());
```

### Deploy

```bash
volcano init nextjs
volcano use my-app

volcano cloud databases migration up --all -d app
volcano cloud variables deploy

# Ship the app with the project variables it needs, for the build and the
# server runtime. Add --app-root if it lives in a monorepo.
volcano cloud frontends deploy --name web --path . --variable-scope all

# Add a custom domain with a certificate you supply
volcano cloud frontends domain create web \
  --domain app.example.com \
  --cert cert.pem --key key.pem --chain chain.pem

volcano cloud frontends logs web --type runtime --follow
```

## What one project saves

- **1 identity across the whole app**: The session your app issues is what the database reads in every policy.
- **0 CRUD endpoints per table**: Row-level security lets the client query directly instead of proxying through routes.
- **60 min allowed for a single build**: Long enough for a large monorepo build, with the old deployment serving throughout.

## Outcomes

- **Safe deploys**: A failed build leaves the running version serving traffic.
- **Less glue**: Auth, data and hosting are configured in one place.
- **Fast everywhere**: Static assets on a global CDN with TLS and IPv6.

## Built on Volcano

- [The hosting itself](https://volcano.dev/features/nextjs-hosting): Next.js 15 and 16, a global CDN, image optimization for next/image, and custom domains with TLS.
- [Your data](https://volcano.dev/features/postgres): PostgreSQL 16 through 18 with row-level security, queryable from a server component or the browser.
- [Sign-in](https://volcano.dev/features/authentication): Email and password, four OAuth providers, and middleware helpers for the App Router.
- [Uploads](https://volcano.dev/features/file-storage): Buckets for avatars and attachments, private by default with policy-based access.

## Frequently asked questions

### Which Next.js versions are supported?

15.x and 16.x. Volcano builds on Node 22 or 24, taken from engines.node in your package.json and defaulting to 22.

### Can I deploy a Vite or static site?

Not today. Frontend hosting accepts Next.js only. Anything else would need to run as a function or be hosted elsewhere against the same Volcano backend.

### Where does work too slow for a route handler go?

Into a durable function. The route handler starts it with volcano.durable.start and returns straight away, and the function runs each stage as a checkpointed step for as long as it needs, retrying what fails. For a monorepo, package the workspace root with --path . and name the app with --app-root apps/web.

### What happens when a build fails?

The deployment is marked failed and the currently active one keeps serving. If you push again while a deploy is still waiting to build, the newer one replaces it.

### How do custom domains work?

Volcano issues the certificate, or you supply it as cert and key PEM files; then point a CNAME at the target the CLI prints. A frontend takes as many custom domains as your plan allows, and one can redirect to another. The Volcano hostname gets TLS automatically.

### Are there preview deployments per branch?

No. There is one active deployment per frontend. To preview a branch, deploy it as a separate frontend or to a separate staging project, and give it a database branch's connection string so it runs against a copy of your data.

## Get started

Ready to deploy your app? Create a free account at https://volcano.dev/signup, or have your coding agent follow the setup playbook at https://volcano.dev/setup.md.
