Skip to Content

← All archived runs

Run: admin-integrations-registry

run.md

Run: admin-integrations-registry

  • issue: #453 # canonical home of the spec + state (labels, comments)
  • branch: claude/admin-integrations-registry-s0b72f
  • pr: #454

00_intake/stub.md

Stub: Admin dashboard — integrations registry

  • feature-slug: admin-integrations-registry
  • epic: admin-dashboard-data
  • personas: Admin
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: admin-data-foundation
  • sequence: 6 of 7

Problem

The IntegrationsCard (connected systems with status, last-sync, record counts) and the AvailableIntegrationsCard are dummy/static. There is no model recording which integrations a tenant has. Per the user's decision, this round delivers a config registry — a per-tenant record of integrations with a manually/heuristically set status — not live OAuth or sync.

Proposed change

  • Add a per-tenant integration registry model: name, type (CRM / Notifications / Automation / Billing), status (healthy | warning | disconnected), last-sync label, optional record count, impact text, and an optional issue note.
  • Wire the IntegrationsCard to the tenant's real registry rows.
  • Wire the AvailableIntegrationsCard to a defined catalogue of integrations not yet added.
  • Build the config UI: admin can add an integration to the registry and set/update its status.

Acceptance criteria (rough)

  • Admin can add an integration and set its type, status, and details; it persists per tenant.
  • IntegrationsCard renders the tenant's real registry (no empty mock array).
  • AvailableIntegrationsCard shows catalogue entries the tenant has not added.
  • Status is set manually/heuristically — no live OAuth/sync is attempted.

Out of scope (this feature)

  • Live third-party connections, OAuth flows, real sync jobs, automated health checks (future epic).
  • Webhook configuration (tracked by the setup checklist, not here).

Notes for Define

  • Decide the integration catalogue (the "available" list) and the type/status enums. The integrationsConnected readiness check in admin-readiness-gaps derives from this model.
  • touches: packages/services/src/db/models (new integration model), apps/web/components/admin/dashboard/{integrations-card,available-integrations-card}.tsx.

01_define/output/spec.md

Spec: Admin integrations registry

  • slug: admin-integrations-registry
  • issue: #453
  • personas: Admin
  • touches: packages/services/src/db/models/tenant-integration.ts (new), packages/services/src/db/models/index.ts, packages/services/src/db/services/tenant-integration (new), packages/services/src/db/services/index.ts, apps/web/lib/integrations-catalogue.ts (new), apps/web/lib/admin-dashboard-data.ts, apps/web/app/(app)/admin/integrations (new page + actions + form), apps/web/components/admin/dashboard/integrations-card.tsx, apps/web/components/admin/dashboard/available-integrations-card.tsx, apps/web/app/(app)/admin/dashboard/page.tsx, apps/web/lib/mock/admin.ts
  • complexity: complex

Problem

On the admin "Platform setup" dashboard, the IntegrationsCard (connected systems with status, last-sync and impact) renders from an empty mock array (integrations: [] in lib/mock/admin.ts), and the AvailableIntegrationsCard renders a hardcoded constant list. There is no model recording which integrations a tenant has, so the integrations stage of setup can never reflect real state and the downstream integrationsConnected readiness check (built later in admin-readiness-gaps) has nothing real to derive from. This is feature 6 of 7 in the admin-dashboard-data epic, which advances Build the Bridge / Q2 2026 Objective 3 — validate technical infrastructure & payout flow by making each dashboard section operational from real, per-tenant data. Unlike the escalation and commercial configs (single key/value flags stored in TenantSetting), an integrations registry is a per-tenant collection of rich rows, so it warrants its own model. Per the epic's decision, this round delivers a config registry — a per-tenant record of integrations whose status is set by the admin — not live OAuth or sync.

