# Implementing Sharing and Access Control with ownableColumns in agent-native

> Learn to implement sharing and access control in agent-native using ownableColumns. Effortlessly manage data access with accessFilter and assertAccess for secure applications.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: how-to-guide
- Published: 2026-06-27

---

**To implement sharing and access control in agent-native, spread `ownableColumns()` into your Drizzle table definition and pair it with `createSharesTable()` to generate a companion shares table, then use `accessFilter()` for reads and `assertAccess()` for writes.**

The **BuilderIO/agent-native** framework standardizes resource ownership through a centralized sharing architecture. By implementing sharing and access control with ownableColumns in agent-native, you can convert any database table into an ownable resource that supports fine-grained grants, organizational visibility, and role-based permissions without custom access logic.

## Database Schema Setup with ownableColumns

The sharing system builds on two factory functions exported from `@agent-native/core/sharing` that establish the database schema.

### Adding Ownership Columns to Resources

The `ownableColumns()` factory, defined in [`packages/core/src/sharing/schema.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/sharing/schema.ts), adds three critical columns to your Drizzle table definition:

- **`owner_email`**: Stores the email of the resource creator as the source of truth for true ownership
- **`org_id`**: Links the resource to an organization for team-level visibility
- **`visibility`**: Controls public access levels (typically `private`, `org`, or `public`)

Spread the result of `ownableColumns()` into your table definition to make the resource ownable:

```typescript
import { table, text, ownableColumns } from '@agent-native/core/db/schema';

export const decks = table('decks', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  data: text('data').notNull(),
  // Spread ownable columns into the table definition
  ...ownableColumns(),
});

```

### Creating the Shares Table

The `createSharesTable(name)` factory generates a companion table that records per-principal grants. This table stores:

- **`resource_id`**: Foreign key referencing the parent ownable resource
- **`principal_type`**: Either `user` or `org`
- **`principal_id`**: The identifier of the user or organization
- **`role`**: The granted permission level (`viewer`, `editor`, or `admin`)
- **`created_by`** and **`created_at`**: Audit metadata

```typescript
import { createSharesTable } from '@agent-native/core/db/schema';

export const deckShares = createSharesTable('deck_shares');

```

The shares table references the parent resource via `resource_id`, enabling the `accessFilter` helper to construct `EXISTS` sub-queries that verify grants without leaving the SQL context.

## Access Control Flow and Role Hierarchy

The agent-native runtime enforces permissions through a consistent flow that handles both write-time ownership injection and read-time access filtering.

### Write-Time Owner Injection

When creating resources, the framework automatically populates ownership fields from the request context. Define your creation actions to accept the authenticated user's email and selected organization ID:

```typescript
export const createDeck = defineAction('create-deck', async (ctx, input) => {
  const { email, orgId } = ctx.requestContext;
  return db.insert(decks).values({
    id: nanoid(),
    title: input.title,
    data: input.data,
    owner_email: email,
    org_id: orgId,
    visibility: 'private',
  });
});

```

### Read-Time Permission Filtering

For queries, use the `accessFilter(parentTable, sharesTable, ...)` helper exported from `@agent-native/core/sharing`. This function returns a Drizzle `WHERE` clause that filters rows based on:

- Direct ownership (`owner_email` matches requester)
- Organizational visibility (`org_id` matches and `visibility` equals `org`)
- Public visibility (`visibility` equals `public`)
- Explicit share grants (where shares table contains a qualifying row)

Internally, `accessFilter` constructs an `EXISTS` sub-query against the shares table, keeping the operation as a single SQL statement for performance.

```typescript
import { accessFilter } from '@agent-native/core/sharing';

export const listDecks = defineAction('list-decks', async (ctx) => {
  const where = accessFilter(decks, deckShares);
  return db.select().from(decks).where(where);
});

```

### Role Hierarchy and Validation

Permission levels follow a numeric hierarchy defined in [`packages/core/src/sharing/schema.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/sharing/schema.ts) as `ROLE_RANK`:

| Role | Rank |
|------|------|
| viewer | 1 |
| editor | 2 |
| admin | 3 |
| owner | 4 |

The `roleSatisfies(actual, minimum)` function compares these ranks to determine if a user holds adequate permissions. For mutations, use `assertAccess()` to enforce requirements before execution:

