@happyvertical/smrt-sites
Site lifecycle management for multi-tenant networks with agent bindings, provisioning tracking, and portal configuration.
v0.20.44Site LifecycleAgent BindingsMulti-Tenant
Overview
smrt-sites manages deployable websites within a tenant hierarchy. Sites progress through a draft-to-archived lifecycle, track infrastructure provisioning, and bind to agent classes with priority ordering and per-site configuration overrides.
Installation
bash
npm install @happyvertical/smrt-sitesQuick Start
typescript
import {
Site, SiteCollection,
SiteAgentBinding, SiteAgentBindingCollection,
SiteService
} from '@happyvertical/smrt-sites';
// Create collections
const sites = await SiteCollection.create({ db });
const bindings = await SiteAgentBindingCollection.create({ db });
// Use SiteService for lifecycle operations
const service = new SiteService({ sites, bindings });
// Create a site (starts in draft status)
const site = await service.createSite('tenant-123', {
name: 'Community Hub',
domain: 'hub.example.com',
tier: 'standard',
});
// Activate after validation (requires name + domain)
await service.activateSite(site.id);
// Mark infrastructure as provisioned
await service.markProvisioned(site.id);
// Bind agents with priority ordering (higher = first)
await service.bindAgent(site.id, 'Praeco', { schedule: '0 * * * *' });
await service.bindAgent(site.id, 'Caelus', { maxArticles: 50 });
// Get enabled agents sorted by priority descending
const agents = await service.getEnabledAgents(site.id);
// Suspend or archive a site
await service.suspendSite(site.id);
await service.archiveSite(site.id);Core Models
Site
typescript
class Site extends SmrtObject {
name: string
domain: string // Unique per tenant
tier: 'free' | 'standard' | 'premium'
status: 'draft' | 'active' | 'suspended' | 'archived'
provisioningStatus: 'pending' | 'provisioning' | 'ready' | 'failed'
provisioningTimestamp?: Date
portalConfig: string // JSON (theme, branding, navigation)
databaseUrl?: string
getPortalConfig(): SitePortalConfig
setPortalConfig(config: SitePortalConfig): void
// @TenantScoped({ mode: 'required' })
}SiteAgentBinding
typescript
class SiteAgentBinding extends SmrtObject {
siteId: string
agentClass: string // Agent class name
priority: number // Higher values execute first
enabled: boolean
config?: string // JSON per-site overrides (nullable)
getConfig(): Record<string, any> | null
// conflictColumns: ['site_id', 'agent_class']
// @TenantScoped({ mode: 'required' })
}Agent Bindings
typescript
// bindAgent() upserts: updates config if exists, creates if not
await service.bindAgent(site.id, 'Praeco', {
schedule: '0 * * * *',
maxItems: 20,
});
// Update an existing binding's config
await service.bindAgent(site.id, 'Praeco', {
schedule: '*/30 * * * *', // Changed to every 30 min
maxItems: 50,
});
// Get enabled agents sorted by priority descending
const agents = await service.getEnabledAgents(site.id);
// Returns SiteAgentBinding[] ordered by priority
// Unbind an agent
await service.unbindAgent(site.id, 'Praeco');Best Practices
DOs
- Use
SiteServicefor all lifecycle operations - Validate name and domain before activating a site
- Use
bindAgent()for upsert behavior on agent bindings - Track provisioning status separately from site status
- Use
getPortalConfig()/setPortalConfig()for type-safe access
DON'Ts
- Don't skip tenant context (both models require tenant scoping)
- Don't create duplicate domain entries within the same tenant
- Don't manually set agent binding config without using
bindAgent() - Don't confuse portalConfig object/string forms (constructor accepts both)