@quxkit/integration-kit

Synced from integration-kit/README.md — the repo is canonical.

QuxKit · tanzanite stone · canonical capabilities, a registry of declarative adapters, tenant connections, a runtime that interprets data

status licence npm

The integration engine — external APIs become standardized capabilities, each integration is stored as a reusable data artifact, and every integration built once is available to every tenant afterwards. Code is the engine; data is the integration library.

                        ┌─────────────────────────────┐
                        │         your app            │
                        │  execute("identity.find")   │
                        └──────────────┬──────────────┘
                                       │ canonical contract (21 capabilities)
                        ┌──────────────▼──────────────┐
                        │      integration-kit        │
                        │                             │
                        │  registry   runtime   auth  │
                        │  lifecycle  templates retry │
                        └──────┬───────────────┬──────┘
                     artifacts │               │ credential ref
                        ┌──────▼──────┐ ┌──────▼──────┐
                        │ PostgreSQL  │ │ your secret │
                        │  registry + │ │   manager   │
                        │ connections │ └─────────────┘
                        └──────┬──────┘
                               │ one certified integration, every tenant
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
         Salesforce         HubSpot        Acme PM API

                 @quxkit/integration-kit — Apache-2.0

Rendered diagrams (mermaid): docs/DIAGRAMS.md.

Where it sits in the family

identity-kit ──▶ who the user is
tenant-kit ────▶ which tenant is asking          integration-kit
billing-kit ───▶ what the usage costs      ────▶ what the OUTSIDE world
mail-kit ──────▶ what the app sends              can do for that tenant:
comm-kit ──────▶ what the app discusses          one contract, any provider

The kits own what happens inside your application. integration-kit owns the seam to everything outside it: the CRM, the ERP, the property manager, the custom API a customer wired together in 2019. Connection rows carry the same opaque text tenant_id the rest of the family keys on, so tenant-kit RLS policies extend to it naturally.

The problem it solves

Every SaaS team builds the same integrations, one bespoke module at a time, and each one is code: written for one customer, entangled with their tenant, maintained forever. This kit inverts that.

  • A canonical contract. Your application calls identity.find or message.send. It never learns what Salesforce is.
  • Integrations are rows, not modules. An integration is a manifest plus declarative artifacts in PostgreSQL — validated on write, interpreted at runtime, versioned and certified. There is no /integrations/salesforce/ directory anywhere, which is the design decision the whole engine hangs on.
  • Global definitions, tenant connections. "How do we talk to Salesforce?" is answered once, in the registry. "How does Acme's Salesforce connect?" is a connection: configuration plus an opaque credential reference. One certified integration serves tenant one and tenant ten thousand.
  • Built for a builder. Because an adapter is data validated against a schema — never code that executes — an AI agent can safely generate one. The certification lifecycle, not the generator, decides what customers use.

Quickstart

import { createIntegrationEngine } from '@quxkit/integration-kit';
import { pgExecutor } from '@quxkit/integration-kit/pg';
import { memoryCredentialStore } from '@quxkit/integration-kit/memory';
import pg from 'pg';

// 1. The engine: a database, a secret seam, and injected side effects.
const engine = createIntegrationEngine({
  db: pgExecutor(new pg.Pool({ connectionString: process.env.DATABASE_URL })),
  credentials: memoryCredentialStore({ 'vault://acme/salesforce': { api_key: 'sk_…' } }),
});

// 2. Register an integration and a version — the manifest declares what it can do.
await engine.registry.define({ slug: 'salesforce', name: 'Salesforce', provider: 'salesforce' });
const version = await engine.registry.createVersion('salesforce', {
  version: '1.0.0',
  manifest: {
    contract_version: '1.0',
    base_url: 'https://api.salesforce.example',
    capabilities: { 'identity.find': true, 'message.send': true },
    authentication: { type: 'api_key' },
  },
});

// 3. The artifacts ARE the integration. Declarative, validated, no code.
await engine.registry.addArtifact(version.id, {
  kind: 'auth',
  definition: { type: 'api_key', in: 'header', name: 'X-Api-Key', valueFrom: '$secret.api_key' },
});
await engine.registry.addArtifact(version.id, {
  kind: 'operation',
  definition: {
    operation: 'message.send',
    request: { method: 'POST', path: '/messages', body: { to: '$input.recipientId', text: '$input.body' } },
    response: { id: '$response.id', createdAt: '$response.created_at' },
    errors: [{ status: 429, action: 'retry' }],
  },
});

// 4. Certify and publish — only then can customers touch it.
await engine.registry.promote(version.id, 'generated');
await engine.registry.promote(version.id, 'testing');
await engine.registry.promote(version.id, 'certified');
await engine.registry.promote(version.id, 'published');

