# How to Implement Data Scoping Using owner_email and org_id in Agent-Native

> Learn how to implement data scoping in Agent Native using owner_email and org_id for robust row-level security. Protect personal and organizational boundaries effectively.

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

---

**Agent-Native implements a dual-key data scoping mechanism using `owner_email` and `org_id` columns to enforce row-level security across personal and organizational data boundaries.**

The BuilderIO/agent-native repository provides a robust multi-tenant architecture that isolates user data through a dual-key scoping pattern. By combining `owner_email` and `org_id` columns on every tenant-scoped table, the system ensures that personal data remains separate from organizational resources while supporting complex sharing scenarios.

## Understanding the Dual-Key Scoping Model

Agent-Native stores every user-owned record with two **scoping columns** that work together to create a composite access key:

- **`owner_email`** – A `text NOT NULL` column containing the email of the authenticated user who created the row. This provides a stable, privacy-preserving identifier that survives organization changes.
- **`org_id`** – A `text` nullable column containing the identifier of the organization the row belongs to, if any. This enables true multi-tenant isolation.

These columns are defined in the database schema located at [`templates/slides/server/db/schema.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/server/db/schema.ts). Together they create a dual-key model that satisfies three distinct data ownership patterns:

- **Personal data** – Rows where `org_id` is `null`.
- **Org-scoped data** – Rows with a non-null `org_id` value.
- **Guest/public data** – Rows owned by a guest account email with `org_id` set to `null`.

## Accessing Request Context

When a Nitro request is handled, the runtime populates a **request context** containing the current user's email and active organization ID. Import these values using the context helpers from `@agent-native/core/server/request-context`:

```typescript
import { getRequestUserEmail, getRequestOrgId } from "@agent-native/core/server/request-context";

```

The `getRequestUserEmail()` function returns the email of the signed-in user or throws an error if the request is unauthenticated. The `getRequestOrgId()` function returns the organization ID resolved from the session, the live organization context, or `null` for guest users.

The context is injected by the `withSlidesRequestContext` and `withPlanRequestContext` helpers defined in [`templates/slides/server/handlers/request-auth-context.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/server/handlers/request-auth-context.ts).

## Writing Data with Scoping Columns

All write actions must explicitly include both scoping columns to prevent orphan rows and ensure proper access control. Here is how the `create-deck` action in [`templates/slides/actions/create-deck.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/create-deck.ts) tags new records:

```typescript
const ownerEmail = getRequestUserEmail();
const orgId = getRequestOrgId();

await db.insert(schema.decks).values({
  title,
  ownerEmail,
  orgId,            // null for personal decks, org id for organization-wide decks
  // ...other fields...
});

```

If the user is not authenticated, `getRequestUserEmail()` throws an error, preventing the creation of unowned data. This pattern ensures that every row carries the complete scoping context necessary for future access control decisions.

## Reading Data with Scoped Queries

Every list and read action must filter results using both scoping predicates to enforce the dual-key isolation. The `list-decks` action in [`templates/slides/actions/list-decks.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/slides/actions/list-decks.ts) demonstrates this pattern using Drizzle ORM's `and()` and `eq()` operators:

```typescript
const ownerEmail = getRequestUserEmail();
const orgId = getRequestOrgId();

const decks = await db.select()
  .from(schema.decks)
  .where(and(
    eq(schema.decks.ownerEmail, ownerEmail),
    eq(schema.decks.orgId, orgId)   // matches null when the user has no org
  ));

```

This query ensures that users only see decks that match both their email and current organization context. When `orgId` is `null`, the query correctly filters for personal data only. To retrieve personal decks specifically, omit the organization filter, though the default implementation always scopes to the current organization context.

## Enforcing Cross-Resource Consistency

When moving resources between containers, Agent-Native verifies that both source and target resources belong to the same organization to prevent data leaks across tenant boundaries. The `move-composition-to-folder` action in [`templates/videos/actions/move-composition-to-folder.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/videos/actions/move-composition-to-folder.ts) implements this guard:

```typescript
if (compositionAccess.resource.orgId &&
    folderAccess.resource.orgId &&
    compositionAccess.resource.orgId !== folderAccess.resource.orgId) {
  throw new Error("Cannot move across org boundaries");
}

```

This validation ensures that cross-resource operations maintain the integrity of the scoping model and prevent accidental exposure of organizational data to unauthorized users.

## Handling Guest and Public Data

Guest-only rows use `null` values for both the organization ID and a specific guest email identifier. The sharing-access matrix in [`templates/plan/server/sharing-access-matrix.spec.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/plan/server/sharing-access-matrix.spec.ts) contains logic that treats a `null` organization ID as "public-within-the-owner-email" scope.

The system includes a null-guard test that ensures guest identities never carry a non-null organization ID. This prevents guest users from accessing or creating organization-scoped data while allowing them to own and share personal resources within their limited access scope.

## Summary

- **Dual-key scoping** relies on `owner_email` and `org_id` columns to isolate data between personal and organizational contexts.
- **Request context helpers** `getRequestUserEmail()` and `getRequestOrgId()` provide the current user's scoping keys for every operation.
- **Write operations** must explicitly include both columns to prevent orphan rows and ensure future queryability.
- **Read queries** use `and(eq(ownerEmail, ...), eq(orgId, ...))` to enforce row-level security at the database level.
- **Cross-resource moves** validate matching `org_id` values to prevent data leakage across organizational boundaries.
- **Guest accounts** operate with `null` organization IDs, limiting them to personal data scopes only.

## Frequently Asked Questions

### What happens if a user is not authenticated when accessing data?

The `getRequestUserEmail()` function throws an error if the request lacks authentication, preventing any data access or modification. This ensures that all database operations occur within a valid user context, eliminating the risk of unscoped data leakage to anonymous users.

### How does Agent-Native handle moving data between different organizations?

The system explicitly blocks cross-organization moves by comparing the `org_id` values of both source and target resources. If the organization IDs differ, the operation throws an error with the message "Cannot move across org boundaries", maintaining strict tenant isolation even during complex data management operations.

### Can a single user own both personal and organization-scoped data simultaneously?

Yes. Users can own personal data where `org_id` is `null` and organization-scoped data where `org_id` matches their active organization. The request context determines which scope is active during a given operation, and queries can be constructed to return data from one scope or both depending on the use case.

### Why does Agent-Native use owner_email instead of a numeric user ID?

The `owner_email` column provides a stable, privacy-preserving identifier that remains constant even if the user's underlying system ID changes or if they move between organizations. This approach ensures that data ownership remains traceable and consistent across organizational changes without exposing internal database identifiers.