Best Practices for Developing with OpenWork: Local-First AI Workflow Architecture

OpenWork development requires a local-first, cloud-ready approach that separates concerns across desktop UI, server-side MCP tools, and infrastructure layers while enforcing strict TypeScript typing, owner-scoped data access, and feature-flag gating for experimental functionality.

OpenWork is a modular, local-first, cloud-ready platform for building and sharing AI-driven workflows. Maintained by the different-ai/openwork repository, this architecture enables developers to create durable AI agents with persistent memory capabilities. Following the best practices for developing with OpenWork ensures your contributions align with strict typing requirements, security boundaries, and the composable UI standards defined in the codebase.

Understanding the Three-Layer Architecture

OpenWork separates functionality into distinct layers to maintain clean boundaries between presentation, business logic, and data persistence.

Desktop and Client Layer

The Electron-based desktop application provides a thin UI that communicates with the server exclusively through the OpenWork MCP. This layer uses a feature-flag system to toggle UI-only experiments—such as the experimental Memory panel—and relies on shadcn/Base UI components to maintain visual consistency across platforms.

Server and den-api Layer

The server hosts the durable runtime, persistence layer, and MCP-exposed tools. It exposes capabilities through search_capabilities and execute_capability, with all routes protected by owner-scoping (user-id enforcement) via the mcp/auth.ts guard. The desktop app never re-implements server logic; instead, it consumes MCP tools like postMemory and getMemorySearch.

Infrastructure Layer

Data storage uses MySQL/PlanetScale with Drizzle ORM and optional full-text search. The system uses type-ids (registered in packages/utils/src/typeid.ts) to ensure all entities carry globally unique prefixes such as memory and memctx. The FULLTEXT index is created idempotently at bootstrap to guarantee searchable content on fresh installations.

Core Development Principles

Adhering to these five principles ensures code quality and architectural alignment:

  1. Local-first, cloud-ready – Develop against the local dev profile using pnpm dev or pnpm dev:worktree, then deploy to a shared cloud-hosted Den without code changes.
  2. Server-consumption first – The desktop app never duplicates server logic; it strictly consumes MCP-exposed tools.
  3. Composable UI – Build interface elements with shadcn/ui and the shared component library (@/components) to maintain consistency across desktop, web, and mobile.
  4. Strict typing – The codebase enforces a "no any" rule; all TypeScript definitions generate from OpenAPI specs (see openapi.ts).
  5. Feature-flag gating – Experimental features (like the Memory bank UI) live behind client-only flags (featureFlags.memory) before server-side hard gates are implemented.

Step-by-Step Development Workflow

Configure Local Development

Start with an isolated development profile to prevent conflicts with production data. Run the following command to create an auto-generated profile:

OPENWORK_DEV_PROFILE=auto pnpm dev:worktree

This workflow is documented in the README.md "Local development" section and creates an isolated worktree for safe experimentation.

Add New MCP Capabilities

To expose new server functionality:

  1. Define the RPC endpoint in ee/apps/den-api/src/routes/workers/core.ts
  2. Assign a unique operationId (e.g., postMemory)
  3. Tag the capability with "Memory" in mcp/policy.ts

All server routes must enforce owner-scoping through the mcp/auth.ts guard to ensure user_id = principal.userId filters prevent cross-user data leakage.

Register Type Identifiers

New persistent entities require globally unique prefixes. Register these in packages/utils/src/typeid.ts:

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

This registration provides compile-time safety and ensures all database records carry identifiable prefixes.

Implement Feature Flags

For experimental UI components:

  1. Toggle the flag in feature-flags-preferences.ts (e.g., featureFlags.memory)
  2. Build the panel UI under apps/app/src/react-app/.../memory-panel.tsx
  3. Access flags through the useLocal hook in apps/app/src/react-app/kernel/local-provider.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>
);

Validate with Tests and Fraimz

Before submitting changes:

  1. Run the test suite: pnpm test (or the run-tests skill)
  2. Capture screenshots or short video demonstrations
  3. Attach the fraimz.html proof to your pull request

As documented in AGENTS.md, the "Pull Request Expectations" section requires visual proof for all UI changes.

Security and Data Integrity Patterns

Owner-Scoped Data Access

Every database query must filter by user_id to prevent cross-user leakage. For example, the Memory search endpoint explicitly enforces:

.where(and(
  sql`user_id = ${userId}`,
  sql`MATCH(content) AGAINST(${q} IN NATURAL LANGUAGE MODE)`
))

This pattern appears in the retrieval flow documented in memory-bank-architecture.md section 8.

Idempotent Full-Text Indexing

The infrastructure guarantees searchable content through an idempotent FULLTEXT index creation at bootstrap. This ensures fresh database installations immediately support lexical search without manual migration steps, as detailed in section 3 of the Memory Bank Architecture document.

Progressive Feature Gating

The feature-flag pattern allows shipping experimental UI without affecting existing users. Client-only flags enable A/B testing, while server-side hard gates can be added later without breaking the MCP contract.

Code Implementation Examples

Connecting to the OpenWork MCP

Configure your agent to communicate with the OpenWork server:

// Add to your opencode.json (client side)
{
  "mcp": {
    "openwork": {
      "type": "remote",
      "enabled": true,
      "url": "https://api.openworklabs.com/mcp/agent",
      "oauth": {}
    }
  }
}

Saving Memories with Owner Scope

Implement the server-side POST endpoint in ee/apps/den-api/src/routes/workers/core.ts:

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();
  // Insert optional contexts in the same transaction …
  return c.json(memory);
});

Searching Memories with Full-Text

Implement lexical search using MySQL's MATCH...AGAINST syntax:

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 });
});

Essential Source Files and References

Understanding these key files accelerates development:

Summary

Following best practices for developing with OpenWork ensures secure, maintainable, and scalable contributions:

  • Develop locally first using OPENWORK_DEV_PROFILE=auto to maintain isolation from production environments.
  • Consume server logic through MCP tools rather than duplicating business logic in the desktop client.
  • Enforce owner-scoping on all database queries via user_id filters to prevent data leakage.
  • Register type-ids for new entities in typeid.ts to maintain global uniqueness.
  • Gate experiments behind feature flags before implementing server-side restrictions.
  • Submit proof with fraimz.html attachments when opening pull requests.

Frequently Asked Questions

What is the local-first development workflow in OpenWork?

The local-first workflow uses pnpm dev:worktree with OPENWORK_DEV_PROFILE=auto to create isolated development environments. This allows developers to test changes against a local server and database before deploying to the cloud-hosted Den, ensuring no code changes are required when transitioning between environments.

How do I add a new MCP capability to the OpenWork server?

Define the RPC endpoint in ee/apps/den-api/src/routes/workers/core.ts, assign a unique operationId (such as postMemory), and tag it in mcp/policy.ts. Ensure the route enforces owner-scoping through the mcp/auth.ts guard to validate that user_id matches the authenticated principal.

How does OpenWork prevent cross-user data leakage?

All server routes implement owner-scoping by filtering database queries with user_id = principal.userId. This pattern is enforced by the authentication guard in mcp/auth.ts and appears in every data retrieval and storage operation, ensuring users can only access their own memory records and contexts.

What is the correct way to implement experimental UI features?

Experimental features should be implemented behind feature flags defined in feature-flags-preferences.ts. Toggle the flag through the useLocal hook in apps/app/src/react-app/kernel/local-provider.tsx to show or hide UI components. This allows safe A/B testing before adding server-side enforcement or releasing to all users.

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 →