// 5. Connect a tenant. The secret stays in the credential store.
const connection = await engine.connections.connect({
  tenantId: 'acme',
  slug: 'salesforce',
  credentialRef: 'vault://acme/salesforce',
});

// 6. The call your application actually makes.
const sent = await engine.execute({
  connectionId: connection.id,
  capability: 'message.send',
  input: { recipientId: 'u_42', body: 'Welcome aboard' },
});
// -> { data: { id, createdAt }, integration: 'salesforce', version: '1.0.0', … }

Apply the schema once:

psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f node_modules/@quxkit/integration-kit/sql/001_integration.sql

The canonical contract

Contract 1.0 is deliberately small — 21 capabilities in 8 domains:

Domain Capabilities
identity find · create · update
organization find · create
record find · create · update · delete
conversation find · create · update
participant find · add · remove
message send · find
event subscribe · receive
file upload · download

An integration implements only what it supports; capability_unsupported is a first-class, typed answer. The vocabulary is closed — growing it is a contract version change, and test/contract.test.ts counts it to keep everyone honest.

The Integration Definition Language

An operation artifact is the engine's whole unit of behaviour:

{
  "operation": "message.send",
  "request": {
    "method": "POST",
    "path": "/conversations/{$input.conversationId}/messages",
    "body": { "to": "$input.recipientId", "text": "$input.body" }
  },
  "response": { "id": "$response.id", "createdAt": "$response.created_at" },
  "errors": [{ "status": 429, "action": "retry" }],
  "retry": { "attempts": 3, "backoff_s": [1, 5, 30] }
}

Template strings that are a reference ("$input.recipientId") keep the referenced value's type; strings with {$…} holes interpolate; everything else is literal. The roots are $input, $config (connection configuration), $secret (resolved credential material), $auth (e.g. access_token), $response, and $event in event mappings. A reference into nothing is a typed mapping failure — the exact signal a future repair agent needs — never an undefined smuggled into a request body.

There are no functions, conditionals or loops, on purpose. The 80–95% of APIs that are regular fit this language; the residue belongs in a custom adapter behind a sandbox (planned), not in a cleverer template.

Auth artifacts name their material as $secret.* references — the validator refuses a literal, so a secret cannot be written into an artifact:

{ "type": "oauth2_client_credentials", "token_url": "https://login…/token",
  "clientIdFrom": "$secret.client_id", "clientSecretFrom": "$secret.client_secret" }

Supported: none, api_key (header/query), bearer, basic, oauth2_client_credentials (with per-connection token caching).

The lifecycle

draft ──▶ generated ──▶ testing ──▶ certified ──▶ published ──▶ deprecated
                           │            ▲
                           └────────────┘ (repair loops back through testing)

Only certified and published versions execute. Certification is a real gate: every supported capability needs its artifact, event.receive needs at least one event mapping, and no recorded test may be failing. Publishing deprecates the predecessor — a partial unique index holds one published version per integration. Artifacts freeze at certification; a fix is a new version.

Connections and credentials

Integration (global)  ──▶  Connection (tenant)  ──▶  credential_ref  ──▶  your secret manager

The runtime exchanges the opaque credentialRef for short-lived material through the CredentialStore seam at execution time. Connection configuration is for things like a region or a base_url override — a secret-shaped key (api_key, password, client_secret, …) is refused with invalid_input at the door.

The runtime

engine.execute({ connectionId, capability, input }) loads the pinned or published version, checks the capability, resolves credentials, applies auth, builds the request from templates, walks the retry ladder (Retry-After wins when the provider sends one; 5xx and 429 retry by default, 4xx fail fast), and maps the response to the canonical shape. Every cycle lands in connection health — successes and typed failures alike.

Health

await engine.health.report(connection.id);
// { status: 'healthy' | 'degraded' | 'failing' | 'unused',
//   requests, errors, consecutiveFailures, lastLatencyMs, … }

Five consecutive failures is failing; day counters reset at midnight but the streak does not — provider drift is not cured by a calendar.

Events

engine.events.receive(connectionId, payload) normalizes an inbound provider payload through the version's event artifacts (declared match path/value, first match wins) into a canonical event row. An unmatched payload is stored with type: null — recorded, not guessed, with the raw payload kept for a later mapping. Transport concerns (the HTTP endpoint, signature verification) stay in the host.