Proposed change

  • New per-tenant TenantIntegration model (packages/services/src/db/models/tenant-integration.ts), following the established tenantPlugin + schemaPlugin + softDeletePlugin model pattern. Fields:
    • catalogueKey — string, the catalogue entry this row was added from (e.g. salesforce). Unique per tenant (a tenant adds a given catalogue integration once).
    • name — string (display name, seeded from the catalogue entry).
    • type — enum CRM | Notifications | Automation | Billing.
    • status — enum healthy | warning | disconnected, default warning.
    • lastSyncLabel — optional string (display-only label, e.g. "Synced 2h ago"; no real sync).
    • impact — optional string (what this integration powers, e.g. "Lead sync").
    • issueNote — optional string (shown by the card when status is warning).
    • Indexed { tenantId: 1, catalogueKey: 1 } unique. Exported from models/index.ts.
  • New TenantIntegrationService (services/tenant-integration/ with index.ts + instance.ts, registered in services/index.ts as tenantIntegrationService), exposing:
    • listIntegrations(tenantId) — the tenant's rows (newest-first or stable order).
    • addIntegration(tenantId, { catalogueKey, name, type, status?, lastSyncLabel?, impact?, issueNote? }) — creates a row; rejects/ignores a duplicate catalogueKey for that tenant.
    • updateIntegration(tenantId, id, { status?, lastSyncLabel?, impact?, issueNote? }) — edits an existing row's status and detail fields.
    • removeIntegration(tenantId, id) — soft-deletes a row (admin removed it wrongly-added).
    • Exports an IntegrationStatus / IntegrationType type and a row type for the web layer.
  • Catalogue constant at apps/web/lib/integrations-catalogue.ts — the defined "available" list with display metadata, replacing the inline AVAILABLE_INTEGRATIONS array in the card. Each entry: { key, name, type, abbr, color }. This round's catalogue:
    • salesforce → Salesforce (CRM), hubspot → HubSpot (CRM)
    • stripe → Stripe (Billing), xero → Xero (Billing)
    • teams → Teams (Notifications), zapier → Zapier (Automation)
  • Config UI — a dedicated /admin/integrations page (server component + actions.ts + _components/ form, mirroring the /admin/escalation and /admin/commercial pattern):
    • The server page reads the tenant's registry rows + the catalogue, and renders the list of added integrations alongside an "add" control listing catalogue entries not yet added.
    • Add: the admin picks a catalogue entry; the row is created seeded from the catalogue (name, type) with a default status of warning ("needs attention") and empty detail fields. New rows are therefore added but unverified, not auto-counted as connected.
    • Edit: the admin can change a row's status (healthy | warning | disconnected) and edit its last-sync label, impact text, and issue note, then save.
    • Remove: the admin can remove a wrongly-added integration.
    • Server actions follow the runActionBody / resolveActionContext({ allowedRoles: ["admin"] }) convention and revalidatePath("/admin/integrations") + revalidatePath("/admin/dashboard"). Sentence-case copy throughout.
  • Wire the loader. Add an integrations slice to loadAdminDashboardData (apps/web/lib/admin-dashboard-data.ts): the tenant's real rows mapped to the card's shape, plus the available catalogue entries (catalogue minus the keys already added). AdminDashboardData gains a typed integrations field.
  • Wire IntegrationsCard to real rows. The dashboard passes the real registry rows (from the loader) instead of d.integrations mock. The card already renders name / status / issue / impact — no shape change beyond sourcing real data; its "Manage integrations" footer already links to /admin/integrations. The empty-state copy already exists for a tenant with no integrations.
  • Wire AvailableIntegrationsCard to the catalogue minus added. The card takes an available prop (catalogue entries the tenant has not added) instead of its hardcoded constant, so adding an integration removes it from "available". Its display metadata (abbr, colour) comes from the catalogue constant.
  • Drop the integrations mock. Remove the integrations: [] array's use from lib/mock/admin.ts wiring (the dashboard no longer reads it for this section).

Acceptance criteria

  • An admin can add an integration from the catalogue on /admin/integrations; it persists per tenant in the new TenantIntegration collection, seeded with the catalogue entry's name and type and a default status of warning.
  • An admin can edit an existing integration's status (healthy | warning | disconnected) and its last-sync label, impact text, and issue note, and the changes persist per tenant.
  • An admin can remove a wrongly-added integration from the registry.
  • The IntegrationsCard on the admin dashboard renders the tenant's real registry rows (status, issue, impact) — never a mock array; a tenant with no integrations shows the existing empty state.
  • The AvailableIntegrationsCard shows only catalogue entries the tenant has not added; adding one removes it from the available list (and vice-versa on removal).
  • tenantIntegrationService exposes listIntegrations / addIntegration / updateIntegration / removeIntegration and a typed row/status/type contract; adding a duplicate catalogue entry for the same tenant does not create a second row.
  • Status is set manually by the admin (defaulted on add, then editable) — no live OAuth, sync, or automated health check is attempted.

