NEW

SurrealDB 4 is here, introducing SurrealDB Earth and SurrealDS!

Learn more

Connecting your Next.js app on Cloudflare to SurrealDB Earth

Engineering

Nov 13, 20265 min read

Tobie Morgan Hitchcock

Tobie Morgan Hitchcock

Show all postsConnecting your Next.js app on Cloudflare to SurrealDB Earth

Our newsletter

Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks

A Next.js app on Cloudflare Workers reaches SurrealDB Earth by opening one surrealdb SDK connection for each request and closing it when the request ends. Server Components, route handlers and server actions can all share one small helper that does this. This guide covers SurrealDB 4 and Earth, which launched on 3 November 2026, with the JavaScript SDK surrealdb 2.2.2.

  • A Next.js app deployed to Cloudflare with the OpenNext Cloudflare adapter, @opennextjs/cloudflare. The code below uses Next.js 16 and Wrangler 4.

  • An Earth instance. Create one from the Deploy page in SurrealDB Studio, or with surreal cloud instance deploy.

  • A database user for the app, which the next section defines.

The dashboard of an Earth instance in Studio shows its public endpoint, in the form wss://<instance-id>.surrealdb.earth/rpc. For HTTP, use the same host with https://. The SDK adds /rpc to a remote URL that does not already end with it, so https://<instance-id>.surrealdb.earth is all the app needs.

Give the app a database user of its own, rather than a root user. Run this in Studio's query view on the instance:

DEFINE USER app ON DATABASE PASSWORD 'hunter123' ROLES EDITOR;

Keep the endpoint and the credentials in Worker bindings. Plain variables hold the URL, namespace, database and user, and the password belongs in a secret. Locally, a .dev.vars file supplies the secret, and Wrangler reads it in preview.

The binding names, such as SURREAL_URL and SURREAL_USER, are this app's own choice and match the Cloudflare Workers example in the docs. Neither the SDK nor the surreal CLI reads them: the helper below passes each value to the SDK itself.

{
  "name": "app",
  "main": ".open-next/worker.js",
  "compatibility_date": "2026-11-01",
  "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
  "assets": { "directory": ".open-next/assets", "binding": "ASSETS" },
  "vars": {
    "SURREAL_URL": "https://<instance-id>.surrealdb.earth",
    "SURREAL_NS": "app",
    "SURREAL_DB": "app",
    "SURREAL_USER": "app"
  }
}

OpenNext exposes bindings through getCloudflareContext().env. Declare their types once in an env.d.ts file that extends CloudflareEnv:

interface CloudflareEnv {
  SURREAL_URL: string;
  SURREAL_NS: string;
  SURREAL_DB: string;
  SURREAL_USER: string;
  SURREAL_PASS: string;
}

Write one helper that opens a client, runs a callback and closes the client. Every Next.js entry point calls it, so the connection settings live in one place. A database user signs in with its namespace and database as well as its name and password:

// lib/db.ts
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { Surreal } from "surrealdb";

export async function withDb<T>(fn: (db: Surreal) => Promise<T>): Promise<T> {
  const { env } = getCloudflareContext();
  const db = new Surreal();
  await db.connect(env.SURREAL_URL, {
    namespace: env.SURREAL_NS,
    database: env.SURREAL_DB,
    authentication: {
      namespace: env.SURREAL_NS,
      database: env.SURREAL_DB,
      username: env.SURREAL_USER,
      password: env.SURREAL_PASS,
    },
    requestTimeout: 5_000,
  });
  try {
    return await fn(db);
  } finally {
    await db.close();
  }
}

A Server Component, a route handler and a server action then each call withDb. Mark the page force-dynamic so that Next.js reads the database on every request.

// app/page.tsx
import { Table } from "surrealdb";
import { withDb } from "../lib/db";
import { addPerson } from "./actions";

export const dynamic = "force-dynamic";

