Skip to main content

Data Organization & Compartmentalization

This guide covers techniques for organizing data across multi-agent SMRT applications.

Options Overview

ApproachComplexityUse Case
Single DatabaseSimpleSolo dev, single agent
Per-Agent DatabasesMediumData isolation by agent
Shared + PrivateComplexMulti-agent organizations

Option 1: Single Database (Default)

All agents share one database. Simplest setup.

// smrt.config.js
export default {
persistence: {
type: 'sqlite',
url: './data.db'
}
};

Pros: Simple, all data queryable together Cons: No isolation between agents


Option 2: Per-Agent Databases

Each agent gets its own database via manual routing.

// In your application
const praecoDb = await sql({ url: './praeco.db' });
const caelusDb = await sql({ url: './caelus.db' });

// Praeco uses its database
const praeco = new Praeco({
db: praecoDb,
// ...
});

// Caelus uses its database
const caelus = new Caelus({
db: caelusDb,
// ...
});

How it works

ObjectRegistry caches collections by db instance ID. Different db objects = different cache entries = isolated collections.

// From registry.ts:1352-1368
let dbId: number | undefined;
if (options.db && typeof options.db === 'object') {
if (!ObjectRegistry.dbInstanceIds.has(options.db)) {
ObjectRegistry.dbInstanceIds.set(options.db, ObjectRegistry.nextDbId++);
}
dbId = ObjectRegistry.dbInstanceIds.get(options.db);
}

const cacheKey = `${className}:${JSON.stringify({
persistence: options.persistence,
db: dbId !== undefined ? `db:${dbId}` : undefined,
ai: options.ai ? 'present' : undefined,
})}`;

Pros: Complete data isolation Cons: Can't query across agents, duplicate shared models


Use a remote database (sqld/libsql) for shared data, local databases for private agent data.

Architecture

┌─────────────────────────────────────────┐
│ blindmanpress.com │
│ (Organization Host) │
├─────────────────────────────────────────┤
│ sqld remote DB (shared) │
│ - profiles (Person, Organization) │
│ - events (shared base table) │
│ - places (shared base table) │
│ - content (shared base table) │
└──────────────┬──────────────────────────┘

┌────────┴────────┐
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ Praeco │ │ Caelus │
│ Agent │ │ Agent │
├───────────┤ ├───────────┤
│ Local DB: │ │ Local DB: │
│ - drafts │ │ - cache │
│ - scrapes │ │ - forecasts│
└───────────┘ └───────────┘

Configuration

// blindmanpress.com/smrt.config.js
import { sql } from '@happyvertical/sql';

// Shared database for organization-wide data
const sharedDb = await sql({
type: 'libsql',
url: 'libsql://blindmanpress-db.turso.io',
authToken: process.env.TURSO_AUTH_TOKEN
});

// Agent-specific local databases
const praecoLocalDb = await sql({ url: './data/praeco-local.db' });
const caelusLocalDb = await sql({ url: './data/caelus-local.db' });

export default {
// Default shared database
persistence: { db: sharedDb },

// Per-module overrides
modules: {
praeco: {
// Praeco uses shared for profiles/events, local for drafts
collections: {
Meeting: { db: sharedDb },
Council: { db: sharedDb },
MeetingDraft: { db: praecoLocalDb }
}
},
caelus: {
collections: {
ForecastLocation: { db: sharedDb },
WeatherCache: { db: caelusLocalDb }
}
}
}
};

STI Schema with Multiple Agents

When your host application has multiple agents as dependencies, STI tables aggregate fields from ALL descendants.

Example

blindmanpress.com depends on both praeco and caelus:

// package.json
{
"dependencies": {
"@happyvertical/praeco": "^0.1.0",
"@happyvertical/caelus": "^0.1.0"
}
}

If both define STI children:

  • praeco: Meeting extends Event, MeetingLocation extends Place
  • caelus: Forecast extends Event, ForecastLocation extends Place

The generated events table includes fields from BOTH:

CREATE TABLE events (
-- Base Event fields
id TEXT PRIMARY KEY,
title TEXT,
startDate DATETIME,

-- STI discriminator
_meta_type TEXT NOT NULL,

-- Child fields stored in JSONB
_meta_data JSONB
);

The _meta_data column stores child-specific fields:

  • For Meeting: { councilId, agendaUrl, minutesUrl }
  • For Forecast: { forecastZone, temperature, humidity }

Why this works

generateSTISchemaFromRegistry calls ObjectRegistry.getDescendants(baseClassName) which returns ALL registered descendants, regardless of which package they came from:

// From schema/generator.ts:608-617
const descendants = ObjectRegistry.getDescendants(baseClassName);
const allClassNames = [baseClassName, ...descendants];

for (const className of allClassNames) {
const classFields = await ObjectRegistry.getAllFields(className);
// Adds columns for each field
}

Key Mechanisms

1. Collection Caching by DB Instance

Pass different db objects to get isolated collection instances. The ObjectRegistry assigns unique IDs to each db instance and uses them in the cache key.

2. STI Descendant Aggregation

All imported packages' STI children contribute to shared tables. The schema generator walks the entire inheritance tree at build time.


Best Practices

  1. Start simple: Use single database until you need isolation
  2. Shared profiles: Keep Person/Organization in shared DB for cross-agent deduplication
  3. Local caches: Agent-specific caches/drafts belong in local databases
  4. STI for polymorphic queries: Use when you need to query across event/content types
  5. Host aggregates schema: The host application should depend on all agents to generate complete STI schemas