Skip to main content

Add CloudBase Authentication to Next.js

In one sentence: Use @cloudbase/js-sdk in a Next.js 14+ App Router project to integrate SMS Verification Code / WeChat Open Platform QR Login, store login state in an httpOnly cookie, read identity in Server Components via cookies(), perform lightweight validation in middleware running on Edge Runtime, and run heavy business logic in Route Handlers with runtime: 'nodejs'.

Estimated time: 35 minutes | Difficulty: Advanced

Applicable Scenarios

  • Applicable: Next.js 14 / 15 App Router full-stack applications where SSR pages need user identity for personalized rendering
  • Applicable: React SPAs already using add-auth-web-with-cloudbase-sdk that want to migrate to Next.js for SSR + middleware interception
  • Applicable: Using middleware for unauthenticated request interception / canary routing / A-B splits
  • Not applicable: Pure Pages Router projects — go directly to the add-auth-web-with-cloudbase-sdk SPA template. Pages Router does not have the concept of Server Components or Server Actions
  • Not applicable: Obtaining admin-level login state on the Node.js side (that requires server tokens from @cloudbase/node-sdk, which is outside the scope of this recipe)

Prerequisites

DependencyVersion
Node.js18.18.0
next14.2.x or 15.x
@cloudbase/js-sdk2.27.3 (or @cloudbase/js-sdk@next, i.e. 3.3.x)
react / react-dom^18.3.0 (Next.js 15 uses ^19.0.0)

External dependencies:

  • An active CloudBase environment ID, region set to Shanghai (SMS Verification Code login is only supported in the Shanghai region)
  • In Console → Authentication → Login Methods, enable the methods you want:
    • "SMS Verification Code Login"
    • "WeChat Open Platform Login" — enter the AppID and AppSecret from a website application registered on WeChat Open Platform
Next.js 15 cookies() is async

Next.js 15 changed cookies() / headers() / params / searchParams to all be async — they must be await-ed before use. Next.js 14 keeps the synchronous API. This recipe is written for Next.js 15; for Next.js 14 simply replace await cookies() with cookies().

Step 1: Install @cloudbase/js-sdk in Your Next.js Project

npx create-next-app@latest my-cloudbase-app --typescript --app --tailwind=false --eslint --src-dir=false --import-alias="@/*"
cd my-cloudbase-app
npm i @cloudbase/js-sdk

Create lib/cloudbase.ts (client-only; the SDK uses crypto / localStorage):

'use client';

import cloudbase from '@cloudbase/js-sdk';

export const app = cloudbase.init({
env: 'your-env-id',
// Omitting region defaults to Shanghai; SMS login is only supported in Shanghai
});

export const auth = app.auth({ persistence: 'local' });

Step 2: Write the Login Page as a Client Component (SMS + WeChat QR)

app/login/page.tsx (marked 'use client' because it uses the SDK):

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { auth } from '@/lib/cloudbase';
import { setSessionCookie } from './actions';

export default function LoginPage() {
const router = useRouter();
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [verificationInfo, setVerificationInfo] = useState<any>(null);
const [countdown, setCountdown] = useState(0);
const [err, setErr] = useState('');

const sendCode = async () => {
setErr('');
if (!/^\+86\s?\d{11}$/.test(phone)) {
setErr('Phone number format should be +86 13800000000');
return;
}
try {
const info = await auth.getVerification({ phone_number: phone });
setVerificationInfo(info);
setCountdown(60);
const t = setInterval(() => {
setCountdown((c) => {
if (c <= 1) { clearInterval(t); return 0; }
return c - 1;
});
}, 1000);
} catch (e: any) {
setErr(e.message || 'Failed to send');
}
};

const submit = async (e: React.FormEvent) => {
e.preventDefault();
setErr('');
try {
await auth.signInWithSms({
verificationInfo,
verificationCode: code,
phoneNum: phone,
});
// Login successful — write access token to httpOnly cookie
const loginState = await auth.getLoginState();
if (!loginState) throw new Error('Login state is empty');
await setSessionCookie(loginState.accessToken);
router.push('/');
} catch (e: any) {
setErr(e.message || 'Login failed');
}
};

return (
<form onSubmit={submit}>
<input
placeholder="+86 13800000000"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
<button type="button" onClick={sendCode} disabled={countdown > 0}>
{countdown > 0 ? `Resend in ${countdown}s` : 'Get verification code'}
</button>
<input
placeholder="6-digit code"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<button type="submit">Login</button>
{err && <div style={{ color: 'red' }}>{err}</div>}
</form>
);
}