```typescript
import { assertAccess } from '@agent-native/core/sharing';

export const updateDeck = defineAction('update-deck', async (ctx, { deckId, data }) => {
  await assertAccess(decks, deckShares, deckId, ctx, 'admin');
  return db.update(decks).set(data).where(eq(decks.id, deckId));
});

```

Additional helpers like `resolveAccess()` and `currentAccess()` enable the UI layer to conditionally render controls based on the resolved permission level.

## Practical Implementation Examples

The following patterns demonstrate the complete lifecycle of an ownable resource, from schema definition to sharing.

### Defining an Ownable Resource and Shares

Combine the schema factories to establish both the parent table and its shares companion:

```typescript
import {
  table,
  text,
  now,
  ownableColumns,
  createSharesTable,
} from '@agent-native/core/db/schema';

export const decks = table('decks', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  data: text('data').notNull(),
  ...ownableColumns(),
});

export const deckShares = createSharesTable('deck_shares');

```

### Querying with Access Control

Always apply `accessFilter` to list operations to ensure users only see resources they own, share, or have organizational access to:

```typescript
import { accessFilter } from '@agent-native/core/sharing';
import { decks, deckShares } from '@/db/schema';

export const listDecks = defineAction('list-decks', async (ctx) => {
  const where = accessFilter(decks, deckShares);
  return db.select().from(decks).where(where);
});

```

### Granting Share Permissions

To share a resource, verify the current user has `admin` or higher access, then insert a row into the shares table:

```typescript
import { assertAccess } from '@agent-native/core/sharing';

export const shareDeck = defineAction('share-deck', async (ctx, { deckId, principal, role }) => {
  await assertAccess(decks, deckShares, deckId, ctx, 'admin');
  
  return db.insert(deckShares).values({
    id: nanoid(),
    resource_id: deckId,
    principal_type: principal.type,
    principal_id: principal.id,
    role,
    created_by: ctx.requestContext.email,
    created_at: now(),
  });
});

```

## Summary

Implementing sharing and access control with ownableColumns in agent-native relies on a consistent pattern across the database, action, and UI layers:

- **Spread `ownableColumns()`** into any Drizzle table to add `owner_email`, `org_id`, and `visibility` columns
- **Generate a shares table** using `createSharesTable()` to store role-based grants for users and organizations
- **Filter reads** with `accessFilter()`, which constructs an `EXISTS` sub-query combining ownership, visibility, and share checks
- **Validate writes** with `assertAccess()` or `resolveAccess()` using the numeric `ROLE_RANK` hierarchy (viewer=1 to owner=4)
- **Reference [`packages/core/src/sharing/schema.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/sharing/schema.ts)** for all type definitions, role ranks, and factory implementations

## Frequently Asked Questions

### What columns does the ownableColumns factory add to a table?

According to [`packages/core/src/sharing/schema.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/sharing/schema.ts), `ownableColumns()` adds three columns: `owner_email` (text, not null) which stores the true owner's email address, `org_id` (text) linking the resource to an organization, and `visibility` (text) controlling access levels such as `private`, `org`, or `public`.

### How does accessFilter determine which rows a user can see?

The `accessFilter` helper constructs a Drizzle `WHERE` clause that returns rows where the user is the owner, the resource is visible to their organization, the resource is public, or an entry exists in the shares table granting at least the required role. It implements this check as an SQL `EXISTS` sub-query against the shares table to maintain single-statement performance.

### What is the difference between assertAccess and resolveAccess?

`assertAccess()` throws an authorization error if the caller lacks the minimum required role, making it ideal for mutation actions that need to fail fast. `resolveAccess()` returns the resolved role string (or null) without throwing, allowing UI logic to conditionally render edit controls or share management interfaces based on the user's actual permissions.

### Can I use createSharesTable with any Drizzle table definition?

Yes, `createSharesTable(name)` is designed to work with any table that includes the columns from `ownableColumns()`. The factory creates a standard Drizzle table definition referencing a parent resource via `resource_id`, meaning it functions across all SQL dialects supported by Drizzle and integrates with the registry system defined in [`packages/core/src/sharing/registry.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/sharing/registry.ts).