A resource manager plugin for Better Auth that gives you CRUD APIs for resources without having to write the same endpoints, validation and database code again and again.
The main idea is simple. You define your resource once using the schema builder and Resource Manager takes care of creating the required endpoints and client-side actions for it.
The project is still in MVP stage, so some things are intentionally kept simple for now.
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");
const todo = resource({
schema: todoSchema,
});
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "sqlite",
}),
plugins: [
resourceManager({
resources: {
todo,
},
}),
],
});From this one resource definition, Resource Manager creates the CRUD endpoints automatically.
The client can then use:
authClient.resourceManager.todo.list();
authClient.resourceManager.todo.get({
id,
});
authClient.resourceManager.todo.create({
title: "Hello",
completed: false,
});
authClient.resourceManager.todo.update({
id,
data: {
completed: true,
},
});
authClient.resourceManager.todo.delete({
id,
});The client API is typed from the same resource definition, so fields that are managed by the server are not expected from the client.
There are currently two special things in the schema: owner() and autofill().
autofill() tells Resource Manager that the value should be generated by the server.
For example:
id: schema
.string()
.primaryKey()
.autofill(() => crypto.randomUUID()),The client does not need to provide id.
For dates:
createdAt: schema
.date()
.autofill(() => new Date()),
updatedAt: schema
.date()
.autofill(() => new Date(), "createOrUpdate"),createdAt is generated when creating the record, while updatedAt is generated when creating and updating the record.
The available autofill modes are:
"create";
"update";
"createOrUpdate";owner() is used for fields which belong to the currently authenticated user.
For the current MVP, ownership is resolved from the Better Auth session.
For example:
userId: schema
.string()
.references("user.id")
.owner(),When a record is created, the server automatically uses:
ctx.context.session.user.id;The client does not provide or control this value.
This is intentionally simple for the MVP. Right now the project has resources which we define ourselves, while some tables such as Better Auth's user table are still generated and managed by Better Auth.
Because of that, ownership is currently based on the authenticated Better Auth user.
Later, when all relevant tables are defined through Resource Manager itself, ownership can be made more generic and can support owners other than the current user.
The schema builder is based on Zod with some additional metadata for Resource Manager.
For example:
schema.string();
schema.number();
schema.boolean();
schema.date();Fields can also have metadata such as:
.primaryKey()
.unique()
.index()
.references("user.id")
.owner()
.autofill(() => ...)The idea is that the schema should eventually become the main source of truth for the resource.
The client plugin takes the same resource definitions so TypeScript can understand which resources are available.
For example:
resourceManagerClient({
resources: {
todo,
},
});The client then exposes:
authClient.resourceManager.todo;with the CRUD methods automatically available.
The input and output types are inferred from the resource schema.
Server-managed fields such as owner() and autofill() are excluded from client create and update inputs.
So this is valid:
await authClient.resourceManager.todo.create({
title: "Hello",
completed: false,
});while fields such as id, userId, createdAt and updatedAt are handled by the server.
This project is currently an MVP.
The core CRUD flow is working on both server and client side. Resource schemas can generate the database schema information, CRUD endpoints are generated automatically, authentication is handled through Better Auth session middleware, and client-side types are inferred from the resource definitions.
Some parts are intentionally not fully generalized yet.
In particular, Better Auth still provides some tables such as user, while Resource Manager manages the resources that we explicitly define. Because of this, some relationships and ownership rules are currently kept simple.
The plan is to improve this later instead of making the MVP unnecessarily complicated.
For now the goal is straightforward: define a resource once, get the CRUD API and typed client for it, and let the server handle the fields that should not be controlled by the client.
Good Night!