Skip to the content.

Better Auth Resource Manager

BARM creates authenticated CRUD endpoints and a typed client from a single Zod-based resource definition.

Requirements

Install

pnpm add @p8labs/barm better-auth zod

The package has three public entry points:

import { resourceManager } from "@p8labs/barm";
import { resourceManagerClient } from "@p8labs/barm/client";
import { resource, schema } from "@p8labs/barm/resource";

Define a resource

Resource fields use the BARM schema builder. The builder returns Zod schemas with database metadata attached.

import { resource, schema } from "@p8labs/barm/resource";

const todoSchema = schema
  .object({
    id: schema
      .string()
      .primaryKey()
      .autofill(() => crypto.randomUUID()),
    title: schema.string().index(),
    completed: schema.boolean(),
    userId: schema.string().references("user.id").owner(),
    createdAt: schema.date().autofill(() => new Date()),
    updatedAt: schema.date().autofill(() => new Date(), "createOrUpdate"),
  })
  .table("todo");

export const todo = resource({
  schema: todoSchema,
});

Field metadata

Method Purpose
primaryKey() Marks a unique primary key field.
unique() Adds a unique database constraint.
index() Marks a field for indexing.
references("resource.field") Adds a foreign-key reference.
owner() Fills the field with the authenticated user’s id on create.
autofill(generate) Generates a server-managed value on create.
autofill(generate, "update") Generates a value on update.
autofill(generate, "createOrUpdate") Generates a value on create and update.
table("name") Uses a different database model name.

Supported primitive fields are string, number, boolean, and date.

Server-managed fields are excluded from typed client create and update inputs. The server always overwrites owner fields and autofilled fields, so clients cannot choose those values.

Configure Better Auth

Pass the resources to the server plugin alongside your database adapter:

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { resourceManager } from "@p8labs/barm";
import { todo } from "./resources.js";
import { prisma } from "./prisma.js";

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "sqlite",
  }),
  plugins: [
    resourceManager({
      resources: { todo },
    }),
  ],
});

The database adapter must support the Better Auth adapter operations used by BARM: create, findOne, findMany, update, and delete.

Every generated endpoint uses Better Auth’s sessionMiddleware. Requests therefore need a valid Better Auth session.

Generated HTTP API

For a resource named todo, BARM registers:

Operation Method Path Success
List GET /resource/todo 200
Get GET /resource/todo/:id 200
Create POST /resource/todo 201
Update PATCH /resource/todo/:id 200
Delete DELETE /resource/todo/:id 200

List pagination

Use page and limit query parameters:

GET /resource/todo?page=2&limit=20

page starts at 1. limit is clamped between 1 and 100, with a default of 20. Invalid values fall back to the defaults.

The response shape is:

{
  "data": [{ "id": "todo-1", "title": "Write docs", "completed": false }],
  "pagination": {
    "page": 1,
    "limit": 20,
    "hasMore": false
  }
}

The server reads limit + 1 records to calculate hasMore; the extra record is not returned.

Create

POST /resource/todo
Content-Type: application/json

{ "title": "Write docs", "completed": false }

The response status is 201. Invalid data returns 400 with the Zod issues in the response data. Missing authentication for an owner() field throws an authentication error.

Get, update, and delete

GET    /resource/todo/todo-1
PATCH  /resource/todo/todo-1
DELETE /resource/todo/todo-1

Update accepts a partial resource input:

{ "completed": true }

Missing ids return 400. A missing record on get returns 404.

Typed client

Register the client plugin with the same resource definitions used by the server:

import { createAuthClient } from "better-auth/client";
import { resourceManagerClient } from "@p8labs/barm/client";
import { todo } from "./resources.js";

export const authClient = createAuthClient({
  plugins: [
    resourceManagerClient({
      resources: { todo },
    }),
  ],
});

Use the generated actions:

await authClient.resourceManager.todo.list({ page: 1, limit: 20 });

await authClient.resourceManager.todo.get({
  id: "todo-1",
});

await authClient.resourceManager.todo.create({
  title: "Write docs",
  completed: false,
});

await authClient.resourceManager.todo.update({
  id: "todo-1",
  data: { completed: true },
});

await authClient.resourceManager.todo.delete({
  id: "todo-1",
});

The client uses GET, POST, PATCH, and DELETE to match the generated server endpoints. Its response follows Better Auth’s fetch response shape: successful calls contain data and error: null; failed calls contain data: null and an error object.

Restricting endpoints

Set an operation to false in the resource access configuration to avoid registering that endpoint:

const readOnlyTodo = resource({
  schema: todoSchema,
  access: {
    create: false,
    update: false,
    delete: false,
  },
});

Access defaults to enabled for all five operations. Boolean false disables endpoint registration. Function rules run when a request arrives and can return a boolean or a promise. List and create rules receive { session } plus create data; get, update, and delete rules receive the current record, the session, and update data where applicable. A denied request returns 403. Authentication is still required for every registered endpoint through Better Auth session middleware.

Database schema generation

BARM exposes resource metadata through the Better Auth plugin schema. primaryKey() and unique() map to unique fields, references() maps to a foreign-key reference, and table() controls the model name. Database migration and adapter setup remain the responsibility of the host application.

Development

From the repository root:

pnpm install
pnpm typecheck
pnpm test
pnpm build

The test suite uses Vitest and covers pagination normalization, validation, generated fields, ownership, adapter calls, missing records, pagination boundaries, authorization rules, and endpoint registration.

GitHub Pages

This directory is a standalone GitHub Pages source. In the repository settings, select Pages, choose Deploy from a branch, select the default branch, and set the folder to /docs. GitHub Pages will build docs/index.md using the included Jekyll configuration and custom stylesheet.

For a repository named barm, the published site is:

https://p8labs.github.io/barm/

License

BARM is released under the MIT license.