ORM, code generation, AI integration, and the DispatchBus. Everything else builds on this.
| Class | File | Purpose |
|---|---|---|
| SmrtObject | src/object.ts |
Base persistent object — save, delete, is(), do(), loadFromId/Slug |
| SmrtCollection | src/collection.ts |
CRUD collection — list, get, create, delete, getOrUpsert |
| ObjectRegistry | src/registry.ts |
Global singleton (globalThis) — class metadata, fields, STI chains, manifests |
| DispatchBus | src/dispatch/bus.ts |
Inter-agent messaging — emit, subscribe (persistent), process |
| GlobalInterceptors | src/interceptors.ts |
Plugin system — beforeList/Get/Save/Delete hooks (used by tenancy) |
constructor(options) → initialize() → ready for save()/delete()/loadFromId()
initialize(): loads field initializers, applies option values (options override initializers), loads from DB if id/slug providedsave(): upsert with STI validation, interceptor execution, auto-embeddingsis(criteria)/do(instructions): AI operations via function callinggetSlug(): auto-generates from name → title → label → idloadRelated(fieldName): lazy-loads relationships (cached in_loadedRelationshipsMap)
await collection.list({
where: { status: 'active', price: { op: '>', value: 10 } },
limit: 50, offset: 0, orderBy: 'created_at DESC'
});WHERE operators: =, >, <, >=, <=, !=, in, not in, like, is null, is not null. Arrays auto-detect IN. Dot notation for JSON paths: metadata.userId.
STI child collections auto-filter by _meta_type.
Key options: tableName, tableStrategy ('cti'|'sti'), conflictColumns, api/mcp/cli (generation config), ai (callable methods), hooks (beforeSave/afterSave/beforeDelete/afterDelete), embeddings (auto-generate), tenantScoped, agent.
Registration sets SMRT_TABLE_NAME static property (survives minification).
emit(signalType, payload, metadata)→ creates persistent Dispatch recordon(pattern, handler)→ in-memory handler (immediate)subscribe({ signalType, subscriber })→ persistent subscription (survives restarts)process(subscriberName, handler)→ process pending dispatches- Wildcards:
campaign.*matchescampaign.completed(single segment only) - Tables:
_smrt_dispatch,_smrt_dispatch_subscriptions - Status:
pending → processing → completed(orfailed)
- Base:
@smrt({ tableStrategy: 'sti' })— children inherit, share one table - Discriminator:
_meta_typecolumn with qualified names (@happyvertical/smrt-content:Article) - Child fields:
@meta()decorator → stored in_meta_dataJSONB (not as columns) - Polymorphic queries: collection loads
_meta_type, creates correct subclass dynamically - Validation: fail-fast on save if
_meta_typemissing or mismatched
| Generator | Location | Output |
|---|---|---|
| REST API | src/generators/rest.ts |
OpenAPI-compliant CRUD endpoints |
| CLI | src/generators/cli.ts |
Commander commands with auto-help |
| MCP Server | src/generators/mcp.ts |
Model Context Protocol tools |
// vite.config.ts — required for @smrt() decorators
export default defineConfig({
esbuild: {
tsconfigRaw: {
compilerOptions: { experimentalDecorators: true, emitDecoratorMetadata: true }
}
}
});- Never override toJSON() — handles STI discriminator + meta field extraction. Use
transformJSON() - Property init order: TypeScript initializers run first, then
initialize()applies option values (options win) - No runtime schema creation: application tables must be prepared explicitly via migrations/tooling; runtime only verifies and fails clearly
- Retry logic:
db.get()(3 retries, 250ms) anddb.upsert()(3 retries, 500ms) have built-in retry - Field caching:
_cachedFieldspopulated duringCollection.create()— eliminates asyncgetFields()per query - Smart cloning: arrays/objects shallow-cloned in property init to prevent aliasing (Issue #22)
- Table verification cache:
isTableVerified(dbUrl, tableName)avoids redundanttableExists()calls - Manifest required: build-time AST scanning creates manifest. Without vitest plugin → "No field metadata"
- Vite plugin loads scanner from
dist/first:src/vite-plugin/import-build-aware.tsprefersdist/when it exists on disk; it only falls back tosrc/on fresh clones. So if you editsrc/scanner/*.tsorsrc/schema/generator.tsand want those edits reflected in consumer manifest generation, you must rebuild (pnpm buildor havepnpm dev/pnpm build:watchrunning in core). This is intentional — sniffing.tsvs.jsviaimport.meta.urlwas non-deterministic under tsx and broke 12–13 publishes (#1139).