Resources & the app factory
A resource is the core of Covara. Each resource maps to a Drizzle table and generates a complete Hono router: CRUD, batch operations, count, aggregations, subscriptions, search, and RPC. You create resources either with the createCovara factory or directly with useResource.
createCovara
createCovara(options) returns a CovaraApp (which extends Hono) pre-wired with RFC 7807 error handling, security headers, health endpoints, OpenAPI, and an optional admin UI. Resources are added with the chainable .resource().
import { createCovara } from "covara";
const app = createCovara({
basePath: "/api", // resource mount prefix (default: "/api")
cors: true, // true | false | hono/cors options
auth: { router, middleware },// the object returned by useAuth()
middleware: [], // extra Hono MiddlewareHandler[] applied to all routes
observability: true, // request/subscription metrics (or { metrics })
health: true, // /healthz + /readyz (default: true)
adminUI: false, // admin dashboard at /__covara/ui (default: false)
openapi: true, // OpenAPI document at /__covara (default: true)
})
.resource(usersTable, { id: usersTable.id, db }) // → /api/users
.resource("/people", usersTable, { id: usersTable.id, db }); // → /api/people
| Option | Type | Default | Description |
|---|---|---|---|
basePath | string | "/api" | Prefix every resource is mounted under. |
cors | boolean | CorsOptions | false | Enable CORS, or pass a hono/cors config. |
auth | { router, middleware } | — | Result of useAuth; mounts auth routes and populates c.get("user"). |
middleware | MiddlewareHandler[] | [] | Extra middleware applied before resources. |
observability | boolean | { metrics } | false | Collect request/subscription metrics. |
health | boolean | HealthOptions | true | Mount /healthz and /readyz. |
adminUI | boolean | AdminUIOptions | false | Mount the admin dashboard. |
openapi | boolean | OpenAPIOptions | true | Serve the OpenAPI spec. |
The mount path is inferred from the table name unless you pass an explicit path as the first argument to .resource().
Serving the app
- Node:
await startServer(app, { port })fromcovara/node— see Node deployment. - Cloudflare Workers:
export default app(orapp.fetch) — see Workers deployment.
useResource
useResource(table, config) returns a plain Hono router. Use it when you want to compose routing yourself:
import { Hono } from "hono";
import { useResource, errorHandler, notFoundHandler } from "covara";
const app = new Hono();
app.onError(errorHandler);
app.notFound(notFoundHandler);
app.route("/api/posts", useResource(postsTable, { id: postsTable.id, db }));
createCovara().resource() is a thin wrapper over useResource that also registers the resource for OpenAPI, the admin UI, and cross-resource subscriptions.
Configuration reference
id (required)
The primary key column.
{ id: postsTable.id }
db (required)
The Drizzle database instance. Wrap it with trackMutations if you want custom routes/procedures to feed subscriptions automatically.
{ db }
auth
Authorization scopes per operation. Each scope is a function of the current user returning an RSQL filter that is AND-combined with the request filter. See Authorization scopes.
{
auth: {
public: { read: true, subscribe: true }, // allow anonymous read/subscribe
read: async (user) => rsql`*`,
create: async (user) => (user ? rsql`*` : rsql``),
update: async (user) => rsql`userId==${user.id}`,
delete: async (user) => rsql`userId==${user.id}`,
subscribe: async (user) => rsql`userId==${user.id}`,
},
}
A scope function's return value decides which rows the operation may touch: rsql`*` allows everything, rsql`userId==${user.id}` restricts to matching rows (AND-combined with the request ?filter=), and rsql `` (empty) denies entirely. Omit a scope to deny that operation for everyone except where public grants it.
public vs a scope returning rsql`*`
These look similar but act at different stages — public controls who (authenticated or not), while a scope function controls which rows an already-authenticated user sees:
publicis checked first and bypasses the auth requirement, so an anonymous request is allowed and resolves to all rows. Onlyreadandsubscribecan be made public (public: trueis shorthand for both) — you cannot opencreate/update/deletethis way.- A scope function returning
rsql`*`is only reached after the user check. An anonymous request gets a401(the function never runs); an authenticated user gets all rows. It means "any logged-in user can read everything, but you must be logged in."
| Config | Anonymous | Authenticated |
|---|---|---|
public: { read: true } | ✅ all rows | ✅ all rows |
scope read returns * | ❌ 401 | ✅ all rows |
| both together | ✅ all rows | ✅ all rows |
| neither (omitted) | ❌ 401 | ❌ 403 |
For the common "public reads, owner-only writes" pattern you need both — set public.read and owner-scoped create/update/delete functions (this is exactly scopePatterns.publicReadOwnerWrite). See Authorization scopes.
capabilities
Enable/disable whole operations. All default to true. Reading, filtering, and sorting are always enabled.
{
capabilities: {
enableCreate: true,
enableUpdate: true, // governs PATCH and PUT
enableDelete: true,
enableBatch: true, // governs all /batch routes incl. /batch/upsert
enableAggregations: true,
enableSubscribe: true,
},
}
fields
Field-level policies. See Fields: masking, writable, computed for full behavior and security guarantees. Every column-name option below takes the Drizzle column (like id); string column names also work but are deprecated.
{
fields: {
readable: [users.id, users.name, users.email, users.createdAt], // allowlist; everything else stripped from responses
writable: [users.name, users.email], // enforced allowlist for create/update (mass-assignment protection)
filterable: [users.name, users.email, users.createdAt], // columns allowed in ?filter=
sortable: [users.name, users.createdAt], // columns allowed in ?orderBy=
},
}
generatedFields
Columns the server fills in (id, timestamps, ownership). They are exempt from fields.writable stripping (a hook can set them) and may be omitted from inbound bodies even with strictInput.
{ generatedFields: [users.id, users.userId, users.createdAt, users.updatedAt] }
strictInput
By default unknown fields in a body are ignored. Set strictInput: true to reject them with a 422 (Zod strict mode).
{ strictInput: true }
computed
Virtual fields added to every response and subscription event, computed from the full (unmasked) row and never persisted. See Fields.
{
computed: {
fullName: (row) => `${row.firstName} ${row.lastName}`,
isOverdue: (row) => row.dueAt != null && Date.parse(row.dueAt) < Date.now(),
},
}
pagination
{ pagination: { defaultLimit: 20, maxLimit: 100 } }
See Pagination.
batch
{ batch: { create: 50, update: 50, replace: 50, delete: 10 } }
See Batch operations.
etag
Optimistic concurrency control. See Optimistic locking.
{ etag: { versionField: posts.version } } // or { updatedAtField: posts.updatedAt }
softDelete
Mark rows deleted instead of removing them. See Soft delete.
{ softDelete: { field: posts.deletedAt } }
relations
Declare belongsTo / hasOne / hasMany / manyToMany relations loadable via ?include=. See Relations.
nestedWrites
Enable write-through of embedded relation objects in POST bodies (requires relations). See Nested writes.
{ nestedWrites: true }
search
Register searchable fields for the GET /search endpoint. See Search.
{ search: { enabled: true, fields: { title: { weight: 2 }, body: {} } } }
rateLimit
Per-resource rate limiting. See Middleware.
{ rateLimit: { windowMs: 60_000, maxRequests: 100 } }
customOperators
Add filter operators. See Filtering → Custom operators.
hooks
Lifecycle hooks around every mutation. See Procedures & hooks.
{
hooks: {
onBeforeCreate: async (ctx, data) => ({ ...data, createdAt: new Date() }),
onAfterCreate: async (ctx, created) => {},
onBeforeUpdate: async (ctx, id, data) => ({ ...data, updatedAt: new Date() }),
onAfterUpdate: async (ctx, updated) => {},
onBeforeDelete: async (ctx, id) => {},
onAfterDelete: async (ctx, deleted) => {},
},
}
procedures
Custom Zod-validated RPC endpoints at POST /rpc/:name. See Procedures.
Generated endpoints
| Method | Path | Description |
|---|---|---|
GET | / | List with pagination, filtering, ordering, projections |
GET | /:id | Get one |
POST | / | Create |
PATCH | /:id | Partial update |
PUT | /:id | Full replace |
DELETE | /:id | Delete (or soft delete) |
GET | /count | Count |
GET | /aggregate | Aggregations |
GET | /aggregate/subscribe | Live aggregation |
GET | /subscribe | SSE subscription |
GET | /search | Full-text search (when configured) |
POST | /batch | Batch create |
PATCH | /batch | Batch update |
DELETE | /batch | Batch delete |
POST | /batch/upsert | Bulk insert-or-update |
POST | /rpc/:name | RPC procedures |
Query parameters
| Parameter | Description | Example |
|---|---|---|
filter | Filter expression | status=="active" |
select | Field projection | id,name,email |
include | Load relations | category,tags(limit:5) |
cursor | Pagination cursor | eyJpZCI6MTB9 |
limit | Page size | 20 |
orderBy | Sort order | name:asc,age:desc |
totalCount | Include total count | true |
having | Filter aggregate groups (/aggregate) | count>=5;sum_total>100 |
withDeleted | Include soft-deleted rows | true |