# OpenWork Best Practices: Local-First Development Guide for AI Workflows

> Master OpenWork best practices for local-first AI workflows. Learn to use isolated dev profiles, MCP tools, shadcn/ui, strict TypeScript, and client-side flags for efficient development. Build better with OpenWork.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: best-practices
- Published: 2026-08-02

---

**The best practices for developing with OpenWork include working local-first with isolated dev profiles, exposing all capabilities through MCP tools with owner-scoped security, building UI components with shadcn/ui, maintaining strict TypeScript typing without `any`, and gating experimental features behind client-side flags.**

OpenWork is a modular, **local-first, cloud-ready** platform for building AI-driven workflows, developed by the team at `different-ai/openwork`. Its architecture separates concerns across desktop client, server runtime, and infrastructure layers, enforcing strict security boundaries and type safety throughout the stack.

## Understand the Three-Layer Architecture

OpenWork organizes functionality into three distinct layers that communicate through well-defined interfaces.

### Desktop Client Layer

The **Electron-based UI** provides a thin client that consumes server capabilities exclusively through the OpenWork MCP. According to the source code in [`apps/app/src/react-app/kernel/local-provider.tsx`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/kernel/local-provider.tsx), the client uses a **feature-flag system** to toggle experimental UI elements like the Memory panel without affecting server logic. All UI components derive from **shadcn/Base UI** to maintain consistency across desktop, web, and mobile surfaces.

### Server Runtime Layer

