Authorization scopes
A scope is an RSQL filter, derived from the current user, that Covara AND-combines with every query for a resource. Scopes are enforced on reads, writes, subscriptions, count, aggregate, and search — server-side, so a client can never widen its own access.
import { useResource, rsql } from "covara";
useResource(postsTable, {
id: postsTable.id,
db,
auth: {
public: { read: true }, // anonymous read
update: async (user) => rsql`authorId==${user.id}`, // own posts only
delete: async (user) =>
user.metadata?.role === "admin" ? rsql`*` : rsql`authorId==${user.id}`,
subscribe: async (user) => rsql`authorId==${user.id}`, // live stream scope
},
});
| Return | Meaning |
|---|---|
rsql`*` | Allow all rows for this operation. |
rsql`<expr>` | Allow only rows matching the expression. |
rsql`` (empty) | Deny — no rows. |
Operations: read, create, update, delete, subscribe. Omit one to deny it (unless public grants it).
Anonymous access — public. public: true makes read and subscribe public (writes still require auth — a safe default). The object form opts each operation in explicitly and can open writes too: public: { read: true, subscribe: true, create: true, update: true, delete: true } for a fully-public resource (e.g. a prototype or a genuinely open collection). With the object form, subscribe is not implied by read — grant it explicitly or anonymous SSE subscriptions return 401. An operation not granted by public and reached without a user returns 401.
Scope patterns
Common cases are presets:
import { scopePatterns } from "covara";
auth: scopePatterns.ownerOnly("userId"),
auth: scopePatterns.publicReadOwnerWrite("userId"),
auth: scopePatterns.ownerOrAdmin("userId", (user) => user.metadata?.role === "admin"),
auth: scopePatterns.orgBased("organizationId"),
auth: scopePatterns.authenticatedFullAccess(), // any signed-in user, full access
auth: scopePatterns.fullyPublic(), // every op public, incl. anonymous writes — demos only
fullyPublic()opts every operation in (read, subscribe, create, update, delete) for unauthenticated callers — it's thecovara createstarter default so the app works end-to-end; lock it down before production. For public reads with authenticated writes, usepublicReadOwnerWrite.
Building scopes programmatically
The rsql template helper interpolates values safely. Or compose with builders. See RSQL for the full builder reference (escaping rules, every helper, sub-scope composition, and the special allScope()/emptyScope()).
import { rsql, eq, ne, gt, gte, lt, lte, inList, notIn, like, notLike, isNull, isNotNull, and, or } from "covara";
const a = eq("userId", user.id);
const b = and(eq("status", "active"), eq("organizationId", user.orgId));
const c = or(eq("userId", user.id), eq("public", true));
const d = like("email", "%@example.com"); // emits %=
const e = notLike("email", "%@spam.com"); // emits !%=
// equivalent template form:
const scope = rsql`userId==${user.id};status=="active"`;
The filter grammar has no NOT combinator, so there is no not() helper. Use the negated operators instead: ne (!=), notIn (=out=), notLike (!%=), isNotNull (=isnull=false).
RSQL template helper
import { rsql } from "covara";
auth: {
update: async (user) => rsql`userId==${user.id}`,
delete: async (user) => rsql`userId==${user.id}`,
}
Interpolated values are escaped, so user IDs and other dynamic values are injected safely.
How enforcement works
Every resource endpoint routes through the secure query builder, which resolves the scope for the operation and combines it with the request filter before touching the database. Subscriptions resolve the subscribe scope at connect and match it in-memory against the changelog, so the live stream never leaks rows outside scope. See the auth contract for the guarantee.