Skip to Content

Deploy Next.js

Deploy a Next.js application to Ghayma.

Prerequisites

  • Next.js 13+ (App Router or Pages Router)
  • Node.js 18+
  • Ghayma CLI installed

1. Initialize

cd my-nextjs-app ghayma init

Auto-detected .ghayma.json:

{ "name": "my-nextjs-app", "framework": "nextjs", "build_command": "npm run build", "start_command": "npm start", "port": 3000 }

2. Configure for Standalone Output

For optimal performance, enable standalone output in next.config.ts:

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

This creates a minimal production build without unnecessary node_modules.

3. Deploy

ghayma deploy

Your app will be live at my-nextjs-app.ghayma.app.

Environment Variables

Set environment variables before or after deploy:

ghayma env set SMTP_URL=smtps://... NEXT_PUBLIC_API_URL=https://api.example.com

A database created on Ghayma needs nothing here: its DATABASE_URL is derived for every site granted access (see injected variables). Set DATABASE_URL yourself only for a database hosted elsewhere.

Variables prefixed with NEXT_PUBLIC_ are embedded at build time. Set them before deploying.

An auth app needs no variables of its own. Every site connected to one is handed GHAYMA_AUTH_APP_ID, GHAYMA_AUTH_URL, GHAYMA_AUTH_PUBLIC_URL and GHAYMA_AUTH_SERVER_KEY — plus NEXT_PUBLIC_GHAYMA_AUTH_APP_ID, derived as a build argument too, so next build inlines it into the browser bundle. See Automatically injected variables.

Internal Networking: If your Next.js app connects to Ghayma services (Auth, databases, storage), you must use internal URLs for the deployed version. External *.ghayma.tech URLs will timeout from inside the cluster. See the Networking Guide for details.

Prisma and database migrations

prisma migrate deploy can run in three places, and all three work.

During the build

The least setup. A site connected to its database receives DATABASE_URL while it builds — as a build secret, never written into the image — so a prebuild script needs nothing else:

package.json
{ "scripts": { "prebuild": "prisma migrate deploy", "build": "next build" } }

With a custom Dockerfile, mount the variable on the step that migrates instead. Both are covered in Using your database during the build. There is nothing to set: a connected database needs no build-time DATABASE_URL of your own.

A migration a build applies stays applied even when the deploy then fails and the previous version keeps serving, so keep each migration compatible with the code that is live — add the new column in one deploy, drop the old one in a later one.

At container start

If you control your app’s start command, run the migration there. It uses the DATABASE_URL the platform injects at runtime:

package.json
{ "scripts": { "start": "prisma migrate deploy && node server.js" } }

Next.js apps built by the platform start with a fixed node server.js, and the standalone image they run carries neither the Prisma CLI nor your prisma/ directory — so for Next.js this route needs a custom Dockerfile that keeps both and sets its own CMD. Other frameworks use the start command from your build settings.

As a one-off, from your machine

Migrate before you deploy, and the build does nothing but build. ghayma connect --local tunnels the site’s databases to your machine and writes a .env.local whose DATABASE_URL points at the tunnel; run the migration with that value in its environment, then deploy:

npx prisma migrate deploy # DATABASE_URL = the tunnel's, from .env.local ghayma deploy --prod

If a migration waits for a lock

Error: P1002 The database server at ... was reached but timed out. Timed out trying to acquire a postgres advisory lock (SELECT pg_advisory_lock(...)).

Prisma Migrate takes a lock so that two migrations never run at once, and gives up after ten seconds. Something else was migrating the same database at that moment — another build of a site connected to it, or a container migrating at start. Deploy again once it has finished; if it keeps happening, let only one of them migrate. Setting PRISMA_SCHEMA_DISABLE_ADVISORY_LOCK=1 as a build-time variable stops Prisma waiting for the lock — safe only when nothing else can migrate that database at the same time.

Using the SDK

Install the Ghayma SDK for storage, auth, and database access:

npm install @ghayma/sdk
lib/ghayma.ts
import { Ghayma } from "@ghayma/sdk"; export const ghayma = new Ghayma({ apiKey: process.env.GHAYMA_API_KEY });

A site connected to a storage bucket or a database already has GHAYMA_API_KEY in its environment: the platform mints a managed key for it and injects it on every deploy, so new Ghayma() works with no argument at all. For auth-app user administration, create an Application key in Project → Settings → API keys and set the variable yourself — see Project API Keys.

Storage (Server Components / API Routes)

app/api/upload/route.ts
import { ghayma } from "@/lib/ghayma"; export async function POST(req: Request) { const formData = await req.formData(); const file = formData.get("file") as File; await ghayma.storage.upload("bucket-id", `uploads/${file.name}`, file); return Response.json({ success: true }); }

Auth (Protected Route)

Verify the user’s JWT locally against your auth app’s JWKS — the Server SDK does not verify tokens. Create lib/auth.ts from JWT Verification, then:

app/api/protected/route.ts
import { verifyAccessToken } from "@/lib/auth"; export async function GET(req: Request) { const token = req.headers.get("authorization")?.replace("Bearer ", ""); if (!token) return Response.json({ error: "Unauthorized" }, { status: 401 }); try { const claims = await verifyAccessToken(token); return Response.json({ user: { id: claims.sub, email: claims.email } }); } catch { return Response.json({ error: "Invalid token" }, { status: 401 }); } }

To protect whole route trees instead of a single handler, use the Next.js middleware example.

Database

import { ghayma } from "@/lib/ghayma"; import { Pool } from "pg"; const conn = await ghayma.database.getConnection("db-id"); const pool = new Pool({ connectionString: conn.url });

Serving Images with Presigned URLs

next.config.ts
const nextConfig: NextConfig = { output: "standalone", images: { remotePatterns: [ { protocol: "https", hostname: "s3.ghayma.tech" }, ], }, };
app/api/images/route.ts
import { ghayma } from "@/lib/ghayma"; export async function GET() { const result = await ghayma.storage.listObjects("bucket-id", { prefix: "images/" }); const images = await Promise.all( result.objects .filter((obj) => !obj.is_folder) .map(async (obj) => ({ key: obj.key, url: (await ghayma.storage.getDownloadUrl("bucket-id", obj.key)).url, })) ); return Response.json({ images }); // Use directly: <Image src={image.url} /> }

Custom Domain

ghayma domain create my-app.com

Add an A record pointing my-app.com at the IPv4 address shown in your Dashboard (Project → Domains → Add domain). SSL is automatic.

Use a single A record and no AAAA (IPv6) record — Ghayma is IPv4-only, and a stray AAAA from a previous host blocks the certificate. See Domains.