Read and Write CloudBase Database in Next.js Server Components
In one sentence: Use
@cloudbase/node-sdkto directlyawait db.collection().get()inside a Next.js App Router Server Component, route write operations through a Server Action ('use server') followed byrevalidatePathto invalidate the cache, and keep Client Components responsible only for forms and triggering — with no awareness of the database.Estimated time: 30 minutes | Difficulty: Advanced
Applicable Scenarios
- Applicable: Full-stack Next.js projects that want to connect to a CloudBase database directly without setting up a separate API route layer
- Applicable: Already using RSC (React Server Components) and want to push data fetching down into the component tree
- Applicable: Already have a CloudBase backend (shared with a Mini Program / mobile app) and are rewriting the web side with Next.js
- Not applicable: Pure frontend SPA / static export (
output: 'export') — use add-auth-web-with-cloudbase-sdk with the client SDK instead - Not applicable: Mini Program business logic — use add-database-wechat-miniprogram
- Not applicable: Deploying the entire Next.js app to Cloudflare Workers / Edge Runtime —
@cloudbase/node-sdkdepends on Node built-in modules and cannot run on the Edge
Prerequisites
| Dependency | Version |
|---|---|
| Node.js | ≥ 18.17 (minimum required by Next.js 14 / 15) |
next | ^14.x or ^15.x (App Router) |
@cloudbase/node-sdk | Latest (npm view @cloudbase/node-sdk version) |
| Deployment runtime | Node Runtime (default) — Edge Runtime is not supported |
Also required:
- A provisioned CloudBase environment ID
- A pair of Tencent Cloud API keys (
SecretId/SecretKey) — apply in the console at Access Management (CAM) with at leastQcloudTCBFullAccess(or the finer-grainedQcloudTCBReadWriteAccess) - A test collection
userswith its permission set to "Only creator and admin can read and write" to prevent the client SDK from bypassing the server and directly accessing the database
@cloudbase/node-sdk uses Tencent Cloud API keys, which carry admin-level access and must never be imported into any Client Component ('use client' file). Next.js bundles Client Component dependencies into the client-side JS, which would expose the keys in the bundle. Every @cloudbase/node-sdk import in this guide appears exclusively in Server Components, Server Actions, or Route Handlers.
Step 1: Shared Initialization in lib/cloudbase.ts
tcb.init maintains a connection pool internally; initializing it in every file wastes resources. Centralize it in a shared module with lazy initialization:
npm i @cloudbase/node-sdk
lib/cloudbase.ts:
import tcb from '@cloudbase/node-sdk';
let cached: ReturnType<typeof tcb.init> | null = null;
export function getCloudbase() {
if (!cached) {
cached = tcb.init({
env: process.env.CLOUDBASE_ENV!,
secretId: process.env.TENCENTCLOUD_SECRETID,
secretKey: process.env.TENCENTCLOUD_SECRETKEY,
});
}
return cached;
}
.env.local (local development only — do not commit):
CLOUDBASE_ENV=your-env-id
TENCENTCLOUD_SECRETID=AKIDxxxxxx
TENCENTCLOUD_SECRETKEY=xxxxxxxx
Key points:
- No
'use server'/'use client'directive — this is a plain ES module; which runtime imports it determines where it runs. This guide only allows the server side to import it. - The three environment variable names follow the Tencent Cloud convention:
TENCENTCLOUD_SECRETID/TENCENTCLOUD_SECRETKEY. These are injected automatically on managed platforms like Tencent Cloud Run / Cloud Functions; use.env.localto simulate them locally. - For Vercel / self-hosted servers: configure the same variable names in the platform's environment variable panel.
Step 2: Directly Await the Database in a Server Component
page.tsx files in App Router are Server Components by default. They can be async, allowing a top-level await on the database that renders synchronous HTML:
app/users/page.tsx:
import { getCloudbase } from '@/lib/cloudbase';
export default async function UsersPage() {
const app = getCloudbase();
const db = app.database();
const { data } = await db.collection('users').limit(10).orderBy('createdAt', 'desc').get();
return (
<main>
<h1>User List</h1>
<ul>
{data.map((u) => (
<li key={u._id}>
{u.name} — {new Date(u.createdAt).toLocaleString('en-US')}
</li>
))}
</ul>
</main>
);
}
Key points:
- The direct
awaitin a Server Component causes Next.js to wait for the query before sending HTML, eliminating first-paint loading flicker. db.collection().get()returns{ data, requestId, ... }. When only the data matters, destructure.data.- To run queries in parallel:
const [a, b] = await Promise.all([q1.get(), q2.get()]) - This file has no
'use client'directive — it is RSC by default and cannot use client hooks likeuseState/useEffect.
Step 3: Write Data with a Server Action
Place write operations in a dedicated actions.ts file with 'use server' at the top. Next.js marks this file as "always runs on the server"; when a Client Component imports it, it only receives an RPC handle — the code is never bundled into the client.
app/users/actions.ts:
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { getCloudbase } from '@/lib/cloudbase';
export async function createUser(formData: FormData) {
const name = (formData.get('name') as string)?.trim();
if (!name) {
return { ok: false, error: 'Name cannot be empty' };
}
try {
const app = getCloudbase();
const db = app.database();
await db.collection('users').add({
data: {
name,
createdAt: Date.now(),
},
});
} catch (e: any) {
return { ok: false, error: e.message ?? 'unknown error' };
}
// Invalidate the RSC cache for the /users path so the query re-runs
revalidatePath('/users');
return { ok: true };
}
Key points:
'use server'must be the very first line in the file (comments may precede it); it cannot be placed inside a function body.- Server Actions must be
async function. Parameters and return values must be serializable —FormData/ primitives / plain objects are fine; functions, class instances, andDateobjects are not. (Dateserializes to a string, so the example usesDate.now()as a number instead.) - Do not
throwerrors to the client. The client receives a generic Next.js-wrapped error with incomplete information. Return{ ok, error }instead. revalidatePath('/users')invalidates all RSC caches under that path. The next request re-runs the query. Without this call, new data only appears on a hard refresh.
app/users/CreateForm.tsx (Client Component):
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createUser } from './actions';
const initialState = { ok: false, error: '' };
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create'}
</button>
);
}
export function CreateForm() {
const [state, formAction] = useFormState(createUser, initialState);
return (
<form action={formAction}>
<input name="name" placeholder="Name" required />
<SubmitButton />
{state?.error && <span style={{ color: 'red' }}>{state.error}</span>}
</form>
);
}
Key points:
<form action={createUser}>is the React 19 / Next.js 14+ pattern; form submission goes directly to the Server Action without needingonSubmit+fetch.useFormStatefeeds the Server Action's return value into React state so error messages can be rendered.useFormStatusprovides thependingstate during submission to disable the button. Note: it must be used in a child component of<form>, not in the same component asuseFormState.- Starting with React 19,
useFormStateis renameduseActionStateand imported fromreactinstead ofreact-dom. Next.js 15 + React 19 uses the new name.
Mount the form in page.tsx:
import { getCloudbase } from '@/lib/cloudbase';
import { CreateForm } from './CreateForm';
export default async function UsersPage() {
const { data } = await getCloudbase().database().collection('users').limit(10).orderBy('createdAt', 'desc').get();
return (
<main>
<h1>User List</h1>
<CreateForm />
<ul>
{data.map((u) => (
<li key={u._id}>{u.name}</li>
))}
</ul>
</main>
);
}
Step 4: Refresh Caches with revalidatePath / revalidateTag
Next.js RSC performs "request-level deduplication" and "Data Cache" by default: identical fetches within a single render are deduplicated, and pages can be statically generated at build time. revalidatePath and revalidateTag are the two mechanisms for actively invalidating that cache.
// Path-level: invalidate all render caches for /users
revalidatePath('/users');
// Dynamic route invalidation: the second argument specifies the type
revalidatePath('/users/[id]', 'page');
// Tag-level: bulk invalidation
import { revalidateTag } from 'next/cache';
revalidateTag('users');
To associate a database query with a tag, wrap it in unstable_cache:
// lib/users.ts
import { unstable_cache } from 'next/cache';
import { getCloudbase } from './cloudbase';
export const getUsers = unstable_cache(
async () => {
const { data } = await getCloudbase().database().collection('users').limit(10).get();
return data;
},
['users-list'],
{ tags: ['users'], revalidate: 60 }, // Auto-stale after 60s, or actively invalidated by revalidateTag('users')
);
After a write in a Server Action, call revalidateTag('users') to simultaneously invalidate all queries tagged with users.
If only one page queries users, revalidatePath alone is sufficient. Reaching for unstable_cache adds complexity. Use it when multiple pages share the same query, or when you need time-based revalidation.
Step 5: Configure Permission Rules
CloudBase database has 6 built-in permission modes. Server-side admin operations bypass collection permissions (API keys carry admin authority), but to prevent the client SDK from bypassing the server entirely, set the collection to the most restrictive mode:
Console → Database → your collection → Permission Settings → select "Only creator and admin can read and write" or "Custom Security Rules":
{
"read": "auth.uid != null && doc._openid == auth.uid",
"write": "auth.uid != null && doc._openid == auth.uid"
}
For user identity verification in the business layer, Server Actions have two approaches for identifying the current user:
Option A — Manage your own user system (recommended)
Use next-auth or a custom session in Next.js. Read a cookie in the Server Action to get userId, then write it as a business field on the document:
'use server';
import { cookies } from 'next/headers';
import { getCloudbase } from '@/lib/cloudbase';
export async function createUser(formData: FormData) {
const session = cookies().get('session')?.value;
const userId = await verifySession(session); // your own verification logic
if (!userId) return { ok: false, error: 'Not logged in' };
const db = getCloudbase().database();
await db.collection('users').add({
data: { name: formData.get('name'), ownerId: userId, createdAt: Date.now() },
});
// ...
}
Option B — Use the CloudBase user system
If CloudBase users already exist (e.g., shared with a Mini Program), exchange the client token for a CloudBase identity on the server:
const app = getCloudbase();
const auth = app.auth();
const ticket = await auth.getUserInfo({ accessToken: clientToken });
// ticket.uid is the CloudBase uid
In practice, the vast majority of SaaS applications use Option A — the server holds admin access for all operations, the business layer defines who can modify what, and the database is simply a locked-down store.
Step 6: Error Boundary
Errors thrown inside a Server Component bubble up to the nearest error.tsx. You can create an error boundary for each route segment:
app/users/error.tsx:
'use client'; // error.tsx must be a Client Component
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Failed to load users</h2>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
);
}
For "not found" semantics, call notFound() directly in the Server Component to navigate to not-found.tsx:
import { notFound, redirect } from 'next/navigation';
export default async function UserDetail({ params }: { params: { id: string } }) {
const { data } = await getCloudbase().database().collection('users').doc(params.id).get();
if (!data || data.length === 0) notFound();
// No permission to view? Redirect.
if (data[0].private) redirect('/login');
return <div>{data[0].name}</div>;
}
notFound() and redirect() work by throwing a special error object that Next.js catches and handles. Do not wrap them in a try/catch — that will swallow them.
Verification
- Local: run
npm run devto start Next.js and visithttp://localhost:3000/users. The page should render immediately with an empty list (no data yet) and the create form. - Enter a name in the form and submit. Within one second, the new entry should appear in the list (
revalidatePathtriggered an RSC re-render). - Console → Database → users collection: confirm the submitted record is present with the correct
nameandcreatedAtfields. - Stop the dev server. Open the browser's Network panel and refresh the page — the
usersdocument response should be complete HTML with the data already rendered, not fetched afterward. This confirms SSR is working correctly. - In the browser Console, search for
TENCENTCLOUD_SECRETIDor the prefix of your key (e.g.,AKID). Nothing should appear. If anything does, a Client Component has mistakenly importedlib/cloudbase.ts— investigate immediately. - Deploy to Vercel / Tencent Cloud Run. Configure
CLOUDBASE_ENV,TENCENTCLOUD_SECRETID, andTENCENTCLOUD_SECRETKEYin the platform's environment variable panel, redeploy, and repeat steps 1–5.
Common Errors
| Error / symptom | Cause | Fix |
|---|---|---|
process is not defined or Node built-in module errors in the browser Console | lib/cloudbase.ts or @cloudbase/node-sdk was imported in a Client Component ('use client' file) | Move all database calls to Server Components / Server Actions; Client Components should only call Server Actions. Alternatively, add @cloudbase/node-sdk to serverExternalPackages in next.config.js to force server-only bundling. |
Server Actions must be async functions from a Server Action call | A non-async export exists in actions.ts | All exports in a 'use server' file must be async functions; synchronous functions are not allowed. |
Cannot pass non-serializable parameter from a Server Action call | A Date instance, class instance, or function was passed to an Action | Use primitives / FormData / plain objects. Replace Date with .getTime() or an ISO string. |
revalidatePath('/users') was called but the page did not refresh | The path is wrong (wrong casing, or a dynamic segment is missing) | The path must match the file route exactly. For dynamic routes, use revalidatePath('/users/[id]', 'page'). |
The edge runtime does not support Node.js after deployment | The route file has export const runtime = 'edge', or the deployment target is an Edge Runtime platform | @cloudbase/node-sdk requires Node Runtime. Remove runtime = 'edge', or switch to the CloudBase client SDK over HTTP. |
Database query timeout / connect ETIMEDOUT | The deployment environment's egress IP is blocked, or the SDK is resolving to the wrong domain | CloudBase defaults to the public internet and requires no IP allowlist for public cloud deployments. For private network deployments, confirm that tcb-api.tencentcloudapi.com is reachable. |
| Client SDK can read from the collection directly in the Console | Collection permission is too permissive ("All users can read") | Change to "Only creator and admin can read and write" or a custom rule. Server admin is unaffected. |
| User identity lost — user state missing | Trying to maintain user login state inside @cloudbase/node-sdk | The node SDK carries admin identity and does not maintain user state. Manage user state via Next.js cookies / session. See Step 5, Option A. |
For error code definitions, see error-code.
Related Documentation
- Node.js SDK Database API — complete signatures for
db.collection().add/get/update/remove - Node.js SDK Initialization —
tcb.initparameter reference - Collection Permission Rules — 6 built-in modes and custom security rule syntax
- add-database-wechat-miniprogram — counterpart guide: operating on the same collection from a Mini Program
- add-vercel-ai-sdk-streaming-chatbot — companion guide: Next.js + AI SDK, composable with this recipe
- deploy-nextjs-to-cloudbase-run — deploy this Next.js project to Tencent Cloud Run
- secure-database-multi-tenant-rules — advanced permission rules for multi-tenant scenarios
Next Steps
- Integrate CloudBase user authentication: use the add-auth-web-with-cloudbase-sdk client SDK to obtain a token on the web side, then verify it on the server
- Push database change notifications to the web client in real time: add-realtime-notifications-database-watch
- Query optimization for high-frequency write scenarios: optimize-database-query-performance