Key points:

  • auth.signInWithSms follows the two-step flow getVerificationsignInWithSms; verificationInfo is the object returned by getVerification and must be passed back as-is
  • After a successful client-side login, the accessToken is written to an httpOnly cookie via the Server Action setSessionCookie, making it accessible to Server Components and middleware
  • WeChat QR Login uses an OAuth redirect flow. The process is the same as in add-auth-web-with-cloudbase-sdk Step 5; in Next.js simply swap the callback page for app/login/wechat-callback/page.tsx — the logic is unchanged

app/login/actions.ts (marked 'use server'):

'use server';

import { cookies } from 'next/headers';

export async function setSessionCookie(accessToken: string) {
const cookieStore = await cookies();
cookieStore.set('cloudbase_session', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 30, // 30 days, aligned with SDK persistence: 'local'
});
}

export async function clearSessionCookie() {
const cookieStore = await cookies();
cookieStore.delete('cloudbase_session');
}

Key points:

  • httpOnly: true prevents JavaScript from reading the cookie, protecting the token from XSS
  • sameSite: 'lax' is the recommended default; do not use 'strict' — cross-site redirects (e.g. WeChat QR callback) will not send the cookie
  • secure: true is required in production; use process.env.NODE_ENV === 'production' so that local HTTP development still works
  • Server Components cannot directly set / delete cookies — they can only read. Write operations must be placed in a Server Action or Route Handler

Step 4: Read Identity in a Server Component via cookies()

app/page.tsx (a Server Component by default — do not add 'use client'):

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

export default async function HomePage() {
const cookieStore = await cookies(); // must await in Next.js 15
const accessToken = cookieStore.get('cloudbase_session')?.value;

if (!accessToken) {
redirect('/login');
}

// If you only need to display "logged in" status, having the cookie is sufficient.
// To fetch detailed user info, call the CloudBase server-side API via a Route Handler,
// or write the uid into a separate cookie alongside the access token.
return (
<div>
<h1>Welcome</h1>
<p>You are logged in (token length: {accessToken.length})</p>
</div>
);
}

If you need uid, phone number, or other user details inside a Server Component, there are two approaches:

  1. At login time in Step 2, write a second cloudbase_uid cookie in addition to accessToken; the Server Component reads it directly
  2. Have the Server Component call an internal Route Handler (runtime: 'nodejs') that exchanges the accessToken for detailed user info via the CloudBase server-side API (see Step 6)

Step 5: Intercept Unauthenticated Requests with middleware.ts

middleware.ts (place in the project root, not under app/):

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
const session = request.cookies.get('cloudbase_session');
const { pathname } = request.nextUrl;

// Redirect logged-in users away from /login back to home
if (session && pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/', request.url));
}

// Redirect unauthenticated users trying to access protected routes to /login
if (!session && !pathname.startsWith('/login')) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}

return NextResponse.next();
}

export const config = {
matcher: ['/((?!login|_next/static|_next/image|favicon.ico|api/public).*)'],
};

Key points:

  • middleware runs on Edge Runtime. @cloudbase/js-sdk uses Node.js APIs (crypto, fs) and cannot be import-ed inside middleware
  • middleware only does lightweight validation: whether the cookie exists and whether its format is valid. Whether the token is actually valid, whether it has expired, and which uid it belongs to should be handled in a Server Component or Route Handler
  • The matcher uses a negative lookahead (?!...) to exclude paths that do not need interception; omitting _next/static causes initial-load chunks to be redirected as well, resulting in a blank page
  • Redirecting logged-in users away from /login prevents the UX loop of "clicking login when already logged in"

Step 6: Call the CloudBase API from a Server Action (Write Data)

When business logic needs to use the accessToken to call CloudBase server-side APIs (read database / invoke cloud functions), place it in a Server Action or Route Handler with runtime: 'nodejs'.

