# Add @suaveplan/api-plugin-site **Package**: `@suaveplan/api-plugin-site` **Status**: PROPOSED **Type**: new-package (migration from suaveplan) - **Spec delta**: ./specs/api-plugin-site/spec.md ## Why Genesis needs the multi-site / multi-domain capability `@suaveplan/api-plugin-site@1.0.21` ships upstream so consumer apps can persist `site` records (name / slug / theme / settings / push) under SurrealDB, resolve incoming `Host` headers to a site through a single canonical service, and expose public + private OpenAPI Hono routes for site management. The upstream package additionally bootstraps a default site on `organization:created` and emits `site:settings:updated` whenever settings change so CDNs and downstream consumers can react. The upstream library exists at `/mnt/s/web/suaveplan/packages/server/api-plugin-site` (11 src files, partial test coverage, three `as any` casts in `plugin.ts`, runtime-monkey-patched `updateSettings`, missing-domain bug in `addDomain`, no OTel surface, no typed error registry). This change lands a single, enterprise-grade `@suaveplan/api-plugin-site@0.1.0` that fixes every flagged smell from the upstream enterprise audit (`/mnt/s/web/suaveplan/enterprise/api-plugin-site.md`) at parity with the upstream feature surface. ## What changes - ADDED: `SITE_CODES` / `SiteError` — `defineCodes("SITE", […])` registry of 14 codes (`INVALID_OPTIONS`, `SERVICE_NOT_AVAILABLE`, `SITE_NOT_FOUND`, `SITE_LIMIT_EXCEEDED`, `DOMAIN_LIMIT_EXCEEDED`, `DOMAIN_ALREADY_CLAIMED`, `LAST_DOMAIN_REMOVAL`, `DOMAIN_NOT_FOUND`, `INVALID_DOMAIN`, `VALIDATION`, `BOOTSTRAP_FAILED`, `STORAGE_ERROR`, `ROUTE_PREFIX_COLLISION`, `RESOLUTION_FAILED`) extending `SuaveplanError`. - ADDED: `SiteSchema`, `CreateSiteSchema`, `UpdateSiteSchema (.strict)`, `SitePublicInfoSchema`, `SiteIdParamSchema`, `SiteSettingsSchema`, `SiteThemeSchema`, `SiteStateNumberSchema` — inlined Zod surface for the site entity. - ADDED: `DomainSchema`, `AddDomainRequestSchema/ResponseSchema`, `RemoveDomainRequestSchema/ResponseSchema`, `DomainResolveParamSchema/ResponseSchema` — inlined Zod surface for the domain entity. - ADDED: `ApplePushConfigSchema`, `FirebasePushConfigSchema`, `PushConfigSchema`, `PushConfigValidationResponseSchema`, `validatePushConfig` — push-config schema + pure validator mirroring the SurrealDB `fn::site::push::validate`. - ADDED: `SitePluginOptionsSchema`, `SitePluginConfig`, `SitePluginOptionsInput`, `DEFAULT_CONFIG`, `mergeConfig` — Zod-validated boundary for plugin options with frozen defaults. - ADDED: `normalizeDomain` — pure, mirroring `fn::site::domain::normalize`. - ADDED: `SiteDb` — structural db port consumed by services (positional-bind `query` + typed `create` / `update`). - ADDED: `SiteService` — canonical CRUD: `create`, `createDefaultSite`, `getById`, `requireById`, `listByOrganization`, `countByOrganization`, `update`, `updateSettings`, `updateState`, `addDomain`, `removeDomain`, `archive`, `getActive`, `search`. Emits `site:settings:updated` natively (no monkey-patching). Caps enforced via plugin config. Bugfix: `addDomain` now persists the new domain (upstream produced an unused local). - ADDED: `DomainResolverService` — `resolve` envelope, `isDomainTaken`, `resolveOrThrow`. Wraps SurrealDB `fn::domain::search`. Emits OTel counter + duration histogram. - ADDED: `createDomainResolverMiddleware` — Hono middleware factory injecting `site` / `organization` / `siteId` / `organizationId` into request context. - ADDED: `createPublicSiteRoutes` — `GET /sites/resolve/{domain}`, `GET /sites/{id}/public`. - ADDED: `createPrivateSiteRoutes` — `POST /sites`, `PATCH /sites/{id}`, `POST /sites/{id}/domains`, `DELETE /sites/{id}/domains`. - ADDED: `createSitePlugin` — `definePlugin` factory wrapping the genesis `PluginDescriptor` with the canonical 4-phase lifecycle. Subscribes to `organization:created` when `autoCreateDefaultSite` is on, emits `site:bootstrap:failed` on failure, registers three services (`siteService`, `domainResolver`, `domainResolverMiddleware`), mounts route trees under configurable `routePrefix`. - ADDED: `getSiteInstruments` / `resetSiteInstruments` / `SiteInstruments` — OTel bundle (5 instruments) with cache reset for test rebinding. - ADDED: `services/registry-augmentation.ts` — declares `siteService`, `domainResolver`, `domainResolverMiddleware`, `app`, `db` on `ServiceRegistry`. - ADDED: `surql/tables/site.surql` — per-table migration with state constants, indices, and the `fn::domain::search` function. ### Source mapping | Upstream source | Genesis location | Notes | |---|---|---| | `src/index.ts` | `src/index.ts` | Re-exports broadened to expose every documented symbol. | | `src/plugin.ts` | `src/plugin/plugin.ts` | Rewritten with `definePlugin()` from `@suaveplan/api-plugin`; every `as any` removed; monkey-patch dropped; `site:bootstrap:failed` event added. | | `src/config.ts` | `src/schemas/options.ts` | Switched from hand-rolled `mergeConfig` to Zod-validated boundary; `DEFAULT_CONFIG` frozen. | | `src/middleware/domain-resolver.middleware.ts` | `src/middleware/domain-resolver.ts` | Uses `normalizeDomain` before resolve. | | `src/routes/public.routes.ts` | `src/routes/public-routes.ts` | OpenAPI route definitions preserved; sanitised public projection. | | `src/routes/private.routes.ts` | `src/routes/private-routes.ts` | Adds `DELETE /sites/{id}/domains`; bodies Zod-validated through @hono/zod-openapi. | | `src/services/site.service.ts` | `src/service/site.ts` | `addDomain` persistence bug fixed; event emit moved into the service; OTel counters added; structural `SiteDb` port. | | `src/services/domain-resolver.service.ts` | `src/service/domain-resolver.ts` | OTel counter + histogram; codes embedded in result envelope; `resolveOrThrow` helper. | | `src/types/site.types.ts` | `src/schemas/site.ts` + `src/schemas/domain.ts` + `src/schemas/push.ts` + `src/types/types.ts` | Type-only file split into Zod schemas (per-feature) and runtime-free types. | | `surql/tables/site.surql` | `surql/tables/site.surql` | Trimmed `fn::site::*` helpers that the application-tier now owns (`normalizeDomain`, `createDefaultSite`) — covered by `### Excluded`. | ### Excluded from this migration The genesis `0.1.0` ships at functional parity with the upstream public surface (services + routes + middleware + plugin + migration). The §3 enterprise-doc enhancements that depend on packages not yet landed on genesis are scoped out with concrete justifications — NOT silently dropped: - **`fn::site::*` SurQL helper suite** (e.g. `fn::site::create`, `fn::site::push::validate`, `fn::site::settings::update`) — application-tier `SiteService` now owns every call site. Keeping those helpers in the migration would duplicate logic between two writers and contradict §0a's "no implicit drops"; the application tier IS the canonical writer in genesis (matching the recent `api-plugin-import` / `api-plugin-search` precedent). The migration ships only `fn::domain::search` because the resolver still delegates pattern matching to the database. - **JWT / permissions / rate-limit / audit middleware wiring** — depends on `@suaveplan/api-plugin-rate-limit` + `@suaveplan/api-plugin-audit` already landed in genesis, but the integration knob is the host app's `app.use(prefix, middleware)` Hono pattern. The site plugin does NOT hard-depend on them so excluding integration from THIS migration does not block consumers from layering them on top. - **ACME / Let's Encrypt automation, edge-CDN cache busting, sitemap auto-gen, theme draft/publish workflow** — every item is a §4 future enhancement, not a §2 current-state capability. None are part of the upstream `1.0.21` shipped feature surface. - **Benchmarks under `src/__benchmarks__/`** — the monorepo benchmark wave is a separate gate; the site plugin will receive its mitata harness in that wave. ## Migration No external consumers exist yet (genesis-only). For internal consumers: - Old: `import { createSitePlugin } from "@suaveplan/api-plugin-site"` from the upstream `1.0.x` line. - New: same import, but the descriptor's lifecycle hooks now use the genesis `PluginContext` (typed `getOwnConfig`, augmented `ServiceRegistry["siteService"|"domainResolver"|"domainResolverMiddleware"]`). - Event-bus events are now strongly typed via `SiteEventMap`; consumers that wrote `bus.on("site:settings:updated" as any, ...)` should drop the cast. - The `addDomain` bug-fix means a site's `domains` array is actually updated after a successful call. Consumers that worked around the bug by manually rewriting the row should remove the workaround. ## Impact ### Affected dependents `bunx turbo ls --filter=...@suaveplan/api-plugin-site` enumerates zero dependents on genesis today (this is the first landing). The package will be picked up by future genesis-original packages (e.g. `api` host orchestrator). No dependent sweep required for this change.