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
Authorizationheader matchingPERSISTENCE_DEV_TOKENor receives401with 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:
- Path validation – API routes require
/api/persistence/{documents|assets|runtime}/<stageId>/... - Key prefixing – All storage keys automatically include the stage identifier
- 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:
- 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
});
-
Provider selection –
server-provider.tschecksNEXT_PUBLIC_PERSISTENCE:- Disabled: Returns no-op provider using local Dexie/IndexedDB
- Enabled: Constructs
ServerPersistenceProviderwith configured backend
-
Authentication gate –
server-auth.tsvalidates theAuthorizationheader againstPERSISTENCE_DEV_TOKEN(dev only) -
Authorization enforcement –
owner-bound-document-store.tsprefixes the operation with the validated stage ID from the URL -
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_PERSISTENCEeliminates attack surface when server persistence is not needed - Token-based authentication in
server-auth.tsprotects development environments without complicating production IAM integration - Stage-scoped authorization in
owner-bound-document-store.tsguarantees 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →