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:
- Local-first, cloud-ready – Develop against the local dev profile using
pnpm devorpnpm dev:worktree, then deploy to a shared cloud-hosted Den without code changes. - Server-consumption first – The desktop app never duplicates server logic; it strictly consumes MCP-exposed tools.
- Composable UI – Build interface elements with shadcn/ui and the shared component library (
@/components) to maintain consistency across desktop, web, and mobile. - Strict typing – The codebase enforces a "no
any" rule; all TypeScript definitions generate from OpenAPI specs (seeopenapi.ts). - 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:
- Define the RPC endpoint in
ee/apps/den-api/src/routes/workers/core.ts - Assign a unique
operationId(e.g.,postMemory) - Tag the capability with
"Memory"inmcp/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:
- Toggle the flag in
feature-flags-preferences.ts(e.g.,featureFlags.memory) - Build the panel UI under
apps/app/src/react-app/.../memory-panel.tsx - Access flags through the
useLocalhook inapps/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:
- Run the test suite:
pnpm test(or therun-testsskill) - Capture screenshots or short video demonstrations
- Attach the
fraimz.htmlproof 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:
-
README.md– Contains overview, installation commands, and the local dev workflow (pnpm dev:worktree). -
AGENTS.md– Defines core philosophy, coding standards, and PR expectations includingfraimz.htmlrequirements. -
docs/memory-bank-architecture.md– Details the persistent Memory feature design, including type-id usage, FULLTEXT indexing, and owner-scoping requirements. -
apps/server/src/openwork-runtime-config.ts– Location where agent prompts are injected, including the "## Memory Bank" section. -
ee/apps/den-api/src/routes/workers/core.ts– Implements the/v1/memoryCRUD endpoints and MCP tool handlers. -
packages/utils/src/typeid.ts– Central registry for type-id prefixes (memory→mem,memctx→mcx). -
apps/app/src/react-app/kernel/local-provider.tsx– Handles feature-flag state management (e.g.,featureFlags.memory).
Summary
Following best practices for developing with OpenWork ensures secure, maintainable, and scalable contributions:
- Develop locally first using
OPENWORK_DEV_PROFILE=autoto 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_idfilters to prevent data leakage. - Register type-ids for new entities in
typeid.tsto maintain global uniqueness. - Gate experiments behind feature flags before implementing server-side restrictions.
- Submit proof with
fraimz.htmlattachments 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →