# How to Implement Organization Management in Agent-Native Using the orgmembers Tables

> Implement organization management in Agent-Native using orgmembers tables. Create organizations, manage members via email, and secure resources with database-level access control.

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

---

**Agent-Native implements organization management through two core SQL tables—`organizations` and `orgmembers`—that enforce access control at the database level, enabling you to create organizations, manage membership via email-based roles, and secure resources through runtime context resolution.**

The Agent-Native framework from BuilderIO treats every workspace as a set of SQL tables encoding the security model. By leveraging the `organizations` table alongside the `orgmembers` junction table, you can implement robust multi-tenant access control without hard-coding user IDs into your application logic.

## Understanding the Organization Schema

### The organizations Table

The `organizations` table stores the core entity definition for each workspace. According to the test specifications driving the sharing logic 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), this table includes fields such as `id`, `name`, and `created_by` to establish the organization boundary.

### The orgmembers Junction Table

The `orgmembers` table serves as the single source of truth for membership relationships. It links a user’s email address to a specific organization through the columns `org_id`, `member_email`, and `role`. All security checks join against this table to determine whether a caller belongs to an organization and what permissions they hold.

## Implementation Workflow

Organization management follows a five-step flow where all checks are performed **at the SQL level**:

1. **Create the Organization**: Insert a row into the `organizations` table to establish the workspace boundary.
2. **Add Members**: Populate `orgmembers` with rows mapping user emails to the organization and assigning roles (owner, admin, or member).
3. **Resolve Access**: Query `orgmembers` via the `resolvePlanAccessContext` logic to determine if the caller is an owner, admin, or viewer.
4. **Enforce Policies**: Validate active organization membership using `requireActiveOrg()` before executing mutations.
5. **Share Resources**: Store an `org_id` on resources via `registerShareableResource`, making them visible to all members when visibility is set to `org`.

## Code Implementation Examples

### Creating an Organization

Insert the initial organization record into the schema:

```typescript
import { sql } from "drizzle-orm";

export async function createOrganization(db, { id, name, createdBy }) {
  await db.run(sql`
    INSERT INTO organizations (id, name, created_by, created_at)
    VALUES (${id}, ${name}, ${createdBy}, ${Date.now()})
  `);
}

```

### Adding Members to orgmembers

Assign users to organizations with specific roles:

```typescript
import { sql } from "drizzle-orm";

export async function addOrgMember(db, { orgId, email, role = "member" }) {
  await db.run(sql`
    INSERT INTO orgmembers (org_id, member_email, role)
    VALUES (${orgId}, ${email}, ${role})
  `);
}

```

### Listing Organization Members

The `list-org-members` action defined in [`templates/mail/actions/list-org-members.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/mail/actions/list-org-members.ts) provides the standard API for retrieving membership data:

```typescript
import { sql } from "drizzle-orm";

export const listOrgMembers = defineAction({
  name: "list-org-members",
  input: z.object({ organizationId: z.string() }),
  output: z.array(z.object({
    email: z.string(),
    role: z.enum(["owner", "admin", "member"]),
  })),
  async resolve({ input, db }) {
    const rows = await db.all(sql`
      SELECT member_email AS email, role
      FROM orgmembers
      WHERE org_id = ${input.organizationId}
    `);
    return rows;
  },
});

```

### Enforcing Membership Guards

Before mutating resources, actions like `queue-email-draft` validate membership via `requireActiveOrg()`:

```typescript
async function requireActiveOrg(context) {
  if (!context.orgId) throw new Error("An active organization is required.");
  const member = await db.one(sql`
    SELECT 1 FROM orgmembers
    WHERE org_id = ${context.orgId} AND member_email = ${context.userEmail}
  `);
  if (!member) throw new Error("Only members of this organization can queue drafts.");
}

```

## Access Resolution and Security

### Runtime Role Resolution

When any resource (such as a Plan) is requested, the `resolvePlanAccessContext` function in [`templates/plan/actions/get-plan-access-status.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/plan/actions/get-plan-access-status.ts) joins the `orgmembers` table to resolve whether the caller is an **owner**, **admin**, or **viewer**. This resolution happens automatically when `process.env.ORGANIZATION_ID` or the request context is populated.

### Organization-Aware Resource Creation

Actions that generate organization-scoped content, such as `create-visual-recap` in [`templates/plan/actions/create-visual-recap.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/plan/actions/create-visual-recap.ts), first verify the caller has an active organization entry before creating visual answers. This pattern ensures that resources are always created within the proper organizational boundary.

## Key Source Files and Their Roles

- **[`templates/plan/server/sharing-access-matrix.spec.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/plan/server/sharing-access-matrix.spec.ts)**: Defines the `organizations` table schema and registers the sharing subsystem via `registerShareableResource`.
- **[`templates/mail/actions/list-org-members.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/mail/actions/list-org-members.ts)**: Public action that reads `orgmembers` to expose membership data to other skills.
- **[`templates/plan/actions/get-plan-access-status.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/plan/actions/get-plan-access-status.ts)**: Contains the `resolvePlanAccessContext` logic that joins `orgmembers` to determine caller permissions.
- **[`templates/mail/actions/queue-email-draft.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/mail/actions/queue-email-draft.ts)**: Demonstrates the `requireActiveOrg()` guard pattern for enforcing membership before mutations.
- **[`templates/plan/actions/create-visual-recap.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/plan/actions/create-visual-recap.ts)**: Illustrates organization-scoped resource creation requiring valid `orgmembers` entries.

## Summary

- The **`orgmembers`** table acts as the single source of truth for organization membership, linking user emails to organizations via `org_id`, `member_email`, and `role` columns.
- Access control is enforced **at the SQL level** by joining `orgmembers` in queries rather than filtering in application code, preventing hard-coded user ID dependencies.
- The **`list-org-members`** action in [`templates/mail/actions/list-org-members.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/mail/actions/list-org-members.ts) provides the standard API for retrieving membership rosters.
- **Guards** such as `requireActiveOrg()` ensure mutations only execute when the caller has a valid entry in `orgmembers` for the current organization context.
- Resources become visible to all organization members when the sharing visibility is set to `org`, leveraging the `org_id` stored in resource records to scope queries automatically.

## Frequently Asked Questions

### What is the difference between the organizations and orgmembers tables?

The `organizations` table stores the entity itself with metadata like `name` and `created_by`, while `orgmembers` is a junction table that links user emails to specific organizations with assigned roles. All membership validation queries join against `orgmembers` to verify a user belongs to a specific organization before granting access.

### How does Agent-Native validate organization membership at runtime?

The runtime automatically scopes queries using `process.env.ORGANIZATION_ID` or the request context. Action handlers explicitly call validation logic—such as `requireActiveOrg()` in [`templates/mail/actions/queue-email-draft.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/mail/actions/queue-email-draft.ts)—which executes a SQL `SELECT` against `orgmembers` to confirm the caller's `userEmail` matches the current `orgId`.

### Can custom roles be added beyond owner, admin, and member?

The schema supports arbitrary role strings in the `orgmembers.role` column. While the `list-org-members` action specifically enumerates "owner", "admin", and "member" in its Zod schema, you can extend the validation logic in [`templates/plan/actions/get-plan-access-status.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/plan/actions/get-plan-access-status.ts) to recognize additional custom roles for your specific access control requirements.

### Where is the organization access context resolved for Plan resources?

Access resolution occurs in [`templates/plan/actions/get-plan-access-status.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/plan/actions/get-plan-access-status.ts), where the `resolvePlanAccessContext` function performs SQL joins against the `orgmembers` table to determine the caller's role level (owner, admin, or viewer) before returning the Plan resource data.