How OpenMAIC Manages Feature Flags: A Centralized TypeScript Pattern

OpenMAIC controls optional functionality through a centralized feature-flags module located at lib/config/feature-flags.ts, where pure predicate functions evaluate environment variables at runtime to enable consistent feature gating across API routes, React components, and test suites.

The THU-MAIC/OpenMAIC repository implements a strict separation between flag definition and consumption. Instead of scattering process.env checks throughout the application, the codebase relies on a single configuration module that exports boolean predicates. This architecture ensures that features like agent runtime, AI chat interfaces, and advanced editor renderers can be toggled atomically via environment variables while maintaining type safety and testability.

Centralized Configuration in lib/config/feature-flags.ts

The feature flag system centers on lib/config/feature-flags.ts, which encapsulates all environment variable logic. This module exports pure functions that compare runtime environment values against expected activation strings, returning strict booleans that the rest of the application consumes.

// lib/config/feature-flags.ts
export const isAgentRuntimeConfigured = (): boolean =>
  process.env.MAIC_AGENT_RUNTIME === 'enabled';

export const isProWorkbenchEnabled = (): boolean =>
  process.env.MAIC_PRO_WORKBENCH === 'true';

export const isPiChatEnabled = (): boolean =>
  process.env.MAIC_PI_CHAT === 'true';

export const isEditorRendererEnabled = (): boolean =>
  process.env.MAIC_EDITOR_RENDERER === 'true';

Each predicate is a zero-arity function that reads process.env at invocation time. This lazy evaluation ensures that flags respond to runtime environment changes during server-side rendering or API execution without requiring application restarts in containerized environments.

Server-Side Usage Patterns

API routes use these predicates to conditionally expose endpoints or abort requests early when dependencies are unavailable. In app/api/stages/route.ts, the application guards agent-specific functionality by checking isAgentRuntimeConfigured() before executing heavy operations.

// app/api/stages/route.ts
import { isAgentRuntimeConfigured } from '@/lib/config/feature-flags';

export async function GET(req: Request) {
  if (!isAgentRuntimeConfigured()) {
    return new Response('Agent runtime not configured', { status: 404 });
  }
  
  // Proceed with agent runtime logic...
  const stages = await fetchAgentStages();
  return Response.json(stages);
}

This pattern prevents uninitialized subsystems from receiving traffic, reducing error rates in deployments where specific microservices or experimental features remain disabled.

Client-Side Implementation

React components and custom hooks import the same predicates to branch between feature variants. The slide editor surface in components/edit/surfaces/slide/use-slide-surface.ts evaluates isEditorRendererEnabled() to determine whether to mount the new renderer or fall back to legacy implementations.

// components/edit/surfaces/slide/use-slide-surface.ts
import { isEditorRendererEnabled } from '@/lib/config/feature-flags';

export function useSlideSurface() {
  const useNewRenderer = isEditorRendererEnabled();
  
  return {
    renderer: useNewRenderer ? 'modern' : 'legacy',
    // ... other surface configuration
  };
}

For UI gating, components can conditionally render entire feature blocks:

// components/chat/ChatBox.tsx
import { isPiChatEnabled } from '@/lib/config/feature-flags';
import { PiChat } from './PiChat';
import { LegacyChat } from './LegacyChat';

export function ChatBox() {
  return isPiChatEnabled() ? <PiChat /> : <LegacyChat />;
}

Testing Strategy with Mocked Predicates

The test suite leverages Vitest's module mocking to verify behavior under different flag states without modifying actual environment variables. By mocking @/lib/config/feature-flags directly, tests achieve deterministic execution paths for both enabled and disabled scenarios.

// Example test file
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ChatBox } from '@/components/chat/ChatBox';

vi.mock('@/lib/config/feature-flags', () => ({
  isPiChatEnabled: () => true,
  isProWorkbenchEnabled: () => false,
}));

describe('ChatBox', () => {
  it('renders PiChat when feature flag is enabled', () => {
    render(<ChatBox />);
    expect(screen.getByTestId('pi-chat')).toBeInTheDocument();
  });
});

This mocking strategy extends to server-side tests, where API routes can be verified to return 404 responses when isAgentRuntimeConfigured returns false, ensuring that security and availability constraints function correctly regardless of deployment environment.

Benefits of the OpenMAIC Feature Flag Architecture

Implementing flags as centralized predicates provides three critical advantages for the OpenMAIC codebase:

  1. Consistency. Every component and API route references the same source of truth for feature state, eliminating synchronization bugs where one module checks process.env.MAIC_PI_CHAT directly while another uses a different conditional.

  2. Ease of Migration. Toggling features requires updating only the corresponding environment variable or predicate default value. The entire application responds uniformly without requiring find-and-replace operations across dozens of files.

  3. Deterministic Testing. Pure predicate functions can be stubbed at the module boundary, allowing unit tests to exhaustively verify both code paths (enabled and disabled) without relying on complex environment setup or external configuration management.

Summary

  • OpenMAIC manages feature flags through lib/config/feature-flags.ts, a centralized module exporting pure predicate functions.
  • Environment variables like MAIC_AGENT_RUNTIME, MAIC_PRO_WORKBENCH, MAIC_PI_CHAT, and MAIC_EDITOR_RENDERER are evaluated at runtime within these predicates.
  • Server routes in files like app/api/stages/route.ts use predicates to guard endpoints and return early for disabled features.
  • Frontend components import predicates to conditionally render modern features or legacy fallbacks.
  • The testing suite mocks the entire feature-flags module to simulate different states deterministically using vi.mock('@/lib/config/feature-flags', ...).

Frequently Asked Questions

How do I add a new feature flag to OpenMAIC?

Create a new predicate function in lib/config/feature-flags.ts that checks a unique environment variable. Export the function, then import it into any API routes, React components, or hooks that need to gate behavior. Remember to add corresponding test mocks in any test files covering the new feature path.

Why does OpenMAIC use functions instead of constant exports for feature flags?

Using zero-arity functions instead of exported constants allows for lazy evaluation and easier mocking. Functions can be spied upon or completely replaced in test environments using vi.mock, whereas exported constants would require more complex module re-injection strategies. Additionally, functions can encapsulate logic beyond simple equality checks if flag evaluation becomes more complex in the future.

Can feature flags be changed without restarting the OpenMAIC server?

In practice, most Node.js deployments cache process.env at startup. While the predicate functions technically read process.env at invocation time, standard containerized environments require a restart to propagate new environment variable values. For hot-swappable flags without redeployment, the architecture would need extension to support external configuration stores like Redis or feature flag services, which OpenMAIC does not currently implement according to the source code.

How does OpenMAIC handle feature flag type safety?

The TypeScript compiler ensures that only exported predicates from lib/config/feature-flags.ts are used throughout the application. By constraining flag checks to this module, the codebase maintains strict typing—predicates always return boolean, preventing truthy/falsy edge cases that might arise from direct string comparisons scattered across files.

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 →