What it does carefully

  • Secrets and artifacts never meet. $secret.*-only auth material, refusal of secret-shaped configuration, an opaque ref in the connection row, material resolved per execution and held only in memory.
  • Uncertified means unexecutable. The lifecycle is enforced in one transaction; connect() refuses unpublished integrations and pins only certified or published versions.
  • Artifacts are validated twice — on the way into the registry and again on the way out to the interpreter, so a hand-edited row cannot reach the runtime unchecked.
  • Failures are a typed union. Retry decisions, credential prompts and repair triggers dispatch on failure.code, never on message prose.
  • Retries are honest. One execution cycle is one health event; a Retry-After header is respected; a mapping miss after a 200 still counts against health, because schema drift is a failure of the integration even when HTTP smiled.

What it delegates

  • Secret storage — the CredentialStore seam; bring Vault, your KMS, or memoryCredentialStore in tests.
  • Tenancy and authorizationtenant_id is opaque text; tenant-kit (or your own RLS) decides who may touch which connection.
  • The webhook endpoint — signature schemes are per-provider transport; hand the verified payload to events.receive.
  • The AI builder — this kit is the substrate the builder writes into; the generator itself ships separately (see CHANGELOG → Planned).

The one database seam, printed in full:

export interface SqlExecutor {
  query<T = Record<string, unknown>>(text: string, params?: readonly unknown[]): Promise<T[]>;
  transaction<T>(fn: (tx: SqlExecutor) => Promise<T>): Promise<T>;
}

@quxkit/integration-kit/pg — the shipped executor

pgExecutor(pool) wraps a pg.Pool; nested transactions become savepoints. pg is an optional peer — the core never imports it.

@quxkit/integration-kit/memory — the dev credential store

memoryCredentialStore({ ref: { key: value } }) with set/delete, for tests and local development.

Schema

One PostgreSQL schema, integration, applied from sql/001_integration.sql: integrations, integration_versions (status + manifest), integration_capabilities, integration_artifacts (auth/operation/event definitions), integration_tests, then the tenant side: connections, connection_health, inbound_events. Text tenant_id, timestamptz throughout, text-plus-CHECK statuses, guarded and re-runnable.

Errors

Every failure is an IntegrationError carrying one variant of IntegrationFailure:

code when
invalid_input malformed slug, unknown capability string, secret-shaped configuration
not_found unknown integration, version, connection or operation artifact
invalid_state lifecycle jump, executing an uncertified version, disabled connection
definition_invalid an artifact or manifest failing DSL/contract validation, certification gaps
capability_unsupported the integration never claimed this capability
credentials no credentialRef, or the store had nothing for it
auth_failed 401/403 from the provider, or a failed token grant
upstream provider failure after retries (carries status, retryable)
rate_limited 429 after retries (carries retryAfterS when sent)
mapping a template referenced a path the scope does not have

Narrow with IntegrationError.is(e) / IntegrationError.hasCode(e, 'upstream') — both work across duplicated module copies where instanceof lies.

Development

pnpm install
createdb integration_kit_test
pnpm test          # node:test via tsx, serialized; DB suites skip without Postgres
pnpm lint && pnpm typecheck && pnpm build

INTEGRATION_KIT_TEST_DATABASE_URL overrides the default connection; REQUIRE_DB=1 turns a missing database from a skip into a failure. See CONTRIBUTING.md for how work lands.

The QuxKit family

Libraries you embed, not services you operate. Each kit owns one narrow thing and composes with the rest over shared shapes — one executor interface, one opaque tenant id, one Money type.

Package Stone What it owns
@quxkit/identity-kit gold Accounts, argon2id credentials, revocable sessions — produces a UserId.
@quxkit/tenant-kit green Tenant directory, request→tenant resolution, row-level-security isolation.
@quxkit/mail-kit ruby Sending domains, idempotent sends, delivery events, suppression, signed webhooks.
@quxkit/comm-kit turquoise Conversations, per-participant channels, bridge deliveries, inbound routing, transcripts.
@quxkit/integration-kit tanzanite Canonical capabilities, a registry of declarative adapters, tenant connections, the runtime that interprets them.
@quxkit/billing-kit blue Metering, exact pricing, a double-entry ledger, provider settlement.
@quxkit/billing-kit-adapters blue Payment providers beyond Stripe and Paddle.
tenant-kit-adapters green Enterprise SSO, SCIM provisioning, RBAC-engine bridges.
billing-kit-components blue shadcn-compatible billing UI, per seat.
@quxkit/billing-kit-mcp blue Exact money math for AI assistants over MCP.

Licence

Apache-2.0. See LICENSE and NOTICE. The OAuth 2.0 client-credentials grant and the declarative HTTP runtime are original implementations against the public RFCs and provider API documentation; no provider SDK is bundled.