Skip to content

Authentication

Authentication in Covenant happens in the contextGenerator — a function that runs on every request and returns per-request context available to all procedures.

With NextAuth v5:

import { auth } from "@/auth"; // your NextAuth config
import { CovenantServer } from "@covenant-rpc/server";
import { emptyServerToSidekick } from "@covenant-rpc/server/interfaces/empty";
import { covenant } from "./covenant";
export const server = new CovenantServer(covenant, {
contextGenerator: async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
return { user: session?.user ?? null };
},
derivation: ({ ctx, error }) => ({
requireUser: () => {
if (!ctx.user) error("Unauthorized", 401);
return ctx.user!;
},
}),
sidekickConnection: emptyServerToSidekick(),
});
server.defineProcedure("getMyProfile", {
resources: ({ ctx }) => ctx.user ? [`user/${ctx.user.id}`] : [],
procedure: ({ derived }) => {
const user = derived.requireUser(); // throws 401 if not logged in
return db.getUserProfile(user.id);
},
});

requireUser() uses the error() function, which throws immediately and never returns — TypeScript correctly narrows the type after the call.

For API clients using bearer tokens:

contextGenerator: async ({ request }) => {
const authorization = request.headers.get("Authorization");
const token = authorization?.replace("Bearer ", "");
if (!token) return { userId: null };
const userId = await verifyJwt(token);
return { userId };
},

Pass the token from the client:

export const client = new CovenantClient(covenant, {
serverConnection: httpClientToServer("/api/covenant", {
Authorization: `Bearer ${token}`,
}),
sidekickConnection: emptyClientToSidekick(),
});

Extend the derivation pattern for roles:

derivation: ({ ctx, error }) => ({
requireUser: () => {
if (!ctx.user) error("Unauthorized", 401);
return ctx.user!;
},
requireAdmin: () => {
if (!ctx.user) error("Unauthorized", 401);
if (ctx.user.role !== "admin") error("Forbidden", 403);
return ctx.user!;
},
}),