fastify-xauth-nile
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:
| URL | Role |
|---|---|
GET /api/auth/csrf | CSRF token |
GET /api/auth/session | Session |
POST /api/auth/signin | Credentials sign-in |
GET /api/me | Session principal |
/admin/* | Superadmin (access: "platform") |
/portal/* | Signed-in user (access: "session") |
/affiliates/* | Session + tenant id (access: "tenant") |
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
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
configs | XAuthNileConfig[] | — | Yes | Non-empty array of instance configs |
Instance config
Plugin-owned (not passed to Nile()):
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
name | string | — | Yes | Unique instance name |
apiUrl | string (URL) | — | Yes | nile-auth API base URL |
user | string | — | Yes | Nile database user |
password | string | — | Yes | Nile database password |
databaseName | string | — | Yes | Nile database name |
basePath | string | unset (mount all) | No | Filter mounted SDK paths (e.g. /api/auth for auth-only) |
prefix | string | — | No | Session-only shorthand. Prefer protectedPaths |
excludedPaths | array | [] | No | Extra skips for guards — string, RegExp, or { url, methods } |
protectedPaths | array | [] | No | { prefix, access } with platform, session, or tenant |
superAdmins | string[] | [] | If any path is platform | User ids and/or emails |
environment | string | — | No | If secureCookies is omitted, development/dev → insecure cookies |
verifyTenant | boolean | false | No | Opt-in membership via nile.tenants.get on requireTenant/access: "tenant" (403 on mismatch) |
forwardHeaders | string[] | [] | No | Extra headers forwarded to nile-auth (blocked headers stay blocked) |
sdkTimeout | number (ms) | unset | No | 504 on stalled nile-auth calls for forwarded routes / requireAuth |
tenantHeader | string | x-tenant-id | No | Header after URL param, before cookie |
forwardHeaders | string[] | — | No | Extra request headers forwarded to nile-auth beyond the built-in allowlist |
sdkTimeout | number (ms) | — | No | Abandon a stalled nile-auth call after this many ms (replies 504) |
cookiePath | string | — | No | Set-Cookie Path rewrite/append, e.g. cookiePath: "/" when guarding routes outside routePrefix |
stripLocationHeader | boolean | false | No | When true, strips the location header from forwarded Nile responses |
verifyTenant | boolean | false | No | Also verify session user's membership of resolved tenant (403 on mismatch) |
tenantId | string | — | No | Fallback tenant for withContext / requireTenant(). Not passed to Nile() |
userId | string | — | No | Not passed to Nile(). Prefer the request session |
Passed through to the Nile SDK:
| Name | Type | Default | Description |
|---|---|---|---|
routePrefix | string | /api | SDK prefix ({routePrefix}/auth/csrf, {routePrefix}/me) |
origin | string (URL) | — | Frontend origin as nile-origin |
callbackUrl | string | — | Override client callback URL |
secureCookies | boolean | true | Use NODE_ENV === "development" ? false : true |
debug | boolean | false | Verbose SDK logging |
databaseId | string | from apiUrl | Nile database id |
db | object | — | pg pool config |
headers | object | Headers | — | Extra nile-auth headers |
extensions | array | — | SDK extensions |
routes | object | — | Override generated nile-auth paths only |
logger | function | — | Custom SDK logger |
skipHostHeader | boolean | — | Skip Host on nile-auth fetches |
Methods
fastify.xAuthNile
| Property | Description |
|---|---|
get(name) | Named instance, or undefined |
default | First configured instance |
configs | Record<name, instance> |
Instance API
- requireAuth() — 401 unless a valid session; sets
request.auth/request.user - requireTenant() — 400 unless a tenant id resolves; sets
request.tenantId - requireSuperAdmin() — 403 unless the session is on
superAdmins - getSession(request) — session without middleware
- signIn(request, creds) — credentials (or provider) sign-in
- withContext(request, fn) — request-scoped SDK (
useLastContext: false) - Protect path surfaces —
/admin,/portal,/affiliates - applyResponseCookies(reply, response) — helper for
rawResponse: trueset-cookiepassthrough
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)— constructsnew Server(config)createAuthMiddleware(nile, options)— session middleware factorycreateTenantMiddleware(nile, options)— tenant middleware factory with optionalverifyTenant: truecreateSuperAdminMiddleware(options)— platform guard factorycreateAuthApi(nile, config)— standalone instance auth helpersapplyResponseCookies(reply, response, options)— copies set-cookie headers with optionalcookiePathrewriterewriteCookiePath(cookie, path)— pure helper to rewrite/append Set-CookiePathshouldForwardResponseHeader(name, options)— response header filter with optionalstripLocationisSuperAdmin,toFastifyPath,collectInstanceRoutes,contextFromRequest,resolveTenantId
Request properties
| Property | Set by | Description |
|---|---|---|
request.auth | requireAuth() / session guard | Full Nile session |
request.user | requireAuth() / session guard | session.user |
request.tenantId | requireTenant() / access: "tenant" | Resolved tenant id — format by default; membership checked when verifyTenant is true |
request.authAccess | protectedPaths match | platform | session | tenant |
request.isSuperAdmin | platform / 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:
- URL param
tenantId tenantHeader(defaultx-tenant-id)nile.tenant-idcookie- 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:
| Status | When |
|---|---|
401 | No/invalid session |
403 | Session is not a superadmin, or tenant membership check failed (verifyTenant: true) |
400 | No tenant id |
404 | No Nile handler for the method/path |
502 | Unexpected upstream Nile SDK error (sanitised body, details logged server-side) |
504 | Nile SDK timeout exceeded (sdkTimeout) |
Middleware logs error.message only.
Environment variables
The plugin never reads process.env. Pass values from your config layer:
| Variable | Passed in as |
|---|---|
NILEDB_API_URL | configs[].apiUrl |
NILEDB_USER | configs[].user |
NILEDB_PASSWORD | configs[].password |
NILEDB_NAME | configs[].databaseName |
NILEDB_ID | configs[].databaseId |
NILE_ORIGIN | configs[].origin |
NILE_SUPERADMINS | configs[].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
