Framework guide
Node & Express
Run Neuctra Authix server-side with a secret key: auth endpoints, per-user data scoped by your own session check, admin reads across every user, and error handling that preserves status codes.
The secret key removes a safety net
In a browser, a publishable key is pinned to the signed-in user by the server — a client cannot reach anyone else's records. A secret key has no such limit, by design: it can read and write every user in the app.
That means scoping becomes your responsibility. Take the user id from a session you verified, never from the request.
Install
Bashnpm install @neuctra/authix express dotenv # or yarn add @neuctra/authix express dotenv pnpm add @neuctra/authix express dotenvAdd your keys
Keep the secret key in the environment and out of source control. Anyone holding it has full authority over your account.
Bash# .env — never commit this file AUTHIX_APP_ID=app_xxxxxxxxxxxx AUTHIX_SECRET_KEY=sk_live_xxxxxxxx_xxxxxxxxxxxxxxxx # Only if your server also acts on behalf of a signed-in end user. AUTHIX_PUBLISHABLE_KEY=pk_live_xxxxxxxx_xxxxxxxxxxxxxxxxCreate the client
1// src/authix.js 2import "dotenv/config"; 3import { NeuctraAuthix } from "@neuctra/authix"; 4 5/** 6 * Full account authority: this client can read and write any user in the app. 7 * Construction throws if the key is malformed or is a publishable key, so a 8 * misconfiguration fails at boot rather than on the first request. 9 */ 10export const authix = new NeuctraAuthix({ 11 appId: process.env.AUTHIX_APP_ID, 12 secretKey: process.env.AUTHIX_SECRET_KEY, 13 baseUrl: "https://server.authix.neuctra.com/api", 14});Expose auth endpoints
Handling signup on the server lets you enforce your own rules — invite codes, allowed domains, seat limits — before the account exists.
1// src/routes/auth.js 2import { Router } from "express"; 3import { authix } from "../authix.js"; 4 5const router = Router(); 6 7router.post("/signup", async (req, res, next) => { 8 try { 9 const { name, email, password } = req.body; 10 11 const result = await authix.signupUser({ name, email, password }); 12 13 await authix.requestEmailVerificationOTP({ 14 userId: result.user.id, 15 email, 16 }); 17 18 res.status(201).json({ userId: result.user.id }); 19 } catch (error) { 20 next(error); 21 } 22}); 23 24router.post("/verify", async (req, res, next) => { 25 try { 26 const { email, otp } = req.body; 27 res.json(await authix.verifyEmail({ email, otp })); 28 } catch (error) { 29 next(error); 30 } 31}); 32 33router.post("/login", async (req, res, next) => { 34 try { 35 const { email, password } = req.body; 36 res.json(await authix.loginUser({ email, password })); 37 } catch (error) { 38 next(error); 39 } 40}); 41 42export default router;Translate SDK errors into HTTP responses
1// src/middleware/errors.js 2 3/** 4 * The SDK throws objects carrying { status, code, message }. Passing the status 5 * through keeps your API honest — a 409 from Neuctra Authix should not become a 500 6 * from you, because the caller can act on a 409 and cannot act on a 500. 7 */ 8export function authixErrors(error, req, res, next) { 9 if (!error?.status) return next(error); 10 11 const body = { message: error.message, code: error.code }; 12 13 if (error.code === "VERSION_CONFLICT") { 14 body.currentVersion = error.currentVersion; 15 } 16 17 // Never forward an INSUFFICIENT_SCOPE upstream — it means your server used 18 // the wrong key, which is your bug, not the caller's. 19 if (error.code === "INSUFFICIENT_SCOPE") { 20 console.error("Neuctra Authix scope error — wrong key for", req.path); 21 return res.status(500).json({ message: "Server misconfiguration." }); 22 } 23 24 res.status(error.status).json(body); 25}
Scoping per-user data
The single most important pattern on this page.
1// src/routes/notes.js
2import { Router } from "express";
3import { authix } from "../authix.js";
4import { requireUser } from "../middleware/requireUser.js";
5
6const router = Router();
7
8router.use(requireUser);
9
10/**
11 * The secret key can read ANY user, so the scoping the browser gets for free
12 * is now your job. Always take the user id from the verified session, never
13 * from the request body or the URL.
14 */
15router.get("/", async (req, res, next) => {
16 try {
17 const page = await authix.getUserData({
18 userId: req.user.id, // <- trusted, from the session
19 category: "notes",
20 limit: 20,
21 cursor: req.query.cursor,
22 });
23
24 res.json(page);
25 } catch (error) {
26 next(error);
27 }
28});
29
30router.post("/", async (req, res, next) => {
31 try {
32 const created = await authix.addUserData({
33 userId: req.user.id,
34 dataCategory: "notes",
35 data: { title: req.body.title, body: req.body.body },
36 });
37
38 res.status(201).json(created);
39 } catch (error) {
40 next(error);
41 }
42});
43
44export default router;Where this goes wrong
Accepting userId from req.body or req.params turns your endpoint into an open door: any caller can read any user by changing one value. It is the most common way a server-side integration leaks data, and it is invisible in testing because your own client always sends the right id.
Admin operations
Listing and searching across users is exactly what a secret key is for.
1// src/routes/admin.js
2import { Router } from "express";
3import { authix } from "../authix.js";
4import { requireAdmin } from "../middleware/requireAdmin.js";
5
6const router = Router();
7
8router.use(requireAdmin);
9
10// Reads across every user in the app. Secret key only.
11router.get("/users", async (req, res, next) => {
12 try {
13 res.json(
14 await authix.getAllUsersFromApp({
15 limit: 50,
16 cursor: req.query.cursor,
17 }),
18 );
19 } catch (error) {
20 next(error);
21 }
22});
23
24router.get("/users/search", async (req, res, next) => {
25 try {
26 res.json(
27 await authix.searchInAllAppUsers({
28 q: req.query.q,
29 keys: req.query.role ? { role: req.query.role } : undefined,
30 limit: 50,
31 }),
32 );
33 } catch (error) {
34 next(error);
35 }
36});
37
38export default router;Filters on user search are restricted to an allowlist — id, username, name, email, phone, address, role, isVerified, isActive. Anything else is rejected, so credential columns can never be filtered on.
Changing records together
When a partial write would leave your data inconsistent, use a batch.
1// Two records that must change together, or not at all.
2await authix.batch({
3 userId,
4 operations: [
5 { type: "create", dataCategory: "orders", total: 40, status: "placed" },
6 { type: "update", id: stockId, version: stock.version, remaining: 9 },
7 { type: "delete", id: draftId },
8 ],
9});Every operation commits or none do. A version mismatch or a missing record aborts the whole batch, so an order can never be recorded without the stock deduction that goes with it.
Wiring it together
1// src/server.js
2import express from "express";
3import authRoutes from "./routes/auth.js";
4import noteRoutes from "./routes/notes.js";
5import adminRoutes from "./routes/admin.js";
6import { authixErrors } from "./middleware/errors.js";
7
8const app = express();
9app.use(express.json());
10
11app.use("/api/auth", authRoutes);
12app.use("/api/notes", noteRoutes);
13app.use("/api/admin", adminRoutes);
14
15app.use(authixErrors);
16
17app.listen(3000, () => console.log("listening on :3000"));Project structure
A layout that keeps the scoping decision in one obvious place.
src/
├── authix.js # one client, secret key
├── server.js # express app
├── middleware/
│ ├── errors.js # maps SDK errors to HTTP responses
│ ├── requireUser.js # your session check
│ └── requireAdmin.js # your role check
└── routes/
├── auth.js # signup, verify, login
├── notes.js # per-user data, scoped by YOU
└── admin.js # cross-user readsCommon mistakes
- Trusting a client-supplied user id. Take it from the verified session, always.
- Returning Neuctra Authix errors verbatim. An
INSUFFICIENT_SCOPEmeans your server used the wrong key — surfacing it tells the caller about your internals and suggests they did something wrong. - Collapsing every failure into a 500. A 409 is actionable and a 500 is not.
- Shipping the secret key to a client. If it ever reaches a browser or a mobile binary, rotate it.
- Looping single deletes. Use
deleteManyUserData— one transaction, up to 100 ids.
You now have
- Server-side signup, verification and login endpoints.
- Per-user data reads scoped to a session you verified.
- Admin endpoints that read across every user.
- Errors that keep their meaning as they cross your API.
Pairing with a frontend
You do not have to proxy everything. A common split is: the browser uses a publishable key for the signed-in user's own data — where the server scopes it for you — and your Node service handles only what genuinely needs a secret key, such as admin views and billing.
Related