Out of scope

  • Live third-party connections, OAuth flows, real sync jobs, automated health checks (future epic).
  • Webhook configuration (tracked by the setup checklist, not here).
  • Per-integration record counts (the card's record-count display is not populated this round).
  • Free-form / custom integrations outside the catalogue — adds are catalogue-only this round.
  • The derived integrationsConnected readiness check and config-gap recomputation — owned by admin-readiness-gaps (this feature provides the model it derives from).
  • Adding catalogue entries from the UI — the catalogue is a code-defined constant this round.

Open questions

  • none

02_build/output/notes.md

Build notes: admin-integrations-registry

  • branch: claude/admin-integrations-registry-s0b72f
  • commits: feat: admin-integrations-registry — model + service, feat: admin-integrations-registry — catalogue, loader + config UI

What changed

  • New model packages/services/src/db/models/tenant-integration.ts — per-tenant TenantIntegration collection with catalogueKey, name, type (CRM | Notifications | Automation | Billing), status (healthy | warning | disconnected, default warning), and optional lastSyncLabel / impact / issueNote. Unique index on { tenantId, catalogueKey }. Uses the standard schemaPlugin + softDeletePlugin + tenantPlugin stack. Exports INTEGRATION_TYPES / INTEGRATION_STATUSES and their literal types. Exported from models/index.ts.
  • New service packages/services/src/db/services/tenant-integration/TenantIntegrationService with listIntegrations / addIntegration (duplicate catalogueKey rejected) / updateIntegration / removeIntegration (soft delete). Instance tenantIntegrationService registered in services/index.ts; AddIntegrationInput / UpdateIntegrationInput / TenantIntegrationService exported.
  • Catalogue constant apps/web/lib/integrations-catalogue.ts — the defined available list (Salesforce/HubSpot → CRM, Stripe/Xero → Billing, Teams → Notifications, Zapier → Automation) with key/name/type/abbr/color and a getCatalogueEntry helper. Shared by the loader, the config page, and the available card.
  • Loader apps/web/lib/admin-dashboard-data.ts — adds a typed integrations slice (connected real rows mapped to the card shape + available = catalogue minus added keys).
  • IntegrationsCard — keyed on the stable row id, renders a last-sync label when present; now fed the real integrations.connected rows.
  • AvailableIntegrationsCard — takes an available prop (now a server component) sourced from the catalogue minus added entries, with an all-added empty state.
  • Config UI apps/web/app/(app)/admin/integrations/ — server page.tsx (admin-gated), actions.ts (add / update / remove, following the resolveActionContext + runActionBody + zodToActionError pattern, each revalidating /admin/integrations and /admin/dashboard), and a client IntegrationsConfig form: add a catalogue entry (status defaults to warning), edit each row's status + last-sync/impact/issue note, remove a row.
  • Dashboard page wires both cards to the real loader slice; the unused integrations: [] mock field was dropped from lib/mock/admin.ts.

Acceptance criteria status

  • Admin can add a catalogue integration on /admin/integrations; persists per tenant seeded with the catalogue name/type and default warning status.
  • Admin can edit status (healthy | warning | disconnected) + last-sync, impact, issue note; changes persist.
  • Admin can remove a wrongly-added integration (soft delete).
  • IntegrationsCard renders the tenant's real registry rows; empty state shown when none.
  • AvailableIntegrationsCard shows only catalogue entries not yet added; adding one removes it (and removal restores it) via the shared catalogue-minus-added computation.
  • tenantIntegrationService exposes list/add/update/remove + typed contract; duplicate catalogueKey is rejected (unique index + duplicate-key guard).
  • Status is admin-set (defaulted on add, then editable) — no OAuth/sync/health check is attempted.

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Ship reads them back from gh pr checks. No check is expected to fail.

Notes for review

  • Storage is a dedicated model (a per-tenant collection of rows), unlike the escalation/commercial configs that use the TenantSetting key/value store — appropriate for a list of rich rows and the seam admin-readiness-gaps will derive integrationsConnected from.
  • The catalogue is code-defined; adds are catalogue-only. type is a structurally identical union in both the catalogue (UI) and the model (DB) to avoid a client→/server value import.
  • A separate, pre-existing components/admin/integrations-card/ (AdminIntegrationsCard, backed by /app/(app)/integrations/actions) is an unrelated feature and was left untouched.

03_ship/output/changelog.md

Changelog: admin-integrations-registry

Persona: admin

Manage your organisation's integrations

Admins can now track their organisation's integrations from the platform setup dashboard:

  • Add integrations from the catalogue and set each one's status, last sync, and impact, from the new integrations page — your changes are saved per organisation.
  • The integrations and available-integrations cards on your setup dashboard now show your real registry — what's connected and what's still available to add — instead of sample content.

This is part of making your platform setup dashboard reflect your organisation's real configuration.

03_ship/output/investor-update.md

Admins can now manage their organisation's integrations

We've made the integrations section of the admin platform setup dashboard real: admins can now record which systems their organisation uses — CRM, billing, notifications, automation — and set each one's status, so the dashboard reflects their actual integration setup instead of placeholder content. It turns another part of the admin control surface into a working control panel, and lays the per-organisation record that the platform's go-live readiness checks build on.

  • Admins can add integrations from a catalogue and set each one's status, last sync, and impact — saved per organisation.
  • The integrations and available-integrations cards on the setup dashboard now reflect the real registry instead of sample content.
  • This is the record later automated health and readiness checks will read from — no new infrastructure needed when they arrive.

This advances the Build the Bridge initiative and our Q2 2026 objective to validate technical infrastructure & payout flow — bringing each organisation's integration setup under explicit, real control.

03_ship/output/pr.md

Ship: admin-integrations-registry

  • PR: #454 — https://github.com/sustentus/sustentus/pull/454
  • branch: claude/admin-integrations-registry-s0b72f
  • CI: format <pending> · lint <pending> · typecheck <pending> · preview build <pass>
  • technical docs: no technical docs impact — apps/docs/technical describes the db/ layer only at a high level ("Mongoose models, db services, plugins"); it does not enumerate individual models, services, or admin routes (mirrors the escalation/commercial siblings).
  • business docs: no business docs impact — no feature-role-matrix/service-journey entity covers admin platform-setup config; this is operational configuration capture, not a service-journey step (mirrors the siblings).
  • release notes: both — investor draft + changelog entry included in this PR (changelog published to apps/help/app/changelog/page.mdx).

Review summary

  • Unique index ignored soft-deletes (correctness, confirmed) — {tenantId, catalogueKey} counted soft-deleted rows, so removing then re-adding an integration threw a duplicate-key error, breaking the core remove/re-add loop. Fixed: added partialFilterExpression (matching csat.ts/lead.ts).
  • Update reported success on a missing row (correctness) — updateIntegration returns null (not a throw) when the row was concurrently removed or the id is stale, and runActionBody treated that as success (green toast, nothing saved). Fixed: the action now throws "This integration no longer exists." when the update returns null.
  • Duplicated IntegrationType + available-list filter (cleanup) — Fixed: catalogue now sources IntegrationType from the model via @sustentus/services/server; availableCatalogueEntries(addedKeys) is the single helper used by both the loader and the config page.
  • Dashboard row id used catalogueKey (consistency) — Fixed: loader now maps id: row._id.toString(), matching the config page and the documented row-id semantics.
  • Accepted as-is: the service's per-field partial-update shape and broad AddIntegrationInput mirror the tenant-setting precedent; the status→label vocabulary in the form/card is left inline (consolidating it is a larger cross-component refactor, out of scope).

Acceptance check (vs spec)

  • Admin can add a catalogue integration on /admin/integrations; persists per tenant seeded with the catalogue name/type and default warning status.
  • Admin can edit status + last-sync, impact, issue note; changes persist.
  • Admin can remove a wrongly-added integration (soft delete).
  • IntegrationsCard renders the tenant's real registry rows; empty state when none.
  • AvailableIntegrationsCard shows only catalogue entries not yet added; adding/removing one moves it between the lists.
  • tenantIntegrationService exposes list/add/update/remove + typed contract; duplicate catalogueKey rejected (unique index + duplicate-key guard).
  • Status is admin-set (defaulted on add, then editable) — no OAuth/sync/health check.
  • Follow-up fix: /admin/integrations added to the proxy route policy (admin-only) so the page loads instead of bouncing to the role home.

Merge & deploy

  • merged: <pending merge-approved gate>
  • deploy: <pending merge>