export default async function Page() {
  const people = await withDb((db) => db.select<{ name: string }>(new Table("person")));
  return (
    <main>
      <ul>{people.map((p, i) => <li key={i}>{p.name}</li>)}</ul>
      <form action={addPerson}><input name="name" /><button>Add</button></form>
    </main>
  );
}
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { surql } from "surrealdb";
import { withDb } from "../lib/db";

export async function addPerson(form: FormData) {
  const name = String(form.get("name"));
  await withDb((db) => db.query(surql`CREATE person SET name = ${name}`));
  revalidatePath("/");
}
// app/api/people/route.ts
import { Table } from "surrealdb";
import { withDb } from "../../../lib/db";

export async function GET() {
  const people = await withDb((db) => db.select(new Table("person")));
  return Response.json(people);
}

Submitting the form creates a person record, the page then lists it, and GET /api/people returns the same records as JSON. The code works unchanged with SURREAL_URL set to a wss:// address.

Use HTTP for Server Components, route handlers and server actions that send one or two queries. Each call is a stateless request, which matches how a Worker handles one request at a time.

Use WebSocket for work that needs state on the connection, or for a handler that sends many queries:

  • Interactive transactions with db.beginTransaction() need WebSocket. Over HTTP, the SDK throws UnsupportedFeatureError.

  • Live queries with live() need WebSocket for the same reason.

  • On Earth, each HTTP request is routed and authenticated on its own, and costs two request units more than the same request over WebSocket. A handler that sends many queries is faster and costs less over one WebSocket connection, opened and closed within the request.

Two other options keep the number of requests down over HTTP. db.transaction([...]) sends several queries as one atomic request, and one SurrealQL query can hold several statements.

The authentication option accepts credentials, a token string, or a function that returns either. With credentials, the SDK exchanges them for a token when it connects. With a token string, the SDK uses the token as it is.

Signing in with a password is deliberately slow, because SurrealDB checks it with a password hashing algorithm that takes a noticeable fraction of a second. A Worker that signs in on every request pays that time on every request. Where the app can keep a token in a secret, such as one from the instance's Connect menu in Studio, pass the token as authentication instead.

The app's own database user should only run work that belongs to the app as a whole. For requests from signed-in end users, use record access. A DEFINE ACCESS ... TYPE RECORD statement defines how end users sign up and sign in, and each signed-in user receives a token for their own record. Pass that user's token as authentication instead of the app's credentials.

A record user can only do what a table's PERMISSIONS clause allows, and a table defined without one denies record users everything. Give each table that end users touch a clause such as FOR select, update WHERE owner = $auth, so that each user reaches only their own records. Keep the app's own credentials out of any request path that serves end users.

A Node server can open one client at startup and reuse it for every request. A Worker opens a client inside the request, closes it before returning, and reads its configuration from bindings instead of process.env.

  • Open the client inside fetch, a Server Component, a route handler or a server action, never at module scope. Cloudflare ties a connection to the request that opened it, so a shared client stops answering.

  • Read the endpoint and credentials from getCloudflareContext().env.

  • Define your tables before the app first reads them. Earth creates the namespace and the database on the first request that uses them, but a select on a table that does not exist returns an error, which a Server Component reports as a 500.

  • Prefer HTTP unless a route needs a transaction that spans several queries with logic between them, a live query, or many queries in one request.

  • Create an Earth instance and copy its endpoint from the instance's dashboard in Studio.

  • Define a database user for the app, and the record access and table permissions for your end users.

  • Add the URL, namespace, database and user as Worker variables, and the password or token as a secret.

  • Replace any module-level client with a per-request withDb helper.

  • Define your schema before the first request.

  • Build with the OpenNext Cloudflare adapter and test the Worker locally with opennextjs-cloudflare preview before you deploy.

The SurrealDB Earth documentation covers deploying, connecting and how usage is measured.

Start free on Earth

Related posts

Our newsletter

Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks