HelloJohn / docs
SDKsNode.js SDK

Fastify

Integrate HelloJohn authentication into Fastify — plugin setup, decorators, route-level hooks, and typed request augmentation.

Fastify

Use the HelloJohn Fastify plugin for authentication with Fastify's type-safe, hook-based architecture.


Installation

npm install @hellojohn/node @hellojohn/fastify

Plugin Setup

Register the HelloJohn Fastify plugin:

import Fastify from "fastify";
import { hellojohnPlugin } from "@hellojohn/fastify";

const app = Fastify({ logger: true });

await app.register(hellojohnPlugin, {
  tenantId: process.env.HELLOJOHN_TENANT_ID!,
  secretKey: process.env.HELLOJOHN_SECRET_KEY!,
});

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

The plugin adds request.auth to all requests and provides app.requireAuth() decorator.


Protecting Routes

Route-level protection

app.get(
  "/api/me",
  { preHandler: app.requireAuth() },
  async (request, reply) => {
    const { userId } = request.auth;
    const user = await hj.users.get(userId);
    return user;
  }
);

Schema + Auth

app.post(
  "/api/posts",
  {
    preHandler: app.requireAuth(),
    schema: {
      body: {
        type: "object",
        required: ["title", "content"],
        properties: {
          title: { type: "string" },
          content: { type: "string" },
        },
      },
    },
  },
  async (request, reply) => {
    const post = await db.post.create({
      data: {
        ...request.body,
        authorId: request.auth.userId,
      },
    });
    reply.status(201).send(post);
  }
);

Request Decoration

After plugin registration, request.auth is available on all routes (authenticated or not):

declare module "fastify" {
  interface FastifyRequest {
    auth: {
      userId: string | null;
      sessionId: string | null;
      orgId: string | null;
      orgRole: string | null;
      isAuthenticated: boolean;
    };
  }
}

Unauthenticated requests have userId: null and isAuthenticated: false.


Role-Based Guards

function requireRole(role: string) {
  return async (request: FastifyRequest, reply: FastifyReply) => {
    if (!request.auth.isAuthenticated) {
      return reply.status(401).send({ error: "Unauthorized" });
    }

    if (request.auth.orgRole !== role && request.auth.orgRole !== "owner") {
      return reply.status(403).send({ error: "Forbidden" });
    }
  };
}

app.delete(
  "/api/users/:id",
  { preHandler: [app.requireAuth(), requireRole("admin")] },
  async (request, reply) => {
    await hj.users.delete((request.params as { id: string }).id);
    reply.status(204).send();
  }
);

Global Auth Hook

Apply auth to all routes except explicitly excluded ones:

app.addHook("preHandler", async (request, reply) => {
  const publicPaths = ["/", "/api/health", "/api/webhook"];

  if (publicPaths.includes(request.url)) return;

  if (!request.auth.isAuthenticated) {
    return reply.status(401).send({ error: "Unauthorized" });
  }
});

Error Handling

The plugin throws FastifyError with status 401 for invalid tokens. Handle globally:

app.setErrorHandler((error, request, reply) => {
  if (error.statusCode === 401) {
    return reply.status(401).send({ error: "Unauthorized" });
  }
  reply.status(500).send({ error: "Internal server error" });
});

On this page