Client integration

A walkthrough for the team running an external ERP that needs to consumeconfigs from this service.

Issuing a token

  1. Sign in to your SGApps.IO desk and launch the ERP System Config app.
  2. Open the project you want the ERP to read (or create one — projects are folders under /apps/site-builder/user/{you}/).
  3. In the Tokens panel of the project overview, click + New token. Fill in: - Name — a label so you can revoke the right one later (e.g. cluj-erp-prod). - Allowed repos — comma-separated repoId list. Leave empty to grant access to all your projects (not recommended). - Expires at — optional date. Leave empty for non-expiring tokens. - Require fingerprint — turn on for production ERPs (see below).
  4. The token value appears once in a green box. Copy it now — the server does not store the plain value retrievably; if you lose it, you have to issue a new one.

Polling pattern

Use commit.short_id as an ETag-style cache key. Pseudocode:

let lastShortId = null;
let cachedTree = null;
let cachedFiles = {};
let resolvedRef = null;   // discovered on first poll, cached afterwards

async function refreshConfigIfChanged() {
    const branches = await fetch(
        '/site-builder/api/erp-config/projects/' + REPO_ID +
        '/repository/branches',
        { headers: { 'PRIVATE-TOKEN': TOKEN } }
    ).then(r => r.json());

    // Pick the branch to track. Don't hard-code `master` — modern repos
    // use `main`, and some projects use custom branch names. The server
    // marks one branch as `default: true` (priority: master → main →
    // alphabetical first) — that's the one a client without a hard
    // preference should follow.
    const branch =
        branches.find(b => b.default) ||
        branches.find(b => b.name === 'master') ||
        branches.find(b => b.name === 'main') ||
        branches[0];
    if (!branch) throw new Error('No branches in this project');
    resolvedRef = branch.name;

    if (branch.commit.short_id === lastShortId) {
        return cachedFiles; // no change
    }

    // Content changed — re-fetch tree + files.
    lastShortId = branch.commit.short_id;
    cachedTree = await fetch(
        '/site-builder/api/erp-config/projects/' + REPO_ID +
        '/repository/tree?ref=' + encodeURIComponent(resolvedRef) + '&recursive=1',
        { headers: { 'PRIVATE-TOKEN': TOKEN } }
    ).then(r => r.json());

    cachedFiles = {};
    for (const entry of cachedTree.filter(e => e.type === 'blob')) {
        cachedFiles[entry.path] = await fetch(
            '/site-builder/api/erp-config/projects/' + REPO_ID +
            '/repository/files/' + encodeURIComponent(entry.path) +
            '/raw?ref=' + encodeURIComponent(resolvedRef),
            { headers: { 'PRIVATE-TOKEN': TOKEN } }
        ).then(r => r.text());
    }
    return cachedFiles;
}

// Call from your existing refresh tick (every N seconds is fine — the
// branches endpoint is cheap; full re-fetch only fires when short_id changes).
Common pitfall — 404 on file read. If your client hard-codes ?ref=master and the project uses main, every file fetch returns 404 File Not Found (path includes the branch name, so a wrong ref resolves to a non-existent folder under the project root). Always discover the ref from the branches listing first, or rely on the default: true flag.

Recommended cadence

ERP role Refresh interval Why
User-facing app (pricing, taxonomy) 30s – 5min Quick reaction to operator changes
Background batch (BOM expansion) At job start + every 15min Cold reads, no need to react fast
Reporting / analytics At job start only Configs are reference data; one read per run is enough

Fingerprint binding

For production ERPs, turn on Require X-Instance-Id fingerprint matchwhen issuing the token. Then on the client:

  1. Compute a stable instanceId for the ERP host. Good sources: - Container ID + image digest (Docker). - Hardware-bound UUID (/etc/machine-id on Linux). - Cloud instance ID (AWS instance-id, GCP instance UUID).
  2. Send it on every request as X-Instance-Id: <id>.
  3. The first request binds the fingerprint server-side. Subsequent requests must match crypto.timingSafeEqual — a leaked token used from any other host returns 401 FINGERPRINT_MISMATCH.

If you migrate the ERP to a new host, you have two options:

Error recovery

Status Recovery
401 (token expired / fingerprint mismatch) Page the on-call. Don't retry — won't change. Issue a fresh token from the webapp.
403 (token doesn't cover repo) The webapp operator added a new repo but forgot to extend the token. PATCH /tokens/{id} with the new repos[].
404 (project missing) Webapp operator deleted the project. Don't auto-create — alert.
5xx Transient. Retry with exponential backoff. Keep serving the last-known-good cache to keep the ERP alive.

Rule of thumb. Treat the config-store as eventually-consistent reference data. Always have a last-known-good in-memory cache so a brief service outage on this side doesn't take your ERP down.

Token rotation

Periodic rotation (e.g. every 90 days):

  1. Issue a new token via the webapp. Same name + suffix -v2 is fine for tracking.
  2. Roll the new value into your ERP's secret store. Restart the ERP — it now uses the new token.
  3. Verify in the webapp Tokens list that the old token's last-use timestamp is stale, then revoke it.

If you need zero-downtime rotation across many ERPs:

  1. Issue the new token. Keep the old one active.
  2. Roll new value to all ERPs over a deployment window.
  3. After the rollout, revoke the old token. Any ERP still on the old value gets 401 and pages the operator (your detection signal).