@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
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.findormessage.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-Afterheader 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
CredentialStoreseam; bring Vault, your KMS, ormemoryCredentialStorein tests. - Tenancy and authorization —
tenant_idis 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.