app/api/profile/route.ts:

import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

// Required: force Node Runtime — @cloudbase/js-sdk cannot run on Edge
export const runtime = 'nodejs';

export async function GET() {
const cookieStore = await cookies();
const accessToken = cookieStore.get('cloudbase_session')?.value;
if (!accessToken) {
return NextResponse.json({ error: 'unauthenticated' }, { status: 401 });
}

// Inside Node Runtime, you can import and use the SDK / call HTTP normally.
// Use accessToken to call the CloudBase server-side OpenAPI, or use @cloudbase/node-sdk.
// This example only demonstrates forwarding the session check.
return NextResponse.json({ ok: true, hasSession: true });
}

Key points:

  • Any server-side code that imports @cloudbase/js-sdk or @cloudbase/node-sdk must declare export const runtime = 'nodejs'
  • The default Route Handler in Next.js inherits the runtime from its parent route.ts config — an explicit declaration is the most reliable approach

Sign Out

'use client';

import { auth } from '@/lib/cloudbase';
import { clearSessionCookie } from '@/app/login/actions';
import { useRouter } from 'next/navigation';

export function SignOutButton() {
const router = useRouter();

const handleSignOut = async () => {
await auth.signOut(); // Clear the SDK's local login state
await clearSessionCookie(); // Server Action deletes the cookie
router.push('/login');
};

return <button onClick={handleSignOut}>Sign Out</button>;
}

signOut() clears the client-side SDK state; clearSessionCookie() clears the server-side cookie. Both steps are required — skipping either causes the client to appear logged out while SSR still shows the authenticated state.

Verification

  1. Run npm run dev and open http://localhost:3000 in a browser — middleware should redirect you to /login?from=/
  2. Use a real phone number for the SMS Verification Code flow; after a successful login you should be redirected back to /, where the Server Component renders the "Welcome" page
  3. In Console → Authentication → User Management, the user record for this login should be visible, with type "Phone"
  4. In browser DevTools → Application → Cookies, the cloudbase_session cookie should be present with the HttpOnly column checked; document.cookie in the Console should not show it
  5. Refresh the page (F5) — the Server Component should still recognize the login state and not redirect back to /login (SSR reads the cookie)
  6. Click Sign Out — the cookie is cleared; after a refresh you are redirected back to /login

Common Errors

Error / SymptomCauseFix
cookies().get is not a function or Property 'get' does not existNext.js 15 made cookies() async; calling it directly returns a PromiseChange to const cookieStore = await cookies(); cookieStore.get(...)
import cloudbase from '@cloudbase/js-sdk' in middleware throws crypto is not defined or Module not found: Can't resolve 'fs'middleware runs on Edge Runtime; the SDK uses Node.js APIsDo not import the SDK in middleware — only read the cookie for existence checks; move real validation to a Route Handler with runtime: 'nodejs'
cookieStore.set(...) in a Server Component throws Cookies can only be modified in a Server Action or Route HandlerThe response headers are already sent during RSC streamingMove set / delete operations into a 'use server' Server Action
Adding sameSite: 'strict' to cookieStore.set causes the WeChat QR callback to not send the cookieStrict mode does not send cookies on cross-site redirectsChange to sameSite: 'lax' (recommended default)
Setting matcher: '/' in middleware leaves /about unprotectedA single string only matches / exactly, not as a prefixUse the negative lookahead pattern ['/((?!login|_next).*)'], or use multiple rules
SMS send returns "region not supported"CloudBase environment is not in ShanghaiSMS login only supports the Shanghai region; switch to a Shanghai environment or use another login method
phone_number format invalidMissing +86 country codePass Chinese mainland numbers as +86 13800000000
Refreshing the page after login redirects back to /loginpath: '/' was omitted in cookieStore.set, so the cookie only applies to the current pathExplicitly add path: '/' to cookieStore.set
Cookie not working after deploying to Vercel / CloudBase Hostingsecure: false in production causes the browser to reject the cookie, or secure: true with an HTTP siteProduction must use HTTPS + secure: true; use secure: process.env.NODE_ENV === 'production' for local HTTP

For error code definitions, see error-code.