The integration kit
The @shopimind/integration-kit-js kit is the runtime of a ShopiMind integration — the JavaScript integration kit (PHP and Go mirror kits are planned later). You describe your integration with a single object — defineIntegration({...}) — and the kit provides everything else:
- a webhook server (Hapi) that receives lifecycle events;
- HMAC signature verification of every incoming request (you write no security code);
- an encrypted local store — a SQLite database, inside your own service (not the ShopiMind database) — that remembers installations, secrets (API keys…), and sync cursors; you write no SQL;
- an incremental-sync scheduler;
- a ready-to-use, already-authenticated ShopiMind SDK client, exposed under
ctx.spm.
The kit depends on and re-exports the SDK @shopimind/sdk-js (export * from '@shopimind/sdk-js'), so you import SDK resources and types from the kit.
The kit is a dependency, not a skeleton to clone
You install the kit (@shopimind/integration-kit-js) and write your integration on top of it. No copy-pasting of a starter: you benefit from kit updates just like any other npm dependency.
Installation
npm install @shopimind/integration-kit-js @shopimind/sdk-js dotenv- ESM only, Node.js ≥ 18.17. Kit dependencies:
@hapi/hapiandbetter-sqlite3. @shopimind/sdk-jsis the SDK the kit re-exports to call the ShopiMind API. The kit builds the authenticated client for you.
The defineIntegration object
Your entire integration fits in a single defineIntegration call. The kit validates it at startup (well-formed slug, unique sync entities, sources provided for per-source steps).
import { defineIntegration } from '@shopimind/integration-kit-js';
import type { Integration, IntegrationContext, ProvisioningPlan, RemoteOption } from '@shopimind/integration-kit-js';
import type { MySettings } from './settings.js';
export const myIntegration: Integration<MySettings> = defineIntegration({
slug: 'my-app', // ^[a-z0-9_-]+$
meta: {
name: 'My App',
version: '1.0.0',
categories: ['crm'], // neutral slugs, resolved at registration
},
configSchema: { steps: [/* … */] },
parseSettings: (raw) => ({ /* typed settings */ }),
testConnection: async (ctx) => true,
remoteData: { /* dynamic options for a select */ },
provisioning: (ctx) => ({ dataSources: [/* … */] }),
widgets: [/* … */],
syncSteps: [/* … */],
inbound: { /* incoming routes called by your app */ },
hooks: { onActivate: async (ctx) => {} },
});The Integration<S> contract
| Field | Type | Role |
|---|---|---|
slug | string | Unique identifier of the integration (^[a-z0-9_-]+$). |
meta | IntegrationMeta | Metadata (see below). |
configSchema | ConfigSchema | The configuration form presented to the merchant. See Configuration. |
parseSettings | (raw) => S | Transforms the raw config values into your typed settings S. |
testConnection | (ctx) => Promise<boolean> | Validates the integration credentials. Called on activation and from the UI. |
remoteData? | Record<string, (ctx) => Promise<RemoteOption[]>> | Feeds the form's dynamic selects. See Configuration. |
provisioning? | (ctx) => ProvisioningPlan | Data sources, custom data, and events to create. See Sync your data. |
widgets? | WidgetDeclaration[] | Your widgets. |
syncSteps | SyncStep<S>[] | The synchronization steps. |
inbound? | Record<string, (ctx, payload) => Promise<void> | void> | The incoming routes that your application calls to trigger an event or push a piece of data in real time. |
hooks? | LifecycleHooks<S> | Reactions to the lifecycle (see Webhooks). |
No migrations field
The Integration contract has no migrations field. The kit's internal store manages its own migrations.
meta — IntegrationMeta
| Field | Type | |
|---|---|---|
name | string | Display name. |
version | string | Integration version (e.g. 1.0.0). |
categories? | string[] | Category slugs (e.g. ['crm'], ['pos']), resolved at registration. |
icon_url? | string | Icon. |
short_description? / description? | string | Descriptions. |
documentation_url? | string | Link to your docs. |
The ctx context (IntegrationContext)
The kit injects a context into each of your callbacks (testConnection, remoteData, provisioning, syncSteps[].run, inbound, hooks):
| Field | Type | Role |
|---|---|---|
installationId | string | Opaque installation token, issued by ShopiMind. Never interpret it. |
settings | S | Your typed settings (the result of parseSettings). |
spm | SpmHttpClient | The already-authenticated raw SDK client. Call SDK static methods on it, e.g. SpmCustomers.bulkSave(ctx.spm, items, { chunk: true }) or SpmEvents.trigger(ctx.spm, codeName, payload). See Sync your data. |
sendBulk | (fn, items) => Promise<{ sent; rejected; rejected_items }> | Safe bulk push: sends chunked, throws on a transport failure, and surfaces per-item rejections (the engine holds the cursor on rejects — no silent loss). The recommended way to push. See Sync your data. |
withSource | (sourceKey) => SourceHandle | Returns a SourceHandle { id, tag(items), send(fn, items) }: tag adds id_data_source to each item, send tags then safely pushes. For catalog/POS integrations. See Sync your data. |
customData | (name) => CustomDataHandle | Returns a CustomDataHandle { id, save(records) } for the custom-data definition name declared in provisioning.customData. save safely upserts records (chunked, transport-safe, surfaces rejections). See Sync your data. |
state | IntegrationStateRepo | Per-installation key-value storage (encryptable). |
inboundSecret | string | The per-installation HMAC secret of the incoming routes, to pass to your application. |
logger | Logger | Logging (automatically redacts secrets). |
setExternalAccount | ({ id, name? }) => void | Links the ShopiMind installation to your internal account (correlation bridge). |
The SDK never throws on HTTP errors
SDK calls return an envelope { ok, statusCode, data, error } — they never throw on an HTTP error. Use the re-exported helpers SpmHelpers.unwrapOrThrow(env) and SpmHelpers.extractCounts(env) when working with the raw ctx.spm client. For pushing data, prefer ctx.sendBulk / withSource / customData, which already handle transport failures and per-item rejections for you.
Mounting the runtime — main.ts
createIntegrationApp(integration, options) assembles the server, the store, the scheduler, and the SDK client. The kit builds the authenticated SDK client itself from the shop's API key received at installation — you never instantiate the SDK in an integration. Here is the reference shape:
import 'dotenv/config';
import { createIntegrationApp } from '@shopimind/integration-kit-js';
import { myIntegration } from './integration.js';
const env = process.env;
const required = (key: string): string => {
const v = env[key];
if (!v) throw new Error(`Required environment variable: ${key}`);
return v;
};
const app = createIntegrationApp(myIntegration, {
databasePath: env.DATABASE_PATH ?? './data/store.sqlite',
webhookSecret: required('WEBHOOK_SECRET'),
credentialsKey: env.CREDENTIALS_KEY ?? null,
// Secrets are fail-closed: without CREDENTIALS_KEY the app refuses to start.
// Allow cleartext ONLY in local dev.
allowPlaintextSecrets: env.NODE_ENV !== 'production',
adminToken: env.ADMIN_TOKEN ?? null,
backfillDays: env.BACKFILL_DAYS ? Number(env.BACKFILL_DAYS) : 365,
syncIntervalMinutes: env.SYNC_INTERVAL_MINUTES ? Number(env.SYNC_INTERVAL_MINUTES) : 15,
port: env.PORT ? Number(env.PORT) : 8080,
});
void app.start().catch((e: unknown) => {
console.error('Integration startup failed', e);
process.exit(1);
});
process.on('SIGTERM', () => void app.stop().then(() => process.exit(0)));
process.on('SIGINT', () => void app.stop().then(() => process.exit(0)));The kit owns the SDK client
There is no SDK import and no gateway factory to wire up. The kit builds the SpmHttpClient (ctx.spm) for each installation using the shop's API key. A makeSpmClient option exists for tests only — production code never sets it.
createIntegrationApp options
| Option | Default | Role |
|---|---|---|
databasePath | — (required) | SQLite store file. |
webhookSecret | — (required) | HMAC secret, provided by ShopiMind at registration. |
credentialsKey | null | AES-256 key (64 hex) — encryption of secrets at rest. Mandatory (fail-closed): without it the app refuses to start, unless allowPlaintextSecrets is set. |
allowPlaintextSecrets | false | Dev-only opt-in to store secrets in cleartext when no credentialsKey is given (emits a loud warning). Never use outside local development. |
adminToken | null | Protects the /admin/* routes and unlocks the operations console. |
adminPort / adminHost | — / 127.0.0.1 | Serve the admin surface on a separate listener (loopback by default) instead of the public server. See Operations console. |
adminSecureCookie | false | Mark the admin session cookie Secure (serve the console over HTTPS). |
port / host | 8080 / 0.0.0.0 | HTTP server. |
backfillDays | 365 | Depth of the initial sync. |
autoSync / syncIntervalMinutes | true / 15 | Incremental sync scheduler (≤ 0 disables it). |
autoBackfillOnActivate | true | Runs a full sync right after activation. |
signatureToleranceSeconds | 300 | Anti-replay window for the signature. |
retentionDays | 90 | Purge window for the log and anti-replay tables. |
rejectedRetentionDays | = retentionDays | Retention for the dead-letter (rejected_item). |
auditRetentionDays | 365 | Retention for the admin audit trail. |
spmBaseUrl | — | Overrides the ShopiMind API base URL (default https://core.shopimind.com). |
createIntegrationApp returns an IntegrationApp with start(), stop(), and runSyncOnce(installationId, { full? }).
Environment variables
The kit does not read process.env itself: it is your main.ts that maps the environment to the options. Convention:
WEBHOOK_SECRET= # HMAC, provided by ShopiMind at registration (mandatory)
CREDENTIALS_KEY= # AES-256 (64 hex) — encryption at rest; MANDATORY in production
ADMIN_TOKEN= # protects the /admin/* routes and unlocks the operations console
ADMIN_PORT= # optional: serve /admin on its own listener (recommended in production)
PORT=8080
SYNC_INTERVAL_MINUTES=15
BACKFILL_DAYS=365
SHOPIMIND_API_URL= # overrides the API base (maps to spmBaseUrl; default https://core.shopimind.com)
DATABASE_PATH=./data/store.sqliteRoutes exposed by the kit
| Method | Path | Role | Auth |
|---|---|---|---|
POST | /webhook/receive | All lifecycle events | ShopiMind HMAC signature (kit) |
POST | /webhook/test-connection | Connection test from the ShopiMind UI | ShopiMind HMAC signature (kit) |
POST | /webhook/remote-data/{resource} | Dynamic options for a select | ShopiMind HMAC signature (kit) |
POST | /inbound/{action} | Incoming routes called by your app | Per-installation HMAC signature (kit) |
GET | /health | Availability probe | — |
GET | /admin/ui | The operations console | admin token / session |
* | /admin/* | Operations API (installations, sync, dead-letter, audit…) | admin token / session |
The full admin surface — reads, actions, session and CSRF — is documented on the Operations console page.
Operations console
Setting adminToken also unlocks a self-contained operations console at /admin/ui: a live, integrator-side view of your installations, sync runs, webhooks, dead-letter and audit trail, plus safe actions (sync, reprovision, purge, reveal). It reads only your local store, masks PII by default, and never returns secrets. In production, put it on its own listener with adminPort and serve it over HTTPS.
What the kit handles for you
- Inbound security: HMAC verification on the raw body, in constant time, with a timestamp anti-replay window — both for ShopiMind webhooks (
X-Shopimind-Signature) and for incoming calls from your app (x-integration-*, per-installation secret). Replay protection is mandatory and server-enforced (no caller opt-in): a replayed inbound call or lifecycle webhook is short-circuited, and lifecycle transitions are idempotent. - Encrypted store: installations, integration state (encrypted API key — AES-256-GCM, fail-closed), sync cursors, run history, webhook log (payloads redacted from the config schema — no secrets in clear text). Log and anti-replay tables are purged after
retentionDays. - Lifecycle: install → activate → config_updated → deactivate → uninstall, with status and API key management.
- Synchronization: scheduler, safe cursor (no data loss), streaming pagination, and bounded concurrency. See Sync your data.
Going further
- Configuration — declare your
config_schema. - Sync your data —
SyncStep,ctx.sendBulk,ctx.withSource,ctx.customData, provisioning. - Lifecycle & webhooks — events, signature, hooks.
- Inbound middleware — expose routes called by your app.
- Operations console — observe and operate your integration from the embedded admin UI.
- Complete example (Hiboutik) — an end-to-end integration.