Operations console
The kit ships a small, self-contained operations console for you, the integrator. Set an adminToken, open http://<host>:<port>/admin/ui, and you get a live view of every installation plus a handful of safe actions.
It runs entirely on your side: the console only reads your integration's local SQLite store and triggers the sync engine you already run. No ShopiMind account, service, or database is involved — nothing here talks to ShopiMind except the sync calls your integration already makes.
Nothing to build
The console is part of the kit. There is no extra package, no frontend to deploy — it is a single self-contained page served by the kit's HTTP server.
Enable it
The console and the whole /admin/* surface are off until you set an adminToken. Without it, every admin route answers 401.
const app = createIntegrationApp(myIntegration, {
databasePath: env.DATABASE_PATH ?? './data/store.sqlite',
webhookSecret: required('WEBHOOK_SECRET'),
credentialsKey: env.CREDENTIALS_KEY,
adminToken: env.ADMIN_TOKEN, // enables /admin/* and the console (32+ high-entropy chars)
adminPort: 9090, // recommended: serve /admin on a PRIVATE listener…
adminHost: '127.0.0.1', // …bound to loopback (the default when adminPort is set)
adminSecureCookie: true, // mark the session cookie Secure (serve the console over HTTPS)
});Then browse to http://127.0.0.1:9090/admin/ui, paste your adminToken, and you are in.
What you can see
| Area | What it shows |
|---|---|
| Dashboard | installation counts per status, webhooks received in the last 24 h (and how many were refused), dead-letter totals by entity, the running kit version. |
| Definition | what this integration declares — metadata, config schema, sync steps, inbound routes, hooks and capabilities. |
| Installations | a searchable, paginated list; open one to see its shop, the linked internal account, and per-installation detail. |
| Cursors & runs | the sync cursor per entity/source (last status, last sync, item count, consecutive failures) and the recent sync runs with their summary. |
| Webhooks & inbound | the lifecycle webhook log and the inbound-call log, paginated and filterable. |
| State | the per-installation key/value store as metadata only — key, type, length, last update. A secret's value is never shown. |
| Dead-letter | items the ShopiMind API refused during a bulk push (entity, reason, payload), globally or per installation. |
| Audit | every admin action (login, sync, reprovision, reveal, purge) as metadata. |
What you can do
From an installation, or the dead-letter view:
- Sync — trigger an incremental sync, or a full backfill.
- Reprovision — re-run your integration's
provisioning()(idempotent find-or-create); handy after you change a data-source or custom-data plan. - Purge a dead-lettered item (scoped to its installation).
- Reveal the raw payload of a single webhook or rejected item — a deliberate, audited action (see below).
Security model
The console is safe by default. The guarantees below are enforced by the kit, not by the UI.
- Two ways in, both token-gated. A browser exchanges the admin token once (
POST /admin/session) for an HttpOnly,SameSite=Strictsession cookie plus a CSRF token; the raw token is never stored in the browser. Automated clients (curl, scripts) can instead send the token directly asx-admin-token(orAuthorization: Bearer). - CSRF double-submit. A state-changing call made through a browser session must echo the CSRF token in the
x-csrf-tokenheader. Token-authenticated clients are not cookie-based, so they are not exposed to CSRF and do not need it. - PII masked by default. Emails, phone numbers, names and addresses in webhook and dead-letter payloads are masked before they are shown.
- Secrets are never returned. State is read as metadata only; an encrypted value (an API key stored via
setSecret) is never materialized — this is enforced at the SQL layer, not just in the UI. - Reveal is separate and audited. Seeing an un-masked payload is an explicit action recorded in the audit trail.
- Bounded & throttled. The admin token is compared in constant time, admin routes are per-IP rate-limited, and sessions expire (sliding 12 h) and are capped in number.
Keep the admin surface private
The admin token protects everything, but treat /admin/* as an internal surface. In production, put it on its own listener with adminPort (bound to loopback or a private interface by default), serve it over HTTPS with adminSecureCookie: true, and use a long, high-entropy adminToken (32+ characters). The kit emits a startup warning if the admin surface is exposed on a public 0.0.0.0 listener, if the token is short, or if the cookie is not marked Secure.
Isolate the admin surface (recommended)
By default /admin/* shares the public server (fine for local development). Set adminPort to move the entire admin surface — the console and its API — onto a separate listener, so the public port only exposes webhooks, inbound calls and /health:
adminPort: 9090, // admin surface on its own port…
adminHost: '127.0.0.1', // …bound to loopback by default; expose it via your ingress if needed
adminSecureCookie: true, // and serve it over HTTPSRetention
The admin data has its own retention, independent from the log tables:
| Option | Default | Purges |
|---|---|---|
rejectedRetentionDays | = retentionDays (90) | the dead-letter (rejected_item). Raise it to keep refused items longer for forensics. |
auditRetentionDays | 365 | the audit trail. ≤ 0 disables the audit purge. |
API access without the console
Every read and action is a plain HTTP endpoint — use it from a script or a health dashboard with the x-admin-token header (no cookie, no CSRF):
curl -s http://127.0.0.1:9090/admin/meta -H "x-admin-token: $ADMIN_TOKEN"
curl -s -X POST "http://127.0.0.1:9090/admin/sync/$ID?full=true" -H "x-admin-token: $ADMIN_TOKEN"Endpoint reference
Authentication for every route below is the admin token or a valid session; state-changing routes additionally require the CSRF token when called from a session.
| Method | Path | Role |
|---|---|---|
GET | /admin/ui | The operations console (HTML). |
POST | /admin/session | Exchange the admin token for a session cookie + CSRF token. |
DELETE | /admin/session | End the current session. |
GET | /admin/meta | Dashboard synthesis (includes the integration identity). |
GET | /admin/definition | What the integration declares (metadata, config schema, sync steps, capabilities…). |
GET | /admin/installations | Installations list (?status=, ?q=, pagination). |
GET | /admin/installations/{id} | One installation, aggregated. |
GET | /admin/installations/{id}/cursors | Its sync cursors. |
GET | /admin/installations/{id}/runs | Its sync runs (paginated). |
GET | /admin/installations/{id}/webhooks | Its webhook log (PII masked, paginated). |
GET | /admin/installations/{id}/inbound | Its inbound events (paginated). |
GET | /admin/installations/{id}/state | Its state as metadata (secrets never returned). |
GET | /admin/rejected | Global dead-letter (?installationId=, ?entity=, PII masked, paginated). |
GET | /admin/audit | Audit trail (paginated). |
POST | /admin/sync/{id}?full=true | Trigger a sync (audited). |
POST | /admin/installations/{id}/reprovision | Re-run provisioning (audited). |
POST | /admin/rejected/purge | Delete dead-lettered items — body { installationId, ids }, installation-scoped (audited). |
POST | /admin/rejected/{id}/reveal | Reveal one rejected item's raw payload (audited). |
POST | /admin/installations/{id}/webhook-log/{logId}/reveal | Reveal one webhook's raw payload (audited). |
Going further
- The integration kit —
createIntegrationAppoptions and the routes the kit exposes. - Sync your data — what feeds the cursors, runs and dead-letter you see here.