Framework guide
Node & Express
Run Neuctra Authix server-side with an API 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 API key removes a safety net
In a browser, an API key is pinned to the signed-in user by the server — a client cannot reach anyone else's records. An API 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 API 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_API_KEY=ak_live_xxxxxxxx_xxxxxxxxxxxxxxxx # Only if your server also acts on behalf of a signed-in end user. AUTHIX_API_KEY=ak_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 an API 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 apiKey: process.env.AUTHIX_API_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 API 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 an API 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. API 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, API 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 API 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 an API 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 an API key, such as admin views and billing.
Related