OpenWork Best Practices: Local-First Development Guide for AI Workflows
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, 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, all routes enforce owner-scoped access (filtering by user_id = principal.userId) through the 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) 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 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), 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. 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.
Register Type IDs for Entities
Before creating new database entities, register their prefixes in packages/utils/src/typeid.ts. For example, the Memory feature declares:
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, assigning an operationId like postMemory and tagging it in mcp/policy.ts. Always enforce user-scoped storage by extracting the user ID from the authentication context:
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:
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:
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 proof to your PR, as required by AGENTS.md "Pull Request Expectations".
Summary
- Work local-first using
pnpm dev:worktreewith isolated profiles to ensure cloud-readiness without code changes. - Expose capabilities via MCP rather than reimplementing logic in the client, consuming tools like
postMemoryandgetMemorySearch. - Enforce owner-scoping in all server routes by filtering against
principal.userIdto prevent cross-user data access. - Register type-ids in
packages/utils/src/typeid.tsbefore creating new entities to maintain global uniqueness. - Gate experimental features behind client-side flags in
feature-flags-preferences.tsbefore adding server-side enforcement. - Maintain type safety by avoiding
anyand 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, then implement the server endpoint in 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) 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 to connect to the OpenWork MCP endpoint:
{
"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.
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 →