Backend JSRuntime GuidesNode.js
Fartify
End-to-end Fastify integration with @wacht/backend auth guards and typed backend operations.
Use this setup to run a Fastify admin service with a reusable guard pipeline and typed backend access, covering preHandler auth verification, permission enforcement, backend API calls, and explicit auth/error handling.
Project structure
Fartify Service
src/
server.ts
plugins/
wacht-auth.ts
routes/
organizations.ts
1. Server bootstrap
import Fastify from 'fastify';
import { initClient } from '@wacht/backend';
import { organizationsRoutes } from './routes/organizations';
import { wachtAuthPlugin } from './plugins/wacht-auth';
initClient({
apiKey: process.env.WACHT_API_KEY!,
baseUrl: process.env.WACHT_BACKEND_API_URL,
});
const app = Fastify({ logger: true });
await app.register(wachtAuthPlugin);
await app.register(organizationsRoutes, { prefix: '/admin/organizations' });
await app.listen({ port: 3000, host: '0.0.0.0' });2. Auth plugin (request decoration + guard)
import fp from 'fastify-plugin';
import { getAuthFromToken, type WachtAuth, WachtAuthError } from '@wacht/backend';
declare module 'fastify' {
interface FastifyRequest {
wachtAuth?: WachtAuth;
}
interface FastifyInstance {
requireWachtPermission: (permission?: string | string[]) => unknown;
}
}
function extractToken(header?: string): string | null {
if (!header || !header.startsWith('Bearer ')) return null;
const token = header.slice(7).trim();
return token.length > 0 ? token : null;
}
export const wachtAuthPlugin = fp(async (app) => {
app.decorate('requireWachtPermission', (permission?: string | string[]) => {
return async (request: any, reply: any) => {
try {
const token = extractToken(request.headers.authorization);
const auth = await getAuthFromToken(token);
await auth.protect({ permission });
request.wachtAuth = auth;
} catch (error) {
if (error instanceof WachtAuthError) {
return reply.status(error.status).send({
error: error.code,
message: error.message,
redirect_url: error.redirectUrl ?? null,
});
}
return reply.status(500).send({ error: 'internal_error' });
}
};
});
});3. Protected routes
import type { FastifyPluginAsync } from 'fastify';
import { organizations } from '@wacht/backend';
export const organizationsRoutes: FastifyPluginAsync = async (app) => {
app.get(
'/',
{ preHandler: app.requireWachtPermission('organization:read') },
async () => organizations.listOrganizations({ limit: 20 }),
);
app.patch(
'/:organizationId',
{ preHandler: app.requireWachtPermission('organization:update') },
async (request: any) => {
return organizations.updateOrganization(request.params.organizationId, request.body);
},
);
};4. API key/OAuth protected auth for machine tokens
import type { FastifyPluginAsync } from 'fastify';
import { gateway, organizations } from '@wacht/backend';
export const machineOrganizationRoutes: FastifyPluginAsync = async (app) => {
app.get('/machine/organizations', async (request, reply) => {
const apiKey = request.headers['x-api-key'];
const decision = await gateway.checkPrincipalAuthz({
principalType: 'api_key',
principalValue: typeof apiKey === 'string' ? apiKey : '',
resource: '/machine/organizations',
method: 'GET',
requiredPermissions: ['organization:read'],
});
if (!decision.allowed) {
return reply.status(403).send({ error: 'forbidden' });
}
return organizations.listOrganizations({ limit: 20 });
});
};Best practices
- Use
preHandlerfor guards so handlers stay business-only. - Decorate request with normalized auth state once.
- Keep one shared SDK client initialized for the process.
- In Node.js, publishable key options can be omitted when env vars are configured.
- Use API key/OAuth protected checks for machine-token endpoints.
- Custom
gatewayUrlhost override is available on Enterprise plans. - Keep auth failures structured and machine-readable.