---
title: "Connecting your Next.js app on Cloudflare to SurrealDB Earth"
description: "Connect a Next.js app on Cloudflare Workers to SurrealDB Earth with the JavaScript SDK: one client per request, HTTP or WebSocket, env bindings."
url: https://surrealdb.com/blog/connecting-your-next-js-app-on-cloudflare-to-surrealdb-earth
date: 2026-11-13
authors: "Tobie Morgan Hitchcock"
---

# Connecting your Next.js app on Cloudflare to SurrealDB Earth

![Connecting your Next.js app on Cloudflare to SurrealDB Earth](https://cdn.surrealdb.com/9zdxyal9oln4yvfgf7jc0klh.auto)

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.

## What you need

- A Next.js app deployed to Cloudflare with [the OpenNext Cloudflare adapter](https://opennext.js.org/cloudflare), `@opennextjs/cloudflare`. The code below uses Next.js 16 and Wrangler 4.
- An Earth instance. Create one from the Deploy page in [SurrealDB Studio](https://studio.surrealdb.com), or with `surreal cloud instance deploy`.
- A database user for the app, which the next section defines.

## How does the Worker connect to Earth?

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:

```surql
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](https://surrealdb.com/docs/learn/querying/concepts-and-guides/connecting-from-serverless-and-edge#cloudflare-workers). Neither the SDK nor the `surreal` CLI reads them: the helper below passes each value to the SDK itself.

```jsonc
{
  "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`:

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

## What is the minimal working code?

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:

```ts
// 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.

```tsx
// 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>
  );
}
```

```ts
// 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("/");
}
```

```ts
// 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.

## Should you use HTTP or WebSocket?

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](https://surrealdb.com/docs/manage/instances/earth#what-is-measured) 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.

## How do auth and tokens work?

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](https://surrealdb.com/docs/reference/query-language/statements/define/access/record). 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.

## What differs from a long-running Node server?

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.

## Migration checklist

- 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](https://surrealdb.com/docs/manage/instances/earth) covers deploying, connecting and how usage is measured.

[Start free on Earth](https://surrealdb.com/earth)
