OpenWork Den Control Plane Architecture for Team and Organization Management

The OpenWork Den control plane uses a three-layer architecture—MySQL persistence with Drizzle ORM, a Hono-based REST API, and a React admin UI—to securely manage multi-tenant organizations, teams, members, and roles.

OpenWork Den is the backend control plane that stores and enforces all multi-tenant data for an OpenWork deployment, including organizations, teams, members, invitations, roles, and related policies. Its architecture is deliberately split into three distinct layers that work together to provide secure, scalable team and organization management. This article examines each layer, the key data models, and the business rules enforced throughout the system.

Three-Layer Architecture Overview

The OpenWork Den control plane architecture consists of persistence, API service, and web UI layers. Each layer has a clearly defined responsibility and communicates through well-defined interfaces.

Persistence Layer (den-db)

The persistence layer stores the canonical state of all control plane entities using a MySQL database. The schema lives in the den-db package and is built with Drizzle ORM.

Key tables include:

  • organization – org metadata, slug, branding, email-domain allow-list, and desktop-app restrictions
  • team – team definitions with organization-scoped slugs
  • member – links users to organizations with optional team assignment
  • invitation – pending invites with tokens, roles, expiry, and optional team_id
  • organization_role – custom roles with JSON permission definitions
  • install_link – one-time tokens for desktop client organization installation

The full schema source is available in [src/schema/org.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/schema/org.ts), which defines OrganizationTable, MemberTable, InvitationTable, and related structures.

API Service Layer (den-api)

The API service exposes HTTP endpoints that the desktop app, web UI, and MCP clients consume. Implemented as a Hono server inside the den-api package, this layer validates Org-scoped permissions, translates requests into Drizzle queries, and returns JSON responses.

Business rules enforced at this layer include:

  • Only org admins can create or delete teams
  • Invitations must belong to the same org and may target a specific team
  • Desktop hand-off and connect-grant tables enforce short-lived tokens for secure desktop hand-off

The Docker image for this service is built by docker/den-api.

Web UI Layer (den-web)

The Web UI provides the admin console (/admin) where organization owners manage teams, members, roles, and branding. This React + TanStack Query application consumes the same TypeScript types returned by the API layer and surfaces policies like desktop_app_restrictions.

UI code lives under apps/den-web, with components triggering the API endpoints described above.

Team Data Model

Teams are a first-class entity attached to organizations. The team table in [src/schema/teams.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/schema/teams.ts) stores organization-scoped team definitions:

export const TeamTable = mysqlTable(
  "team",
  {
    id: denTypeIdColumn("team", "id").notNull().primaryKey(),
    organizationId: denTypeIdColumn("organization", "organization_id").notNull(),
    name: varchar("name", { length: 255 }).notNull(),
    slug: varchar("slug", { length: 255 }).notNull(),
    metadata: json("metadata").$type<Record<string, unknown> | null>(),
    createdAt: timestamp("created_at", { fsp: 3 }).notNull().defaultNow(),
    updatedAt: timestamp("updated_at", { fsp: 3 })
      .notNull()
      .default(sql`CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)`),
  },
  (table) => [
    uniqueIndex("team_slug_org").on(table.organizationId, table.slug),
  ],
);

Key design choices:

  • organizationId foreign key ensures teams are strictly scoped to one organization
  • slug combined with organizationId forms a unique index for URL-friendly team identifiers
  • metadata JSON field allows extensible custom attributes without schema migrations

Members optionally reference a team_id, and invitations can carry a team_id so new members automatically join the specified team upon acceptance.

Business Rules and Policy Enforcement

The API layer enforces critical security and consistency rules for team and organization management.

Rule Enforcement Location
Org-admin role required for team CRUD operations src/api/team.ts – validates member.role against organization_role
Team-scoped invitations auto-assign on acceptance src/api/invitation.ts – validates team.organization_id matches invitation's organization_id
Desktop hand-off tokens expire after short TTL src/schema/desktop-handoff-grant.ts – API validates expires_at field
Custom permissions interpreted by policy engine src/policy/role-policy.ts – parses JSON from organization_role.permission

