Files
skills/docs/api-stack/api/api-plugin-site/src/plugin/plugin.md
2026-05-29 08:20:56 +01:00

3.8 KiB

plugin/plugin.ts

(no submodules)

Purpose

Owns createSitePlugin, the canonical factory that produces the PluginDescriptor consumed by @suaveplan/api-plugin's PluginRegistry. The factory composes every other module in this package — SitePluginOptionsSchema validates options at the boundary, the SiteService + DomainResolverService are constructed once, the Hono middleware is built and exposed through ServiceRegistry, the public + private route trees are mounted under the configured routePrefix, and the optional organization:created listener is wired when autoCreateDefaultSite is on. This file is the single integration point — every consumer of the package builds its descriptor through this one function.

The lifecycle is the canonical 4-phase shape: validate-at-construction, build-services in onInit, mount-routes in onReady, drop-listener in onClose. Each phase fails fast with a typed SiteError carrying a SITE.* code so misconfigurations are visible at registry intake rather than deferred to the first request.

Features

  • Zod-validated option boundary — SITE.INVALID_OPTIONS on bad config.
  • Resolves db from ServiceRegistry; throws SITE.SERVICE_NOT_AVAILABLE when missing.
  • Constructs and registers siteService, domainResolver, domainResolverMiddleware.
  • Optional organization:created listener for default-site auto-bootstrap, with SITE.BOOTSTRAP_FAILED event emission on error and the site.bootstrap_failed_total counter incremented on each failure.
  • Route mounting under config.routePrefix (defaults to /api) with prefix-collision detection via SITE.ROUTE_PREFIX_COLLISION.
  • optionalDependencies: ["database", "auth", "events"] so orchestrators topo-sort the site plugin after siblings without making them mandatory.
  • Emits site:initialized and site:ready lifecycle events.

Usage: Basic

import { createSitePlugin } from "@suaveplan/api-plugin-site";

registry.register(createSitePlugin({ maxSitesPerOrg: 25 }));

Usage: Advanced

import { createSitePlugin } from "@suaveplan/api-plugin-site";

const plugin = createSitePlugin({
    routePrefix: "/v1",
    autoCreateDefaultSite: true,
    defaultDomainPattern: "{slug}.acme.com",
    enableDomainResolution: true,
});

await registry.register(plugin, { db: surrealAdapter, app: openApiHono });
await registry.start();

// Downstream plugin can now resolve the service:
const siteService = ctx.getService("siteService")!;
await siteService.create({
    organization_id: "organization:abc",
    name: "Customer Site",
    domains: ["customer.acme.com"],
});

API Reference

  • createSitePlugin(options?: CreateSitePluginOptions): PluginDescriptor<SitePluginConfig>
  • type CreateSitePluginOptions = SitePluginOptionsInput
  • The descriptor name is "site", version "0.1.0".

Implementation Notes

  • Service handles are stored in closure-scoped variables and bound via a private bind helper so the onReady hook can reuse the same instances constructed in onInit. The mustService / mustResolver guards trip when an orchestrator runs onReady before onInit (which the canonical registry never does, but tests / misuse can).
  • The plugin treats the host event bus as EventBusLike<SiteEventMap> so the cumulative event typing is enforced inside the plugin without bleeding into the global EventBusLike declaration on the host.
  • Route mounting is keyed by detecting any existing path starting with ${prefix}/sites/; a collision raises SITE.ROUTE_PREFIX_COLLISION so two plugins can't silently overwrite each other.
  • enableDomainResolution: false suppresses ONLY the public router (which contains the resolve endpoint); the private router always mounts so CRUD remains usable even when public lookup is disabled.