Next.js
Two clients, split so the secret key never reaches the browser.
Next.js runs your code in two places, so you create two clients. The split is not a workaround — it is the same distinction as publishable and secret keys.
Install
npm install @nulvless/nulvless-jsEnvironment
NEXT_PUBLIC_NULVLESS_URL=https://<project>.nulvless.co
NEXT_PUBLIC_NULVLESS_KEY=pk-…
NULVLESS_SECRET=sk-…Only the NEXT_PUBLIC_ values reach the browser. NULVLESS_SECRET must never
gain that prefix.
The browser client
'use client';
import { createClient, Storage } from '@nulvless/nulvless-js';
export const nulvless = createClient(
process.env.NEXT_PUBLIC_NULVLESS_URL!,
process.env.NEXT_PUBLIC_NULVLESS_KEY!,
{ storage: Storage.Local, trackUser: true },
);The server client
import 'server-only';
import { createServerClient } from '@nulvless/nulvless-js';
export const nulvlessServer = createServerClient(
process.env.NEXT_PUBLIC_NULVLESS_URL!,
process.env.NULVLESS_SECRET!,
);server-only makes a mistake fail at build time rather than shipping the secret
key to the browser.
Sign in from a client component
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { nulvless } from '@/lib/nulvless';
export default function SignIn() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function submit(formData: FormData) {
const { data, error } = await nulvless.auth.signIn({
email: String(formData.get('email')),
password: String(formData.get('password')),
});
if (error) return setError('Check your email and password.');
if (data.challenge) return router.push('/sign-in/verify');
router.push('/dashboard');
}
return (
<form action={submit}>
<input name="email" type="email" />
<input name="password" type="password" />
{error && <p role="alert">{error}</p>}
<button type="submit">Sign in</button>
</form>
);
}Sign-in belongs on the client: the SDK stores the session and refreshes the token there.
Check permissions on the server
Route handlers and server actions verify the caller's token rather than trusting the request.
import { nulvlessServer } from '@/lib/nulvless-server';
export async function GET(request: Request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
const { data, error } = await nulvlessServer.auth.verify(
{ resource: 'proj_16tbwq34', roles: ['owner', 'admin'] },
{ headers: { Authorization: `Bearer ${token}` } },
);
if (error) return new Response('Forbidden', { status: 403 });
return Response.json({ user: data.user.id });
}Forward the user's headers
Anything the server does on behalf of a user should carry that user's headers, or the session records your infrastructure instead of their device.
import { forwardUserHeaders } from '@nulvless/nulvless-js';
await nulvlessServer.auth.grants.invite(
{ invites: [{ email, role_id: roleId }], resource: orgSlug },
{ headers: forwardUserHeaders(request) },
);Which client where
Client componentscreateClientSign-in, sign-up, session, profile — anything the user does as themselves.
Route handlers, server actionscreateServerClientPermission checks, grants, admin, comms — anything requiring the secret key.