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 initAuto-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:
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 deployYour 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.comA 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:
{
"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:
{
"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 --prodIf 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/sdkimport { 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)
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:
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
const nextConfig: NextConfig = {
output: "standalone",
images: {
remotePatterns: [
{ protocol: "https", hostname: "s3.ghayma.tech" },
],
},
};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.comAdd 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.