The **den-api** layer hosts the durable runtime and exposes tools through **`search_capabilities`** and **`execute_capability`** endpoints. As implemented in [`ee/apps/den-api/src/routes/workers/core.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/workers/core.ts), all routes enforce **owner-scoped** access (filtering by `user_id = principal.userId`) through the [`mcp/auth.ts`](https://github.com/different-ai/openwork/blob/main/mcp/auth.ts) guard. This prevents cross-user data leakage by design.

### Infrastructure Layer

Data persists in **MySQL/PlanetScale** using **Drizzle ORM**, with optional full-text search capabilities. The system uses **type-ids** (registered in [`packages/utils/src/typeid.ts`](https://github.com/different-ai/openwork/blob/main/packages/utils/src/typeid.ts)) to assign globally unique prefixes like `memory` and `memctx` to entities. The **FULLTEXT** index is created idempotently at bootstrap to guarantee searchable content on fresh installations.

## Follow Core Development Principles

The [`AGENTS.md`](https://github.com/different-ai/openwork/blob/main/AGENTS.md) file establishes five non-negotiable principles for contributors.

### Develop Local-First, Cloud-Ready

Always develop against a local dev profile using `pnpm dev` or `pnpm dev:worktree`, which creates an isolated environment via `OPENWORK_DEV_PROFILE=auto`. This approach ensures zero code changes when pushing to a shared cloud-hosted Den, maintaining parity between local and production environments.

### Consume Server Logic via MCP

The desktop app never re-implements business logic. Instead, it consumes **MCP-exposed tools** such as `postMemory` and `getMemorySearch`. This server-consumption-first pattern ensures single-source-of-truth for capabilities and simplifies client-side code.

### Build with Composable UI

Construct interface elements using **shadcn/ui** components from the shared library (`@/components`). This standardization keeps the look-and-feel consistent across all platforms and reduces maintenance overhead.

### Maintain Strict Type Safety

The codebase enforces a "no `any`" rule. All TypeScript definitions generate from OpenAPI specs (see [`openapi.ts`](https://github.com/different-ai/openwork/blob/main/openapi.ts)), providing compile-time guarantees that match runtime behavior.

### Gate Experimental Features

Deploy unfinished capabilities behind client-only feature flags defined in [`feature-flags-preferences.ts`](https://github.com/different-ai/openwork/blob/main/feature-flags-preferences.ts). Toggle `featureFlags.memory` to expose the Memory Bank UI before implementing server-side hard gates, allowing safe experimentation without breaking existing contracts.

## Implement New Capabilities Safely

Adding persistent features requires coordination across the stack, from type registration to UI implementation, as detailed in [`memory-bank-architecture.md`](https://github.com/different-ai/openwork/blob/main/memory-bank-architecture.md).

### Register Type IDs for Entities

Before creating new database entities, register their prefixes in [`packages/utils/src/typeid.ts`](https://github.com/different-ai/openwork/blob/main/packages/utils/src/typeid.ts). For example, the Memory feature declares:

```typescript
export const typeIdMapNameToPrefix = {
  // …existing entries…
  memory: 'mem',
  memctx: 'mcx',
};

```

This registration ensures all entities receive globally unique identifiers with appropriate prefixes.

### Add MCP Tools with Owner Scoping

Define new RPC endpoints in [`ee/apps/den-api/src/routes/workers/core.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/workers/core.ts), assigning an `operationId` like `postMemory` and tagging it in [`mcp/policy.ts`](https://github.com/different-ai/openwork/blob/main/mcp/policy.ts). Always enforce user-scoped storage by extracting the user ID from the authentication context:

```typescript
router.post('/v1/memory', async (c) => {
  const { content, tags, contexts } = c.req.body;
  const userId = c.get('user').id;
  // Force user-scoped storage
  const memory = await db.insert(memoryTable).values({
    user_id: userId,
    content,
    tags,
    source: 'chat',
    scope: 'user',   // overrides any client value
  }).returning();
  return c.json(memory);
});

```

This pattern prevents data leakage by overriding any client-provided scope values.

### Enable Full-Text Search

For searchable content, implement endpoints using MySQL's FULLTEXT capabilities with owner-scoped filters:

```typescript
router.get('/v1/memory/search', async (c) => {
  const q = c.req.query('q');
  const userId = c.get('user').id;
  const results = await db
    .select()
    .from(memoryTable)
    .where(and(
      sql`user_id = ${userId}`,
      sql`MATCH(content) AGAINST(${q} IN NATURAL LANGUAGE MODE)`
    ))
    .limit(20);
  return c.json({ results });
});

```

The idempotent FULLTEXT index guarantees this works immediately on fresh database installations.

### Configure Feature Flags in the UI

Expose experimental functionality through the feature flag system. Import the hook from the local provider and toggle visibility:

```tsx
import { useLocal } from '@/components/feature-flags';
const { featureFlags } = useLocal();

return (
  <Switch
    checked={featureFlags.memory}
    onCheckedChange={(c) => setFeatureFlags({ ...featureFlags, memory: c })}
  >
    Enable Memory Bank UI
  </Switch>
);

```

This client-side gating allows iterative development while maintaining production stability.

### Validate Changes with Tests and Fraimz

Before submitting pull requests, run `pnpm test` to verify your changes. Capture screenshots or a short video demonstrating the feature and attach the generated [`fraimz.html`](https://github.com/different-ai/openwork/blob/main/fraimz.html) proof to your PR, as required by [`AGENTS.md`](https://github.com/different-ai/openwork/blob/main/AGENTS.md) "Pull Request Expectations".

## Summary

- **Work local-first** using `pnpm dev:worktree` with isolated profiles to ensure cloud-readiness without code changes.
- **Expose capabilities via MCP** rather than reimplementing logic in the client, consuming tools like `postMemory` and `getMemorySearch`.
- **Enforce owner-scoping** in all server routes by filtering against `principal.userId` to prevent cross-user data access.
- **Register type-ids** in [`packages/utils/src/typeid.ts`](https://github.com/different-ai/openwork/blob/main/packages/utils/src/typeid.ts) before creating new entities to maintain global uniqueness.
- **Gate experimental features** behind client-side flags in [`feature-flags-preferences.ts`](https://github.com/different-ai/openwork/blob/main/feature-flags-preferences.ts) before adding server-side enforcement.
- **Maintain type safety** by avoiding `any` and using OpenAPI-generated definitions throughout the codebase.

## Frequently Asked Questions

### How do I set up a local development environment for OpenWork?

Clone the repository and run `OPENWORK_DEV_PROFILE=auto pnpm dev:worktree` to create an isolated worktree profile. This command establishes a local-first environment that mirrors cloud deployment without requiring configuration changes when you push to production.

### What is the correct way to add a new persistent feature to OpenWork?

First register the entity's type-id prefix in [`packages/utils/src/typeid.ts`](https://github.com/different-ai/openwork/blob/main/packages/utils/src/typeid.ts), then implement the server endpoint in [`ee/apps/den-api/src/routes/workers/core.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/workers/core.ts) with owner-scoped filters (`user_id = principal.userId`). Finally, expose the capability through the MCP policy and optionally gate the UI behind a feature flag until the implementation stabilizes.

### Why does OpenWork require owner-scoped filters on all server routes?

The architecture mandates that all data access filters by the authenticated user's ID (enforced in [`mcp/auth.ts`](https://github.com/different-ai/openwork/blob/main/mcp/auth.ts)) to prevent cross-tenant data leakage. This design choice ensures that even if client-side validation fails, the database layer blocks unauthorized access attempts by checking `user_id` against the JWT principal on every query.

### How do I integrate OpenWork capabilities into an external agent?

Configure your agent's [`opencode.json`](https://github.com/different-ai/openwork/blob/main/opencode.json) to connect to the OpenWork MCP endpoint:

```json
{
  "mcp": {
    "openwork": {
      "type": "remote",
      "enabled": true,
      "url": "https://api.openworklabs.com/mcp/agent",
      "oauth": {}
    }
  }
}

```

This allows any compatible agent to consume tools like `search_capabilities` and `execute_capability` without hosting the OpenWork desktop application.