X Enterprises

fastify-xauth-nile

Fastify 5 plugin for Nile Auth — SDK default /api/auth routes, session and tenant helpers, and /admin /portal path guards.

fastify-xauth-nile

Fastify 5 plugin for Nile Auth on @niledatabase/server. It mounts the generated nile-auth routes (same surface as @niledatabase/express), wraps each request in nile.withContext, and guards app prefixes such as /admin and /portal. Pair it with nuxt-x-auth-nile for the frontend.

The plugin never reads process.env. You pass console credentials into register().

1.5.0. cookiePath Set-Cookie Path rewrite; stripLocationHeader to suppress upstream redirects; unexpected Nile SDK errors sanitized to 502 (no URL/credential leak). SHA a45d337 / release b40f033.

Installation

npm install @xenterprises/fastify-xauth-nile fastify@5

Quick Start

import Fastify from "fastify";
import xAuthNile from "@xenterprises/fastify-xauth-nile";

const fastify = Fastify();

const superAdmins = (process.env.NILE_SUPERADMINS || "")
  .split(",")
  .map((s) => s.trim())
  .filter(Boolean);

await fastify.register(xAuthNile, {
  configs: [
    {
      name: "api",
      apiUrl: process.env.NILEDB_API_URL,
      user: process.env.NILEDB_USER,
      password: process.env.NILEDB_PASSWORD,
      databaseName: process.env.NILEDB_NAME,
      origin: process.env.NILE_ORIGIN,
      secureCookies: process.env.NODE_ENV === "development" ? false : true,
      superAdmins,
      protectedPaths: [
        ...(superAdmins.length ? [{ prefix: "/admin", access: "platform" }] : []),
        { prefix: "/portal", access: "session" },
        { prefix: "/affiliates", access: "tenant" },
      ],
      excludedPaths: ["/portal/health"],
    },
  ],
});

const auth = fastify.xAuthNile.default;

fastify.get("/portal/home", async (request) => ({ user: request.user }));

await fastify.listen({ port: 3000 });

Omit routePrefix so the SDK default /api applies:

