Documentation
Build withFramework quickstartsFastify

Fastify

A plugin that decorates the request, and hooks that gate the route.

Fastify prefers decorators and hooks over middleware, so the integration is a small plugin rather than a pile of functions. The Node page explains what it calls underneath.

Install

npm install @nulvless/nulvless-js fastify fastify-plugin

The plugin

fastify-plugin stops Fastify from encapsulating the decorators, so routes registered elsewhere can still see them.

plugins/nulvless.ts
import fp from 'fastify-plugin';
import { createServerClient } from '@nulvless/nulvless-js';
import type { FastifyReply, FastifyRequest } from 'fastify';

export default fp(async (app) => {
  const nulvless = createServerClient(
    process.env.NULVLESS_URL!,
    process.env.NULVLESS_SECRET!,
  );

  app.decorate('nulvless', nulvless);
  app.decorateRequest('userId', null);
  app.decorateRequest('session', null);

  app.decorate('requireAuth', async (request: FastifyRequest, reply: FastifyReply) => {
    const token = request.headers.authorization?.replace('Bearer ', '');
    if (!token) return reply.code(401).send();

    const { data, error } = await nulvless.auth.verify(undefined, {
      headers: { Authorization: `Bearer ${token}` },
    });

    if (error) return reply.code(401).send();

    request.userId = data.user.id;
    request.session = data.session;
  });

  app.decorate('requireRole', (roles: string[]) =>
    async (request: FastifyRequest, reply: FastifyReply) => {
      const token = request.headers.authorization?.replace('Bearer ', '');

      const { error } = await nulvless.auth.verify(
        { resource: (request.params as { org: string }).org, roles },
        { headers: { Authorization: `Bearer ${token}` } },
      );

      if (error) return reply.code(403).send();
    },
  );
});

A hook that sends a reply ends the request — returning reply.code(401).send() is how you stop the handler from running.

Declare the decorators

types/fastify.d.ts
import type { NulvlessServerClient, Session } from '@nulvless/nulvless-js';
import type { FastifyReply, FastifyRequest } from 'fastify';

declare module 'fastify' {
  interface FastifyInstance {
    nulvless: NulvlessServerClient;
    requireAuth: (request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
    requireRole: (
      roles: string[],
    ) => (request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
  }

  interface FastifyRequest {
    userId: string | null;
    session: Session | null;
  }
}

Gate the routes

Attach the hook per route rather than globally, so public routes stay public.

server.ts
import Fastify from 'fastify';
import nulvlessPlugin from './plugins/nulvless';

const app = Fastify();
await app.register(nulvlessPlugin);

app.get('/me', { preHandler: app.requireAuth }, async (request) => {
  return { user: request.userId };
});

app.delete(
  '/orgs/:org/keys/:id',
  { preHandler: app.requireRole(['owner', 'admin']) },
  async (request, reply) => {
    reply.code(204).send();
  },
);

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

Act on the user's behalf

Pass request.raw

A FastifyRequest is not a Node IncomingMessage — it wraps one. Give forwardUserHeaders the raw request or the headers will not be found.

import { forwardUserHeaders } from '@nulvless/nulvless-js';

app.post('/orgs/:org/invites', { preHandler: app.requireAuth }, async (request, reply) => {
  const body = request.body as { email: string; role_id: string };

  const { data, error } = await app.nulvless.auth.grants.invite(
    { invites: [body], resource: (request.params as { org: string }).org },
    { headers: forwardUserHeaders(request.raw) },
  );

  if (error) return reply.code(400).send({ error });
  return data;
});

Require a fresh confirmation

app.delete('/keys/:id', { preHandler: app.requireAuth }, async (request, reply) => {
  const { error } = await app.nulvless.auth.sessions.revoke({
    id: (request.params as { id: string }).id,
    nonce: request.headers['nlv-sudo-nonce'] as string,
  });

  if (error) return reply.code(403).send({ error });
  reply.code(204).send();
});

See Sudo mode.

Next

On this page