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/fastifyPlugin 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" });
});