Integrations Framework Implementation Guide
Task 41 turns the integrations layer into a reusable framework instead of a collection of one-off partner hooks.
Purpose
Task 41 turns the integrations layer into a reusable framework instead of a collection of one-off partner hooks.
The implementation now provides:
- adapter registration in
@tov-plus/integrations - connection lifecycle and credential redaction
- scoped integration principals
- planner management APIs and UI
- admin visibility APIs and UI
- inbound callback handling
- worker-driven background sync execution
- migration artifacts for durable SQL authoring
Adapter model
Adapters are registered in packages/integrations/src/contracts/integration.ts.
Each adapter declares:
- identity:
integration_key, name, description, category, direction, mode - supported capabilities
- supported action keys
- config fields
- secret fields
- whether background sync and inbound callbacks are supported
- validation rules
- optional sync implementation
- optional inbound callback mapping
The framework currently ships two example adapters:
printing_partnervenue_ops
printing_partner is the primary proof-of-framework adapter. It supports validation, background sync, inbound callbacks, redacted credentials, and planner-managed lifecycle.
Connection lifecycle
Planner lifecycle routes:
GET /v1/occasions/:occasion_id/integrationsPOST /v1/occasions/:occasion_id/integrations/connectionsPATCH /v1/occasions/:occasion_id/integrations/connections/:connection_id
Lifecycle states:
draftenableddisabledrevoked
Validation behavior:
- adapters validate
config,secrets, andscope - invalid connections may exist as
draft enabledis blocked when validation has errors- degraded health is surfaced when configuration drift exists
Principal and scope model
Each connection gets a generated integration principal:
principal_type = integration_principalsubject_id = integration:<connection_id>- unique
key_id - explicit scope with
platform,occasion, orevent - explicit allowed
action_keys
The server creates direct grants for the integration principal during connection creation so the permission model stays explicit and auditable.
Credential handling
The current runtime keeps raw secrets only in the shared in-memory integration store and returns only redacted summaries from APIs and UI payloads.
Current baseline:
- raw secrets are never returned from planner/admin APIs
- UI surfaces display only
redacted_secrets - secret rotation creates a new
key_id - last-used metadata is tracked for resolved integration keys
- inbound callbacks validate
x-tov-integration-secret
The SQL migration adds the durable table structure needed for future encrypted-at-rest persistence.
Worker handoff
The worker now consumes the same shared integration store as the server bootstrap seam.
Background flow:
- enabled pull-sync connections are discovered by
integrationSyncScheduler - the scheduler enqueues
integrations.sync_partner_state integrationSyncHandlerresolves the adapter- adapter sync updates health and
last_sync_at - activity is recorded and
integration.sync_completedwebhook events are emitted
Planner and admin surfaces
Planner:
frontend/dash/src/pages/IntegrationsPage.tsx- catalog visibility
- connection creation
- lifecycle toggles
- scope/action visibility
- recent activity
Admin:
frontend/admin/src/pages/IntegrationsPage.tsx- platform-wide connection visibility
- failing-connection counts
- support-oriented activity feed
Inbound and outbound interaction seams
Inbound:
POST /v1/integrations/callbacks/:integration_key/:connection_id- adapter validation and payload mapping
- shared-secret validation
- activity recording
Outbound/background:
- adapter
syncConnection - worker retries through the normal job framework
- webhook emission for sync completion
Future extension points
The next task-specific integrations should plug into the existing seams rather than bypass them:
- add a new adapter definition
- add adapter validation
- add sync/callback mapping
- extend UI labels or helper copy only if needed
- keep connection persistence generic unless the data is truly adapter-specific