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

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, 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:

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:

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 provides the standard API for retrieving membership data:

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():

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 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, 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

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 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—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 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, 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →