Framework guide
Next.js
Set up Neuctra Authix in a Next.js App Router project: one browser client, one server client, session state in a provider, and route protection in middleware.
The one thing to get right
Next.js runs your code in two places, and Neuctra Authix has two credentials. They line up exactly: the pk_live_… key belongs in code that reaches the browser, the sk_live_… key belongs in code that never does. Get that mapping right and the rest of this guide is mechanical.
Install the SDK
Bashnpm install @neuctra/authix # or yarn add @neuctra/authix pnpm add @neuctra/authixAdd your keys
Next.js only exposes variables prefixed with
NEXT_PUBLIC_to the browser. That prefix is the whole security boundary, so the secret key must not carry it.Bash# .env.local # Shipped to the browser — safe to expose. NEXT_PUBLIC_AUTHIX_APP_ID=app_xxxxxxxxxxxx NEXT_PUBLIC_AUTHIX_PUBLISHABLE_KEY=pk_live_xxxxxxxx_xxxxxxxxxxxxxxxx # Server only. No NEXT_PUBLIC_ prefix — that prefix is what makes a # variable public, and a secret key in the bundle is a full compromise. AUTHIX_SECRET_KEY=sk_live_xxxxxxxx_xxxxxxxxxxxxxxxxCreate the browser client
One shared instance. Creating a client per component means each one keeps its own session, and signing in on one will not sign in the others.
1// lib/authix.js 2import { NeuctraAuthix } from "@neuctra/authix"; 3 4/** Browser-safe client. Acts on behalf of the signed-in user. */ 5export const authix = new NeuctraAuthix({ 6 appId: process.env.NEXT_PUBLIC_AUTHIX_APP_ID, 7 publishableKey: process.env.NEXT_PUBLIC_AUTHIX_PUBLISHABLE_KEY, 8 baseUrl: "https://server.authix.neuctra.com/api", 9});Create the server client
Only needed if you call privileged endpoints — listing every user, reading app-wide data, or acting on a user other than the one signed in.
1// lib/authix.server.js 2import "server-only"; 3import { NeuctraAuthix } from "@neuctra/authix"; 4 5/** 6 * Full account authority. The "server-only" import makes the build fail if a 7 * client component ever imports this file, which is a much better outcome than 8 * discovering the key in a bundle later. 9 */ 10export const authixAdmin = new NeuctraAuthix({ 11 appId: process.env.NEXT_PUBLIC_AUTHIX_APP_ID, 12 secretKey: process.env.AUTHIX_SECRET_KEY, 13 baseUrl: "https://server.authix.neuctra.com/api", 14});Install server-only
npm install server-only. It has no runtime behaviour; importing it makes the build fail if a client component pulls the file in. That failure is the point — it catches a leaked secret key at build time rather than in production.Hold session state in a provider
Every component that cares whether someone is signed in reads from one place, and one
checkUserSession()call on mount decides it.1// app/providers.jsx 2"use client"; 3 4import { createContext, useContext, useEffect, useState } from "react"; 5import { authix } from "@/lib/authix"; 6 7const AuthContext = createContext(null); 8 9export function AuthProvider({ children }) { 10 const [user, setUser] = useState(null); 11 const [loading, setLoading] = useState(true); 12 13 // checkUserSession never throws — an expired or missing cookie simply 14 // resolves to authenticated: false, so no try/catch is needed here. 15 const refresh = async () => { 16 const session = await authix.checkUserSession(); 17 setUser(session.authenticated ? session.user : null); 18 setLoading(false); 19 return session; 20 }; 21 22 useEffect(() => { 23 refresh(); 24 }, []); 25 26 const value = { 27 user, 28 loading, 29 isSignedIn: Boolean(user), 30 refresh, 31 async signIn(email, password) { 32 await authix.loginUser({ email, password }); 33 return refresh(); 34 }, 35 async signUp(fields) { 36 await authix.signupUser(fields); 37 return refresh(); 38 }, 39 async signOut() { 40 await authix.logoutUser(); 41 setUser(null); 42 }, 43 }; 44 45 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; 46} 47 48export function useAuth() { 49 const context = useContext(AuthContext); 50 if (!context) throw new Error("useAuth must be used inside <AuthProvider>"); 51 return context; 52}Then wrap the app once:
1// app/layout.jsx 2import { AuthProvider } from "./providers"; 3 4export default function RootLayout({ children }) { 5 return ( 6 <html lang="en"> 7 <body> 8 <AuthProvider>{children}</AuthProvider> 9 </body> 10 </html> 11 ); 12}Build the login page
1// app/login/page.jsx 2"use client"; 3 4import { useState } from "react"; 5import { useRouter } from "next/navigation"; 6import { useAuth } from "../providers"; 7 8export default function LoginPage() { 9 const { signIn } = useAuth(); 10 const router = useRouter(); 11 const [error, setError] = useState(""); 12 const [busy, setBusy] = useState(false); 13 14 async function onSubmit(event) { 15 event.preventDefault(); 16 setBusy(true); 17 setError(""); 18 19 const form = new FormData(event.currentTarget); 20 21 try { 22 await signIn(form.get("email"), form.get("password")); 23 router.replace("/dashboard"); 24 } catch (err) { 25 // 401 means the credentials were wrong; anything else is worth 26 // distinguishing so the user is not told to check a correct password. 27 setError( 28 err?.status === 401 29 ? "Wrong email or password." 30 : "Something went wrong. Please try again.", 31 ); 32 } finally { 33 setBusy(false); 34 } 35 } 36 37 return ( 38 <form onSubmit={onSubmit}> 39 <input name="email" type="email" required /> 40 <input name="password" type="password" required /> 41 {error && <p role="alert">{error}</p>} 42 <button disabled={busy}>{busy ? "Signing in…" : "Sign in"}</button> 43 </form> 44 ); 45}
Protecting routes with middleware
Middleware runs before a protected page renders, so an unauthenticated visitor never sees a flash of the dashboard before being redirected.
1// middleware.js
2import { NextResponse } from "next/server";
3
4const PROTECTED = ["/dashboard", "/settings", "/billing"];
5
6export function middleware(request) {
7 const { pathname } = request.nextUrl;
8
9 const needsAuth = PROTECTED.some(
10 (base) => pathname === base || pathname.startsWith(base + "/"),
11 );
12 if (!needsAuth) return NextResponse.next();
13
14 // Presence of the cookie, not validity. Middleware runs on the edge and this
15 // check is only here to avoid a pointless render — the API still verifies the
16 // token on every request, which is where the real protection lives.
17 const hasSession = request.cookies.has("authix_user_session");
18 if (hasSession) return NextResponse.next();
19
20 const login = new URL("/login", request.url);
21 login.searchParams.set("next", pathname);
22 return NextResponse.redirect(login);
23}
24
25export const config = {
26 matcher: ["/dashboard/:path*", "/settings/:path*", "/billing/:path*"],
27};Middleware is UX, not security
This checks that a cookie exists. It does not verify the token — that needs the signing secret, which does not belong on the edge. A forged cookie gets past middleware and is then rejected by every API call it tries to make.
That is the correct division: middleware saves a wasted render, the server enforces access.
Reading user data
Data calls belong in client components, where the session cookie travels with the request.
1// app/dashboard/notes.jsx
2"use client";
3
4import { useEffect, useState } from "react";
5import { authix } from "@/lib/authix";
6import { useAuth } from "../providers";
7
8export default function Notes() {
9 const { user } = useAuth();
10 const [page, setPage] = useState(null);
11 const [loading, setLoading] = useState(false);
12
13 async function load(cursor) {
14 setLoading(true);
15 const result = await authix.getUserData({
16 userId: user.id,
17 category: "notes",
18 limit: 20,
19 cursor,
20 });
21
22 setPage((current) =>
23 cursor
24 ? { ...result, data: [...current.data, ...result.data] }
25 : result,
26 );
27 setLoading(false);
28 }
29
30 useEffect(() => {
31 if (user) load();
32 }, [user]);
33
34 if (!page) return <p>Loading…</p>;
35
36 return (
37 <>
38 <ul>
39 {page.data.map((note) => (
40 <li key={note.id}>{note.title}</li>
41 ))}
42 </ul>
43
44 {page.hasMore && (
45 <button onClick={() => load(page.nextCursor)} disabled={loading}>
46 {loading ? "Loading…" : "Load more"}
47 </button>
48 )}
49 </>
50 );
51}Why not fetch this in a Server Component?
You can, but only with the secret client — a Server Component has no access to the browser's cookie jar, so the publishable client there is unauthenticated. Using the secret key to fetch one user's notes also means you become responsible for checking that the request belongs to that user. Reading from the client keeps the server's ownership guarantee doing that work.
Privileged work in Server Actions
Anything that reads across users needs the secret key, which means it must run on the server.
1// app/admin/actions.js
2"use server";
3
4import { authixAdmin } from "@/lib/authix.server";
5
6/**
7 * Runs on the server, so it may use the secret key and read across users.
8 * Authorise the caller yourself before returning anything — a Server Action is
9 * a public endpoint, not a private function.
10 */
11export async function listUsers(cursor) {
12 return authixAdmin.getAllUsersFromApp({ limit: 50, cursor });
13}Authorise the caller
A Server Action is reachable by anyone who can reach your site. The secret key gives it full account authority, so check that the caller is allowed to run it before returning anything — being logged in is not the same as being an admin.
Project structure
Where each piece ends up in a typical App Router project.
your-app/
├── app/
│ ├── layout.jsx # wraps everything in <AuthProvider>
│ ├── providers.jsx # "use client" — session state + useAuth()
│ ├── login/page.jsx # "use client" — sign-in form
│ ├── signup/page.jsx # "use client" — sign-up + verification
│ ├── dashboard/
│ │ ├── page.jsx # protected by middleware
│ │ └── notes.jsx # "use client" — reads user records
│ └── admin/
│ └── actions.js # "use server" — secret key only
├── lib/
│ ├── authix.js # publishable client (browser)
│ └── authix.server.js # secret client ("server-only")
├── middleware.js # redirects unauthenticated users
└── .env.localCommon mistakes
- Prefixing the secret key with NEXT_PUBLIC_. It will be inlined into the JavaScript bundle and readable by anyone. Rotate the key immediately if this has happened.
- Creating a client inside a component. Each instance keeps its own session; put one in
lib/authix.jsand import it. - Calling data methods from a Server Component with the publishable client. There is no cookie there, so the call is unauthenticated and returns
NO_USER_SESSION. - Treating middleware as the security boundary. It cannot verify a token. The API does that on every request.
- Skipping email verification. An unverified account can sign in but cannot store records — the write fails with a 403 later, far from the cause.
You now have
- Sign-up, sign-in and sign-out backed by a real session.
- Protected routes that redirect before rendering.
- Per-user data reads that cannot reach another user's rows.
- A server client for privileged work, kept out of the bundle.
Related