Persistence & stores
The kit persists your integration's operational state: installations, secrets (encrypted), sync cursors, idempotency of inbound calls, webhook anti-replay, and the bounded logs (webhooks, dead-letter, audit). It is not a business database: the data volume is small and the logs are purged by retention. You write no SQL.
Since kit v2, this persistence goes through a port (IntegrationStore) with two official adapters whose behavior is strictly identical:
| Store | Who it's for | Driver to install |
|---|---|---|
| SQLite (default) | deployments with a persistent disk, zero configuration | better-sqlite3 |
| PostgreSQL | plug in your existing database: ephemeral containers, managed platforms, a single database to operate/back up | pg |
Driver = optional dependency
The kit imposes no driver on you: better-sqlite3 and pg are optional peer dependencies. Install the one for your backend. A PostgreSQL integration never compiles a native module.
SQLite: the zero-config default
A local file, nothing to operate. This is the kit's historical behavior, unchanged:
yarn add @shopimind/integration-kit-js better-sqlite3const app = await createIntegrationApp(myIntegration, {
databasePath: env.DATABASE_PATH ?? './data/store.sqlite',
// …
});Mount ./data on a persistent volume (the SQLite file is your integration's state). For fine-grained control (injected clock in tests, tooled access), the same store can be built explicitly:
import { createSqliteStore } from '@shopimind/integration-kit-js/store-sqlite';
const store = await createSqliteStore({ path: './data/store.sqlite' });
const app = await createIntegrationApp(myIntegration, { store, /* … */ });PostgreSQL: your database, a dedicated schema
Point the kit at the PostgreSQL database you already operate: no more local file, no persistent filesystem to provision, a single backup. The kit's tables live in a dedicated schema of your database, and nothing else is touched.
yarn add @shopimind/integration-kit-js pgimport { createIntegrationApp } from '@shopimind/integration-kit-js';
import { createPostgresStore } from '@shopimind/integration-kit-js/store-postgres';
const app = await createIntegrationApp(myIntegration, {
store: await createPostgresStore({
connectionString: env.DATABASE_URL!, // postgres://user:pass@host/db
schema: 'shopimind_my_integration', // one schema PER integration
}),
// …the other options are unchanged
});createPostgresStore options:
| Option | Default | Role |
|---|---|---|
connectionString | — | URL of your database. The store opens (and owns) its own small pool. |
pool | — | Alternative: reuse an existing pg.Pool from your application (the kit will not close it). Exactly one of the two is required. |
schema | shopimind_kit | PostgreSQL schema for the kit's tables (created if missing). Name it after your integration, e.g. shopimind_hiboutik. |
maxConnections | 10 | Pool size when the store owns it. |
connectionTimeoutMs | 5000 | Maximum wait for a free connection. Bounds every store call when the pool is saturated or the server unreachable. |
statementTimeoutMs | 30000 | Server-side statement_timeout: a query stuck on a lock is cancelled instead of holding a pool slot. |
pingTimeoutMs | 3000 | Upper bound on the ping() behind /health: a probe must answer, even when PostgreSQL stops responding. |
onPoolError | — | Called when the pool reports an error on an idle connection (managed-PostgreSQL failover, PgBouncer timeout). The store always attaches a listener (without one, pg would kill the process) and forwards the error so you can log it. |
There is nothing to prepare in the database. At startup the connector creates its schema and tables if they do not exist, then applies whatever migrations are missing. The operation is idempotent: restarting replays nothing. It runs inside a transaction and is guarded by a lock, so two instances starting at the same time cannot conflict. One simply waits for the other.
The only requirement is privileges: the PostgreSQL role behind connectionString must be able to create a schema in the database (CREATE on the database) on that very first startup. If your policy forbids it, create the empty schema by hand and grant the role rights on it; the kit takes care of the tables.
What is encrypted, whatever the backend
Secret encryption is application-level, above the store: sensitive values (setSecret, sensitive fields of the config_schema, the shop's API token) are encrypted with AES-256-GCM before they reach the backend. An adapter (including your shared PostgreSQL database) never sees a secret in cleartext.
The rest (shop domains, cursors, logs) is stored in cleartext: at-rest confidentiality of that data is a matter of your storage policy (privileges on the schema, disk or cluster encryption), exactly as with the SQLite file.
Before you deploy
Run one instance at a time
Two instances of the same connector running in parallel would sync the same installations twice. The kit does prevent two syncs from overlapping, but that lock lives in memory: it protects a process from itself, not two processes from each other.
In Kubernetes that means replicas: 1 and strategy: Recreate (or maxSurge: 0). The default setting starts the new pod before stopping the old one, and for those few seconds, two instances are running.
With SQLite the question never came up: a volume only mounts on one pod at a time. With PostgreSQL nothing prevents it technically any more, so it is now up to your deployment to guarantee it.
If your platform puts the service to sleep
The kit's sync scheduler is a timer inside the process: it only runs while the process is alive. On a platform that scales down to zero instances when traffic stops, automatic syncs will not fire.
In that case, turn it off and drive the syncs from outside:
const app = await createIntegrationApp(myIntegration, {
store,
autoSync: false, // no internal scheduler
// …
});An external cron then calls POST /admin/sync/{id} (see Operations console), or, if you write your own trigger, app.runSyncOnce(installationId).
If you are coming from kit v1 (SQLite)
Your file is reused as-is: same tables, same installations, same encrypted secrets, same cursors, same anti-replay. Move to kit v2 and restart, nothing else.
Exactly one thing happens on that first startup: a migration rewrites the timestamps v1 recorded (in SQL form, YYYY-MM-DD HH:MM:SS) into the ISO-8601 UTC form v2 uses. That is what keeps retention purges and counters exact. Expect a few extra seconds on a large store, once.
Back the file up before switching
This rewrite does not undo itself. Going back to kit v1 still works (it reads everything), but its /health probe can no longer compute the age of syncs written by v2. In short: the real way back is restoring your backup, not just redeploying the previous image.
If you are moving from SQLite to PostgreSQL
The kit does not transfer your data from one backend to the other: changing store means starting from empty storage. And that storage holds the API tokens of the installed shops; without it, your connector no longer knows which shop it works for.
- Before going live (no real installation yet): nothing to do, change the configuration and restart.
- In production: either have the merchants concerned reinstall the integration (ShopiMind then replays install and activation), or transfer the data yourself. That is workable because both backends use the same table layout, and secrets stay decryptable as long as you keep the same
credentialsKey.
Writing your own store (advanced)
The port is an official extension point: any backend can host the kit by implementing the IntegrationStore interface (exported from the package root). Its surface is deliberately simple:
- pure storage, no business logic: encryption, pagination bounds, and defensive serialization stay in the kit, above the port;
- one call = one atomic write: no transaction API to implement; only the
claim(...)methods carry a concurrency requirement ("insert if absent", atomic); - text timestamps (ISO-8601 UTC, generated by the kit), so no SQL date function to provide.
Validate your adapter with the conformance suite, the executable contract that both official adapters pass in CI:
// my-store.conformance.test.ts (vitest or jest)
import { describe, it, expect } from 'vitest';
import { runStoreConformanceSuite } from '@shopimind/integration-kit-js/store-testing';
import { createMyStore } from './my-store.js';
runStoreConformanceSuite(() => createMyStore(), { describe, it, expect });It notably checks the atomicity of the claims under concurrency, the "a secret's preview is always null" invariant, date-based purges, literal search (% and _ are not wildcards) and pagination stability.
Port compatibility policy
The port may gain methods in a minor kit version (the official adapters are updated at the same time). Re-run the conformance suite on your adapter at every version bump. Removals or signature changes only happen in a major version.
Going further
- The integration kit: all the
createIntegrationAppoptions. - Operations console: the admin API (including
POST /admin/sync/{id}). - The package
CHANGELOG: the v1 → v2 migration guide (the store API became asynchronous).