Practical Code Examples

Creating an Organization

import { db } from "@/den-db/src/client";
import { organization } from "@/den-db/src/schema/org";

// Insert an org with a slug and default desktop restrictions
await db.insert(organization).values({
  id: "org_01",                 // generated Den TypeID
  name: "Acme Corp",
  slug: "acme",
  allowedEmailDomains: ["acme.com"],
  desktopAppRestrictions: {}, // empty JSON = no extra restrictions
});

Adding a Team via API

// POST /v1/orgs/:orgSlug/teams
await fetch(`${BASE_URL}/v1/orgs/acme/teams`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
  body: JSON.stringify({ name: "Engineering", slug: "engineering" }),
});

Inviting a User to a Specific Team

await fetch(`${BASE_URL}/v1/orgs/acme/invitations`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
  body: JSON.stringify({
    email: "bob@acme.com",
    role: "member",
    teamId: "team_02",          // optional – attaches invite to team
    expiresAt: "2027-01-01T00:00:00Z",
  }),
});

Fetching Teams in the Frontend

const { data: teams } = useQuery(["org", orgSlug, "teams"], () =>
  fetch(`${BASE_URL}/v1/orgs/${orgSlug}/teams`).then((r) => r.json())
);

Key Source Files

Component Path
Core schema (organizations, members, invitations) [ee/packages/den-db/src/schema/org.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/schema/org.ts)
Team table definition [ee/packages/den-db/src/schema/teams.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/schema/teams.ts)
Desktop hand-off grant table [ee/packages/den-db/src/schema/desktop-handoff-grant.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/schema/desktop-handoff-grant.ts)
Drizzle client setup [ee/packages/den-db/src/client.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/client.ts)
Organization API routes ee/apps/den-api/src/routes/org.ts
Team API routes ee/apps/den-api/src/routes/team.ts
Admin UI components apps/den-web/src/pages/admin/
Helm deployment chart packaging/helm/openwork-ee/README.md

Summary

  • Three-layer architecture separates concerns: Drizzle ORM persistence, Hono REST API, and React admin UI
  • Strict organization scoping ensures multi-tenant isolation through foreign keys and composite unique indexes
  • Teams as first-class entities with slugs, metadata, and automatic member assignment via invitations
  • Policy enforcement happens at the API layer, checking roles and permissions before any database mutation
  • Type-safe end-to-end implementation uses shared TypeScript types across database, API, and frontend

Frequently Asked Questions

How does OpenWork Den ensure teams remain isolated between organizations?

The team table enforces isolation through a composite unique index on (organizationId, slug) and foreign key constraints. Every team row is explicitly tied to one organization, and the API layer validates that all operations are performed by members of that same organization according to [src/schema/teams.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/schema/teams.ts).

What database migrations are used for the control plane schema?

The schema uses Drizzle ORM with its migration system defined in the den-db package. Migrations are generated from the TypeScript schema files and applied to the MySQL database. The Drizzle client configuration is located in [src/client.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/client.ts).

Can custom roles define organization-specific permissions?

Yes. The organization_role table stores custom role definitions with a flexible permission JSON column. The policy engine in src/policy/role-policy.ts parses this JSON to make authorization decisions, allowing organizations to define granular access controls beyond the default admin/member roles.

How are desktop clients securely connected to organizations?

Desktop hand-off uses short-lived tokens stored in the desktop_handoff_grant table as defined in [src/schema/desktop-handoff-grant.ts](https://github.com/different-ai/openwork/blob/dev/ee/packages/den-db/src/schema/desktop-handoff-grant.ts). These tokens are scoped to an organization, expire quickly, and are consumed once to establish the desktop client's session.

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 →