Skip to content
Nile Js
Features

Authentication and hooks

Protect actions with JWT verification, enforce required roles and run checks before or after an action

Read Markdown ↗

Nile Js verifies JWTs for actions you mark as protected, and it runs hooks around every action. Use the built-in verifier for identity, a global before hook for policy such as roles, and local hooks for steps that belong to one action.

When to use it

Use isProtected for any action that needs a signed-in user. Use accessControl to list the roles an action requires, then enforce that list in a hook, because Nile stores the labels but does not check them. Use local hooks when one action needs a shared step, such as loading a policy, before its handler runs.

Complete example

This file configures JWT verification and a global before hook that checks every role an action lists. config.jwtSecret comes from your configuration module and must stay on the server.

import {
  type BeforeActionHandler,
  createAction,
  createNileServer,
  createServices,
} from "@nilejs/nile";
import type { NileContext } from "@nilejs/nile";
import { Err, Ok } from "@nilejs/nile/fp";
import { z } from "zod";

const requireRolesHook: BeforeActionHandler<boolean, string> = (hookContext) => {
  if (!hookContext.action.isProtected) {
    return Ok(true);
  }

  const session = hookContext.nileContext.getSession("rest");
  if (session === undefined) {
    return Err("Sign in to continue");
  }

  const requiredRoles = hookContext.action.accessControl ?? [];
  if (requiredRoles.length === 0) {
    return Ok(true);
  }

  const roles = session.roles;
  const hasEveryRole =
    Array.isArray(roles) &&
    requiredRoles.every((role) => roles.includes(role));

  if (!hasEveryRole) {
    return Err("Access denied");
  }

  return Ok(true);
};

const profileSchema = z.object({
  includeOrganization: z.boolean().optional().default(false),
});

type ProfileInput = z.infer<typeof profileSchema>;

const profileHandler = (
  data: ProfileInput,
  context?: NileContext
) => {
  const session = context?.getSession("rest");
  if (session === undefined) {
    return Err("Sign in to continue");
  }

  return Ok({
    userId: session.userId,
    organizationId: data.includeOrganization ? session.organizationId : null,
  });
};

const profileAction = createAction({
  name: "profile",
  description: "Read the signed-in user profile",
  isProtected: true,
  validation: profileSchema,
  handler: profileHandler,
});

const invoicesAction = createAction({
  name: "invoices",
  description: "List invoices for billing administrators",
  isProtected: true,
  accessControl: ["billing-admin"],
  validation: profileSchema,
  handler: profileHandler,
});

const services = createServices([
  {
    name: "account",
    description: "Account operations",
    actions: [profileAction, invoicesAction],
  },
]);

export const server = await createNileServer({
  serverName: "team-workspace",
  services: services,
  auth: { secret: config.jwtSecret, method: "header" },
  onBeforeActionHandler: requireRolesHook,
  rest: { baseUrl: "/api", allowedOrigins: ["https://app.example.com"] },
});

The account.profile action needs any valid token. The account.invoices action needs a token whose roles claim includes billing-admin. The runnable version of this pattern, with token generation and requests, is in the protected profile example.

With no token, a protected action returns:

{
  "status": false,
  "message": "No JWT token found in header",
  "data": {}
}

A token that is valid but lacks the required role returns Access denied with the same envelope. Both are HTTP 400.

Options that matter

WhereOptionEffect
Serverauth.secretKey used to verify tokens. Required when any action is protected.
Serverauth.method"header" (default) reads Authorization: Bearer. "cookie" reads auth_token by default.
Serverauth.headerName, auth.cookieName, auth.algorithmChange the source header or cookie name, or the algorithm (HS256 by default).
ServeronBeforeActionHandlerGlobal check that runs for every action, after JWT verification.
ActionisProtectedRequires verified JWT. Both auth and isProtected must be set.
ActionaccessControlRole labels. Enforced only by your hook.
Actionhooks.before, hooks.afterLocal hooks that run another action's handler. Set isCritical to decide whether a failure stops the action.

The verifier requires userId (or id or sub) and organizationId (or organization_id or orgId) as non-empty strings. Other claims, such as roles, remain on the request session as getSession("rest").

How it behaves

  • Nile runs JWT verification first for protected actions, then the global before hook, then local before hooks, validation, the handler, local after hooks and the global after hook.
  • The global before hook runs for every action. Return Ok(true) to continue. Its returned value does not replace the payload.
  • A local hook runs its referenced action's handler directly. It does not run that action's JWT check, validation or hooks. Put required checks inside the hook's handler.
  • Unprotected actions skip token verification. Sending a token to them does not populate their session.
  • auth also verifies tokens from HTTP requests for protected actions. Custom routes do not run the action check. Use verifyJWT there, as shown in authentication.

Limits

  • Verification failures return a fixed message: JWT authentication failed: token expired, JWT authentication failed: invalid token, or a short reason for issuer, audience, algorithm or timing problems. The message never contains the token, its header or its claims, so it is safe to log.
  • Role checks only protect the actions that use them. Check resource ownership inside the domain operation.
  • Discovery has its own secret gate. It does not inherit JWT or role checks.

Common mistakes

  • Setting isProtected without an auth block. Protection needs both.
  • Writing accessControl and expecting Nile to enforce it. Your hook must read action.accessControl.
  • Trusting an organizationId in the payload. Compare it with the verified session.
  • Putting the JWT secret in client code or in a committed file.

See also

On this page