OpenMAIC Server-Backed Persistence Security Model: Three-Layer Defense Explained

OpenMAIC's server-backed persistence security model uses a three-layer defense: feature-gate activation, token-based authentication in development, and stage-scoped authorization to isolate user data.

OpenMAIC, the multi-agent intelligence collaboration framework, allows users to store documents, assets, and chat history on a remote server rather than relying solely on browser-based IndexedDB. This server-backed persistence capability requires careful security design to prevent unauthorized access and cross-workspace data leakage. The implementation in THU-MAIC/OpenMAIC follows a defense-in-depth approach with explicit build-time controls, runtime authentication, and strict data isolation boundaries.

The Three Security Layers

OpenMAIC's persistence security architecture operates through three complementary layers that must all pass before any server storage operation succeeds.

Layer 1: Feature-Gate Build-Time Control

The foundation of the security model is a compile-time feature flag that completely eliminates server-side persistence routes when disabled.

Environment Variable Effect When Set Implementation Location
NEXT_PUBLIC_PERSISTENCE=1 Enables /api/persistence/* routes and server provider selection app/api/persistence/[...path]/route.ts

When this variable is absent or unset, the application compiles without server persistence endpoints. This fail-closed design ensures that deployment misconfigurations cannot accidentally expose storage APIs.

Layer 2: Development-Mode Authentication

Requests to persistence endpoints must carry a valid authorization token when running in development environments.

The lib/persistence/server-auth.ts module enforces this through the PERSISTENCE_DEV_TOKEN mechanism:

  • Development: Every request must include Authorization header matching PERSISTENCE_DEV_TOKEN or receives 401 with message "server persistence requires PERSISTENCE_DEV_TOKEN (development auth only)"
  • Production: Token verification is bypassed, delegating authentication to the hosted service's native IAM policies (e.g., AWS IAM, Cloudflare Access)

This split approach lets developers test securely without hardening requirements, while production deployments integrate with existing identity infrastructure.

Layer 3: Stage-Scoped Authorization and Isolation

Once authenticated, all operations are strictly bound to a specific workspace stage, preventing any cross-tenant data access.

The lib/persistence/owner-bound-document-store.ts implements this scoping:

// Key structure: every document prefixed with stage ID
const scopedKey = `${stageId}/${documentKey}`;

The lib/persistence/server-provider.ts extracts the stage ID from the request URL path (/:stageId/...) and validates it against the authenticated session before instantiating the store. This creates three enforced boundaries:

  1. Path validation – API routes require /api/persistence/{documents|assets|runtime}/<stageId>/...
  2. Key prefixing – All storage keys automatically include the stage identifier
  3. Session binding – The provider rejects operations for stages not owned by the authenticated user

Request Flow and Security Enforcement

Understanding the complete request lifecycle clarifies how the three layers interact.

Step-by-Step Request Processing

When a client initiates a persistence operation, the following sequence executes:

  1. Client builds request – The persistence store (@/lib/persistence/...) encodes the current stage context into the URL:
// Frontend code constructing a server-backed request
const stageId = useStageContext(); // Current workspace identifier
const response = await fetch(`/api/persistence/documents/${stageId}/my-doc.json`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/octet-stream' },
  body: documentData
});
  1. Provider selection – server-provider.ts checks NEXT_PUBLIC_PERSISTENCE:

    • Disabled: Returns no-op provider using local Dexie/IndexedDB
    • Enabled: Constructs ServerPersistenceProvider with configured backend
  2. Authentication gate – server-auth.ts validates the Authorization header against PERSISTENCE_DEV_TOKEN (dev only)

  3. Authorization enforcement – owner-bound-document-store.ts prefixes the operation with the validated stage ID from the URL

  4. Backend operation – The S3-compatible provider executes the scoped request

Failure Modes and Graceful Degradation

The security model includes resilient fallback behavior that maintains availability without compromising isolation:

Failure Scenario Response User Impact
Missing dev token 401 Unauthorized with explicit error message Immediate feedback, no data exposure
NEXT_PUBLIC_PERSISTENCE disabled Silent fallback to IndexedDB Full functionality, local-only storage
Server error Converted to persist-health channel warning UI notification, transparent IndexedDB fallback
Wrong stage ID in URL Rejected by provider validation Access denied, isolated to authorized stage

The lib/store/persist-health.ts module aggregates these health states, allowing the UI to display non-blocking warnings while continuing local operation.

Implementation Examples

Creating a Stage-Bound Document Store

The standard pattern for server-backed persistence in OpenMAIC:

import { createOwnerBoundDocumentStore } from '@/lib/persistence/owner-bound-document-store';

// Initialize store with explicit stage binding
const docStore = await createOwnerBoundDocumentStore(stageId);

// All operations automatically scoped to this stage
await docStore.setDocument('config.json', new Uint8Array([...]));
const config = await docStore.getDocument('config.json');
await docStore.deleteDocument('config.json');

The same abstraction supports assets (/api/persistence/assets/...) and runtime state (/api/persistence/runtime/...) with identical security guarantees.

API Route Implementation

The Next.js API route in app/api/persistence/[...path]/route.ts orchestrates all three security layers:

// Simplified route handler structure
export async function POST(request: Request) {
  // 1. Provider selection (Layer 1)
  const provider = await getProvider(); // Checks NEXT_PUBLIC_PERSISTENCE
  
  // 2. Authentication (Layer 2)
  await verifyAuth(request); // server-auth.ts validation
  
  // 3. Stage extraction and authorization (Layer 3)
  const stageId = extractStageFromPath(request);
  const store = createOwnerBoundDocumentStore(stageId, provider);
  
  // Execute scoped operation
  return store.handleRequest(request);
}

Key Source Files

File Security Responsibility
lib/persistence/server-provider.ts Feature-gate enforcement and provider selection
lib/persistence/server-auth.ts Development token verification
lib/persistence/owner-bound-document-store.ts Per-stage data isolation and key namespacing
app/api/persistence/[...path]/route.ts Request routing and layer orchestration
tests/persistence/stage-access-fidelity.test.ts Security boundary verification suite

Summary

  • Build-time control via NEXT_PUBLIC_PERSISTENCE eliminates attack surface when server persistence is not needed
  • Token-based authentication in server-auth.ts protects development environments without complicating production IAM integration
  • Stage-scoped authorization in owner-bound-document-store.ts guarantees strict workspace isolation through automatic key prefixing and URL validation
  • Graceful degradation to IndexedDB preserves functionality when server persistence is unavailable or rejected

Frequently Asked Questions

How does OpenMAIC prevent one user from accessing another user's documents?

The owner-bound-document-store.ts module enforces mandatory stage prefixing on every storage key. When a request arrives at /api/persistence/documents/<stageId>/..., the provider validates that the authenticated user owns that stage ID, then prepends it to all backend operations. A user requesting a different stage's URL path receives an authorization failure before any data access occurs. The test suite in stage-access-fidelity.test.ts continuously verifies this boundary.

What happens if I deploy OpenMAIC without setting PERSISTENCE_DEV_TOKEN?

In production builds, the PERSISTENCE_DEV_TOKEN check is completely bypassed—the code assumes your deployment environment (S3, R2, MinIO, etc.) enforces its own authentication. In development mode, missing this token causes all server persistence requests to return 401 Unauthorized with the message "server persistence requires PERSISTENCE_DEV_TOKEN (development auth only)". The application continues running with local IndexedDB storage.

Can I use server-backed persistence with multiple storage backends simultaneously?

The current architecture in server-provider.ts selects one provider at initialization based on environment configuration. While the abstraction layer could theoretically support multiple backends, the authorization model binds operations to a single provider instance per request. For multi-backend scenarios, you would deploy separate OpenMAIC instances with distinct NEXT_PUBLIC_PERSISTENCE configurations and route traffic accordingly.

How does OpenMAIC handle server downtime or network failures?

The persist-health channel (lib/store/persist-health.ts) monitors all server operations. When the ServerPersistenceProvider encounters HTTP errors, it publishes health status updates and transparently falls back to IndexedDB for reads and writes. Successfully persisted local data can later synchronize when server connectivity returns, though explicit conflict resolution must be implemented at the application layer if needed.

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 →