Connected Account Integration Guide
How clinics connect external services they own (Google Calendar, Slack, HubSpot, future EHRs) to RestartiX, and how feature-tier engineers add new connector implementations.
Foundation 1C.5 ships the framework (Cat B — Connected Account). The catalog (
integration_services) is empty at foundation — the first per-service connector and its catalog row land with the F-tier consumer that needs it (likely Google Calendar at F4 Scheduling). This page documents the contract; per-connector setup recipes (OAuth scopes, redirect URIs, etc.) get added when each connector ships.
Architectural model
Two layers, both required for an OAuth connection to work end-to-end:
- Cat A — OAuth client. RestartiX registers ONE Google Cloud project, ONE Slack app, ONE HubSpot OAuth app per provider. The
client_id+client_secretper provider live inplatform_service_providerskeyed by capability slugoauth_client_<provider>(e.g.oauth_client_google). Operators rotate these via the Console superadmin endpoints; runbook in credential-rotation.md. - Cat B — Per-clinic connection. Each clinic admin connects their own Google Calendar / Slack workspace / HubSpot portal via the clinic admin UI. The platform stores the resulting access + refresh tokens in
organization_integrations.credentials_encrypted(AES-GCM, encrypted at rest).
The two layers compose: Cat A holds the keys to mint tokens; Cat B holds the tokens themselves.
Auth types
integration_services.auth_type is locked at three values. New auth types require both a migration extending the CHECK constraint AND a Connector implementation that handles the new shape.
auth_type | Created via | Foundation status |
|---|---|---|
api_key | POST /v1/organizations/{id}/integrations | Supported. Clinic admin pastes the API key in the UI; the service validates + encrypts + persists. |
webhook_in_only | POST /v1/organizations/{id}/integrations | Supported. Connection holds only an inbound signing secret (used by Cat D inbound webhook framework, 1C.6). |
oauth2 | /oauth/callback/{provider} (deferred) | Schema + framework only. The OAuth callback handler ships with the first OAuth-using F-tier consumer. |
The service refuses direct creation of oauth2 connections through the per-org endpoint with 400 oauth_requires_callback_flow. OAuth needs the redirect-based consent dance — the callback handler resolves the platform OAuth client (Cat A), exchanges the auth code for tokens, and writes the organization_integrations row via AdminPool.
Lifecycle
organization_integrations.status is locked at five values. The user-facing recovery differs by status — expired shows a "Reconnect" button while error shows "Provider unavailable, retrying":
| Status | Meaning |
|---|---|
pending | OAuth flow started but not yet completed. Auto-deleted if no transition in 30 min. |
connected | Auth working, healthy. |
expired | OAuth refresh-token rejected. Clinic must re-OAuth. |
revoked | Clinic admin disconnected via UI. Terminal — clinic creates a new connection. |
error | Provider returned 401/403 repeatedly via healthcheck or runtime. |
Connector contract (for F-tier engineers)
Each new integration ships in the same PR as:
A migration that adds an
integration_servicesrow with the newslug. (For OAuth connectors: also extendchk_psp_capability_providerin migration 000015 to allow the newoauth_client_<provider>capability + bootstrap the platform OAuth client row.)A
Connectorimplementation ininternal/core/integrations/connectors/<slug>/that registers itself viainit():gopackage google_calendar import "github.com/restartix/restartix-platform/services/api/internal/core/integrations" func init() { integrations.Register(&Connector{}) } type Connector struct{} func (*Connector) Slug() string { return "google_calendar" } func (*Connector) ValidateConfig(ctx context.Context, config map[string]any) error { // surface humanised messages — they reach the clinic admin via 400 invalid_config. } func (*Connector) ValidateCredentials(ctx context.Context, creds map[string]any) error { // for OAuth connectors this is effectively a no-op; the callback handler // builds a known-good envelope. } func (*Connector) Healthcheck(ctx context.Context, creds []byte, config map[string]any) error { // ping the external service with a tight timeout (≤ 5s). Return nil on success. } func (*Connector) RefreshOAuthToken(ctx context.Context, creds []byte) ([]byte, error) { // OAuth-only path. Non-OAuth: return nil, nil. }An import of the new package somewhere in the binary's wiring tree (typically
cmd/api/main.go) so theinit()actually runs at process start. The framework returns502 connector_not_registeredif a catalog row references a slug nobody has imported.
Endpoints
The full endpoint set lives in the OpenAPI spec (apps/docs/openapi.yaml). Quick reference:
GET /v1/integration-services— public catalog (no auth, rate-limited per IP underpublic_resolve).POST /v1/organizations/{id}/integrations— create (API-key + webhook_in_only only).GET /v1/organizations/{id}/integrations[?status=&service_slug=&limit=&offset=]— list.GET /v1/organizations/{id}/integrations/{integrationId}— read (credentials never returned).PATCH /v1/organizations/{id}/integrations/{integrationId}— updatetitleand/orconfig.DELETE /v1/organizations/{id}/integrations/{integrationId}— soft-delete (status='revoked').POST /v1/organizations/{id}/integrations/{integrationId}/test— runs the connector'sHealthcheck; surfaces success/failure to the clinic admin.
All per-org routes gate on organizations.manage_integrations (admin-only at the system role template). Per P47, the {id} URL parameter is verified against the principal's CurrentOrganizationID before the handler runs.
Data classification
organization_integrations.credentials_encrypted is auth_secret — never logged, never included in any egress, even support_export. The config column is org_internal with support_export so platform support can investigate broken integrations; if a connector's config payload would carry sensitive material, that material belongs in credentials_encrypted (split the field). Full registry rows in data-classification.md → Connected Accounts.