URLRole
GET /api/auth/csrfCSRF token
GET /api/auth/sessionSession
POST /api/auth/signinCredentials sign-in
GET /api/meSession principal
/admin/*Superadmin (access: "platform")
/portal/*Signed-in user (access: "session")
/affiliates/*Session + tenant id (access: "tenant")
Do not set routePrefix to /auth — CSRF becomes /auth/auth/csrf. Do not put /admin in SDK routes (SIGNIN, CSRF, ME). That object remaps generated nile-auth paths only. App surfaces go in protectedPaths.

Options

Plugin options

NameTypeDefaultRequiredDescription
configsXAuthNileConfig[]—YesNon-empty array of instance configs

Instance config

Plugin-owned (not passed to Nile()):

NameTypeDefaultRequiredDescription
namestring—YesUnique instance name
apiUrlstring (URL)—Yesnile-auth API base URL
userstring—YesNile database user
passwordstring—YesNile database password
databaseNamestring—YesNile database name
basePathstringunset (mount all)NoFilter mounted SDK paths (e.g. /api/auth for auth-only)
prefixstring—NoSession-only shorthand. Prefer protectedPaths
excludedPathsarray[]NoExtra skips for guards — string, RegExp, or { url, methods }
protectedPathsarray[]No{ prefix, access } with platform, session, or tenant
superAdminsstring[][]If any path is platformUser ids and/or emails
environmentstring—NoIf secureCookies is omitted, development/dev → insecure cookies
verifyTenantbooleanfalseNoOpt-in membership via nile.tenants.get on requireTenant/access: "tenant" (403 on mismatch)
forwardHeadersstring[][]NoExtra headers forwarded to nile-auth (blocked headers stay blocked)
sdkTimeoutnumber (ms)unsetNo504 on stalled nile-auth calls for forwarded routes / requireAuth
tenantHeaderstringx-tenant-idNoHeader after URL param, before cookie
forwardHeadersstring[]—NoExtra request headers forwarded to nile-auth beyond the built-in allowlist
sdkTimeoutnumber (ms)—NoAbandon a stalled nile-auth call after this many ms (replies 504)
cookiePathstring—NoSet-Cookie Path rewrite/append, e.g. cookiePath: "/" when guarding routes outside routePrefix
stripLocationHeaderbooleanfalseNoWhen true, strips the location header from forwarded Nile responses
verifyTenantbooleanfalseNoAlso verify session user's membership of resolved tenant (403 on mismatch)
tenantIdstring—NoFallback tenant for withContext / requireTenant(). Not passed to Nile()
userIdstring—NoNot passed to Nile(). Prefer the request session

Passed through to the Nile SDK:

NameTypeDefaultDescription
routePrefixstring/apiSDK prefix ({routePrefix}/auth/csrf, {routePrefix}/me)
originstring (URL)—Frontend origin as nile-origin
callbackUrlstring—Override client callback URL
secureCookiesbooleantrueUse NODE_ENV === "development" ? false : true
debugbooleanfalseVerbose SDK logging
databaseIdstringfrom apiUrlNile database id
dbobject—pg pool config
headersobject | Headers—Extra nile-auth headers
extensionsarray—SDK extensions
routesobject—Override generated nile-auth paths only
loggerfunction—Custom SDK logger
skipHostHeaderboolean—Skip Host on nile-auth fetches

Methods

fastify.xAuthNile

PropertyDescription
get(name)Named instance, or undefined
defaultFirst configured instance
configsRecord<name, instance>

Instance API

Also on each instance: getCsrf, listProviders, signOut, signUp, forgotPassword, resetPassword, callback, mfa, refreshSession, getMe, and instance.nile (nile.auth, nile.users, nile.tenants, nile.query, nile.db).

Named Exports

Available from @xenterprises/fastify-xauth-nile:

  • createNileService(config) — constructs new Server(config)
  • createAuthMiddleware(nile, options) — session middleware factory
  • createTenantMiddleware(nile, options) — tenant middleware factory with optional verifyTenant: true
  • createSuperAdminMiddleware(options) — platform guard factory
  • createAuthApi(nile, config) — standalone instance auth helpers
  • applyResponseCookies(reply, response, options) — copies set-cookie headers with optional cookiePath rewrite
  • rewriteCookiePath(cookie, path) — pure helper to rewrite/append Set-Cookie Path
  • shouldForwardResponseHeader(name, options) — response header filter with optional stripLocation
  • isSuperAdmin, toFastifyPath, collectInstanceRoutes, contextFromRequest, resolveTenantId

Request properties

PropertySet byDescription
request.authrequireAuth() / session guardFull Nile session
request.userrequireAuth() / session guardsession.user
request.tenantIdrequireTenant() / access: "tenant"Resolved tenant id — format by default; membership checked when verifyTenant is true
request.authAccessprotectedPaths matchplatform | session | tenant
request.isSuperAdminplatform / requireSuperAdmin()true after allowlist check

Routes

Every path in nile.paths (plus extras on nile.routes omitted from paths: MFA, verify-email, invites, user-tenants) is registered as GET/POST/PUT/PATCH/DELETE and forwarded to nile.handlers[METHOD] inside nile.withContext. {param} becomes Fastify :param. Status, headers, and set-cookie (via getSetCookie()) pass through.

Default routePrefix /api matches the SDK routes table. Set basePath: "/api/auth" to mount auth-only (no /api/me, /api/signup, /api/tenants).

Mounted Nile routes skip protectedPaths so CSRF stays public.

Signup verification email

Nile POST /api/signup creates the user and often replies 400 Email verification is required for sign in. Signup itself does not hit SMTP.

This plugin then POSTs /api/auth/verify-email (CSRF + form body, callbackUrl → {origin}/auth/login?verified=1) so SendGrid in the Nile console sends once. When origin is set, a foreign nile.callback-url cookie is ignored. The Nuxt layer must not POST verify-email again on signup. The verify click confirms the address only — no session cookie. SMTP is never @xenterprises/fastify-xemail; the plugin never reads process.env and never holds SMTP credentials.

If nile.routes is missing on the SDK Server, extras (verify-email, MFA, OAuth callback/:provider, …) are inferred from nile.paths. Without that, credential sign-in can 404 on GET /api/auth/callback/credentials.

Tenant context

requireTenant() / withContext resolve tenant id in this order:

  1. URL param tenantId
  2. tenantHeader (default x-tenant-id)
  3. nile.tenant-id cookie
  4. Optional static config.tenantId (request-scoped only)

Values must match ^[A-Za-z0-9_-]{1,128}$.

Error reference

Registration throws xauthnile: errors (option name + example). Runtime middleware:

StatusWhen
401No/invalid session
403Session is not a superadmin, or tenant membership check failed (verifyTenant: true)
400No tenant id
404No Nile handler for the method/path
502Unexpected upstream Nile SDK error (sanitised body, details logged server-side)
504Nile SDK timeout exceeded (sdkTimeout)

Middleware logs error.message only.

Environment variables

The plugin never reads process.env. Pass values from your config layer:

VariablePassed in as
NILEDB_API_URLconfigs[].apiUrl
NILEDB_USERconfigs[].user
NILEDB_PASSWORDconfigs[].password
NILEDB_NAMEconfigs[].databaseName
NILEDB_IDconfigs[].databaseId
NILE_ORIGINconfigs[].origin
NILE_SUPERADMINSconfigs[].superAdmins (comma-separated; trim)

How it works

Each config is validated and merged with defaults, then new Server(config) is constructed (not the Nile() process-wide singleton). SDK plugin keys are stripped before the constructor. Fastify routes are collected from nile.paths. protectedPaths register an onRequest hook with boundary matching (/admin does not match /administrator). Pooled connections are released on Fastify onClose.

AI Context

package: "@xenterprises/fastify-xauth-nile"
version: "1.5.0"
type: fastify-plugin
use-when: Nile Auth on Fastify — cookie sessions, /api/auth/csrf, /api/me, signup then POST verify-email for SMTP, /admin and /portal guards, tenant-aware Postgres
decorator: fastify.xAuthNile (get, default, configs)
request-decorators: request.auth, request.user, request.tenantId, request.authAccess, request.isSuperAdmin
routePrefix: /api (SDK default) — do not use /auth or /nile
protectedPaths: /admin platform, /portal session, /affiliates tenant
sdk-routes: remaps SIGNIN/CSRF/ME only — not app guards
opts: cookiePath, stripLocationHeader, verifyTenant, forwardHeaders, sdkTimeout, applyResponseCookies
env: consumer-set only — plugin never reads process.env
frontend: @xenterprises/nuxt-x-auth-nile
Copyright © 2026