How to Implement Sharing and Access Control in Agent-Native Using the `accessFilter`
Agent-Native centralizes access control through the accessFilter helper in @agent-native/core/sharing, which automatically generates SQL WHERE clauses that enforce ownership, organization boundaries, and explicit share permissions across all Drizzle ORM queries.
The Agent-Native framework (BuilderIO/agent-native) provides a type-safe security layer for multi-tenant AI applications. When you implement sharing and access control in Agent-Native using the accessFilter, you eliminate boilerplate authorization logic by composing a single predicate that respects three permission dimensions: resource ownership, workspace membership, and direct shares.
How accessFilter Enforces the Three Permission Dimensions
The accessFilter function—implemented in packages/core/src/sharing.ts—constructs a composite SQL predicate that checks three distinct axes of permission:
- Owner: Validates that the
owner_emailcolumn matches the authenticated user’s email. - Organization: Confirms that the user’s
org_idmatches the resource’s workspace identifier. - Explicit Shares: Joins against a dedicated shares table (e.g.,
folderShares) to discover grants to individual users or groups.
Because the filter returns a raw Drizzle SQL expression, the database engine can utilize composite indexes defined on the shares tables while ensuring no endpoint can accidentally bypass authorization.
The Request Context Flow
Authorization depends on the request context populated by server-side middleware. The runWithRequestContext function (typically invoked in server plugins like server/plugins/agent-chat.ts) validates the session and attaches the following to the request object:
userId: The authenticated user’s primary key.email: The user’s email address for ownership checks.orgId: The active organization/workspace identifier.groupIds: An array of role-based group memberships.
accessFilter reads these values at query time, ensuring that permission checks always reflect the current caller’s identity without manual parameter passing.
Querying Resources with accessFilter
To protect read operations, import accessFilter from the core sharing package and pass it to Drizzle’s .where() clause along with the resource table and its corresponding shares table.
// templates/videos/actions/list-folders.ts
import { accessFilter } from "@agent-native/core/sharing";
import { db } from "@/server/db";
export const listFolders = async (ctx) => {
const { schema } = ctx;
const folders = await db
.select()
.from(schema.folders)
.where(accessFilter(schema.folders, schema.folderShares));
return folders;
};
This pattern ensures that listFolders only returns folders where the caller is the owner, belongs to the same organization, or appears in the folderShares mapping table.
Enforcing Write Permissions with assertAccess
For mutations, use assertAccess to throw an authorization error before executing destructive operations. This function performs the same checks as accessFilter but validates a specific resource ID and required permission level.
// templates/forms/actions/delete-file.ts
import { assertAccess } from "@agent-native/core/sharing";
export const deleteForm = async (ctx, { formId }) => {
const { schema } = ctx;
// Throws if caller lacks 'owner' permission on this specific form
await assertAccess(schema.forms, schema.formShares, formId, "owner");
await db.delete(schema.forms).where({ id: formId });
};
Unlike the read filter, assertAccess is designed for imperative gate-keeping inside action handlers.
Checking Access Conditionally with resolveAccess
When rendering UI elements that depend on permission state (e.g., showing an "Edit" button), use resolveAccess to obtain a boolean without throwing errors.
// templates/design/actions/edit-design.ts
import { resolveAccess } from "@agent-native/core/sharing";
export const canEditDesign = async (ctx, designId) => {
const { schema } = ctx;
return resolveAccess(schema.designs, schema.designShares, designId, "editor");
};
This returns true only if the user holds the specified role (or higher) on the resource, enabling fine-grained conditional rendering.
Defining Share Tables in Your Schema
To enable sharing on a custom resource, define a link table that references both the resource and the user or group being granted access. Below is the pattern used across templates like videos, slides, and forms:
// server/db/schema.ts
import { pgTable, serial, text, integer, primaryKey } from "drizzle-orm/pg-core";
export const myResources = pgTable("my_resources", {
id: serial("id").primaryKey(),
owner_email: text("owner_email").notNull(),
org_id: integer("org_id").notNull(),
// additional columns...
});
export const myResourceShares = pgTable("my_resource_shares", {
resource_id: integer("resource_id")
.notNull()
.references(() => myResources.id),
user_id: integer("user_id").references(() => users.id),
group_id: integer("group_id").references(() => groups.id),
}, (table) => ({
pk: primaryKey({ columns: [table.resource_id, table.user_id, table.group_id] })
}));
After defining the schema, protect queries by passing both tables to accessFilter:
.where(accessFilter(schema.myResources, schema.myResourceShares))
Key Implementation Files
| File | Purpose |
|---|---|
packages/core/src/sharing.ts |
Core implementation of accessFilter, assertAccess, and resolveAccess. |
templates/videos/actions/list-folders.ts |
Example of read-level filtering in the videos template. |
templates/forms/actions/delete-file.ts |
Example of write-level protection using assertAccess. |
templates/design/actions/edit-design.ts |
Example of conditional UI logic with resolveAccess. |
templates/*/server/db/schema.ts |
Defines owner columns and <resource>Shares junction tables. |
templates/*/server/plugins/db.ts |
Contains index definitions optimized for the filter’s WHERE clauses. |
packages/core/docs/content/sharing.mdx |
Official documentation for the sharing API surface. |
Summary
- Agent-Native centralizes authorization logic in
@agent-native/core/sharing, exposingaccessFilterfor reads andassertAccessfor writes. - The system evaluates ownership, organization membership, and explicit shares through a single SQL predicate that composes with Drizzle queries.
- Request context is injected via
runWithRequestContextmiddleware, making user identity available without manual thread-local storage. - Share tables follow a consistent junction-table pattern (e.g.,
folderShares) indexed for fast lookup alongside owner and visibility columns. - You can combine
accessFilterwith additional.where()constraints using Drizzle’sand()operator for complex business logic.
Frequently Asked Questions
What is the difference between accessFilter and assertAccess?
accessFilter returns a SQL expression suitable for Drizzle .where() clauses, filtering result sets to only visible records. assertAccess performs an imperative check on a specific resource ID and throws an authorization error if the user lacks the required permission, making it ideal for guarding mutations.
How does accessFilter handle public or visibility-flagged resources?
The function automatically includes predicates for public visibility flags (e.g., visibility = 'public') when the owner_email and org_id checks fail. As implemented in packages/core/src/sharing.ts, the generated OR clause ensures public resources are appended to the result set without compromising the security of private records.
Can I compose accessFilter with other WHERE clauses in Drizzle?
Yes. Because accessFilter returns a standard Drizzle SQL expression, you can combine it with additional constraints using and() or or() operators. For example: .where(and(accessFilter(schema.folders, schema.folderShares), eq(schema.folders.status, 'active'))).
Where is the request context populated in the Agent-Native stack?
The context is populated by runWithRequestContext middleware, typically invoked in server plugins such as templates/*/server/plugins/agent-chat.ts. This middleware validates the session token, extracts the user’s email, orgId, and group memberships, and binds them to the request object so that accessFilter can access them during query execution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →