@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-sites

Quick 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 SiteService for 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)

Related Modules