Skip to main content

Introduction

The Better Skills API is built with tRPC, providing end-to-end type safety from the server to your TypeScript/JavaScript clients. All API procedures are defined in the centralized AppRouter and exposed through a Hono server.

Architecture

Core Packages

  • @better-skills/api - Shared tRPC router and context
  • apps/server - Hono backend that hosts the tRPC endpoints
  • @better-skills/auth - Better Auth integration

Transport Layer

The tRPC API is served via Hono at the /trpc/* endpoint:
import { trpcServer } from "@hono/trpc-server";
import { appRouter, createContext } from "@better-skills/api";

app.use(
  "/trpc/*",
  trpcServer({
    router: appRouter,
    createContext: (_opts, context) => {
      return createContext({ context });
    },
  }),
);

AppRouter Structure

The main application router is defined in packages/api/src/routers/index.ts:
export const appRouter = router({
  // Health check endpoint
  healthCheck: publicProcedure.query(() => {
    return "OK";
  }),

  // Get current user session
  me: protectedProcedure.query(({ ctx }) => {
    return {
      user: {
        id: ctx.session.user.id,
        name: ctx.session.user.name,
        email: ctx.session.user.email,
      },
    };
  }),

  // Skills management router
  skills: skillsRouter,

  // Vaults management router
  vaults: vaultsRouter,

  // Onboarding status
  hasActivated: protectedProcedure.query(async ({ ctx }) => { /* ... */ }),

  // Complete onboarding
  completeOnboarding: protectedProcedure.mutation(async ({ ctx }) => { /* ... */ }),
});

export type AppRouter = typeof appRouter;

Procedure Types

The API defines three types of procedures:

Public Procedures

Public procedures are accessible without authentication:
export const publicProcedure = t.procedure;
Example:
healthCheck: publicProcedure.query(() => {
  return "OK";
})

Protected Procedures

Protected procedures require a valid session:
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({
      code: "UNAUTHORIZED",
      message: "Authentication required",
      cause: "No session",
    });
  }
  return next({
    ctx: {
      ...ctx,
      session: ctx.session,
    },
  });
});

Admin API Procedures

Admin procedures require a token via the x-better-skills-admin-token header:
export const adminApiProcedure = t.procedure.use(({ ctx, next }) => {
  const configuredToken = process.env.BETTER_SKILLS_ADMIN_TOKEN;
  if (!configuredToken || configuredToken.trim().length === 0) {
    throw new TRPCError({
      code: "INTERNAL_SERVER_ERROR",
      message: "Admin API token is not configured",
    });
  }

  const providedToken = ctx.requestHeaders.get("x-better-skills-admin-token");
  if (!providedToken || providedToken !== configuredToken) {
    throw new TRPCError({
      code: "UNAUTHORIZED",
      message: "Invalid admin API token",
    });
  }

  return next({
    ctx: {
      ...ctx,
      adminApiToken: providedToken,
    },
  });
});

Client Setup

TypeScript Client

Import the AppRouter type for full type safety:
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "@better-skills/api";

const trpc = createTRPCClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "http://localhost:3000/trpc",
      headers: async () => {
        // Include session cookies or auth headers
        return {};
      },
    }),
  ],
});

// Fully typed API calls
const result = await trpc.healthCheck.query();
const user = await trpc.me.query();

JavaScript Client

For JavaScript projects without TypeScript:
import { createTRPCClient, httpBatchLink } from "@trpc/client";

const trpc = createTRPCClient({
  links: [
    httpBatchLink({
      url: "http://localhost:3000/trpc",
      fetch: (url, options) => {
        return fetch(url, {
          ...options,
          credentials: "include", // Include cookies for session auth
        });
      },
    }),
  ],
});

// API calls (no type checking)
const result = await trpc.healthCheck.query();
const user = await trpc.me.query();

Base URL Configuration

The API base URL varies by environment:
  • Development: http://localhost:3000
  • Production: Set via SERVER_URL env variable
Web app (apps/web) uses:
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL;
CLI (apps/cli) uses:
const serverUrl = env.SERVER_URL; // defaults to http://localhost:3000

Main Routers

The AppRouter is organized into feature-based sub-routers:
  • skills - Skills CRUD, search, graph, and mention management
  • vaults - Vault memberships, invitations, and enterprise vault management
See the Skills Router and Vaults Router pages for detailed procedure documentation.

Error Handling

tRPC uses typed error codes. Common errors:
  • UNAUTHORIZED - Missing or invalid session/token
  • FORBIDDEN - Insufficient permissions
  • NOT_FOUND - Resource does not exist
  • BAD_REQUEST - Invalid input or request
  • INTERNAL_SERVER_ERROR - Unexpected server error
Example error handling:
try {
  const skill = await trpc.skills.getById.query({ id: "skill-id" });
} catch (error) {
  if (error.data?.code === "UNAUTHORIZED") {
    console.error("Not authenticated");
  } else if (error.data?.code === "NOT_FOUND") {
    console.error("Skill not found");
  }
}

CORS Configuration

The server uses credential-based CORS:
app.use(
  "/*",
  cors({
    origin: allowedOrigins,
    allowMethods: ["GET", "POST", "OPTIONS"],
    allowHeaders: ["Content-Type", "Authorization", "x-better-skills-admin-token"],
    credentials: true,
  }),
);
Allowed origins are configured via the CORS_ORIGIN environment variable.

Build docs developers (and LLMs) love