Skip to main content

Integrations

How Helm connects to third parties — Google Business Profile, supplier dealer portals, accounting bridges, marketing tools, anything else the shop owner needs Helm to talk to.

Live 2026-05-18 (migrations 036 + 037)

GBP is wired with manual ID configuration today (OAuth coming). The owner-defined custom_integrations system ships five auth-type templates so the shop can connect to anything from a single API-key vendor to a username+password dealer portal — no schema change per vendor.

Why two tables (and not just one)

GBP is a known, high-value target. The product treats it specially:

  • It appears on the Today dashboard (reviews card).
  • The Customer Compact promises Helm helps the shop with review replies.
  • The integration has its own settings UI surface with a "Connected to business name" status.

So integration_gbp is a single-row, named table with first-class columns for the things GBP-specifically requires. Adding QBO or QBP in the future would follow the same pattern (one named table each).

Everything else — the shop's custom marketing tool, a second supplier portal, a one-off accounting bridge — goes into custom_integrations. Owner-defined, owner-named, no migration required to add a new one.

integration_gbp (migration 036)

Single-row (CHECK (id = 1)). Captures the GBP-specific shape:

ColumnNotes
account_resource_nameaccounts/NNN form, from GBP
location_resource_namelocations/NNN form, from GBP
business_name_cachedSnapshot at connect time so the UI shows the connected business name without re-fetching
verified_stateVERIFIED / UNVERIFIED / SUSPENDED / UNKNOWN
scopes_granted_jsonJSON array of OAuth scopes (when OAuth lands)
refresh_token_secret_keyReference into Cloudflare secret store (prod)
refresh_token_stubDev-only stub; never used in prod
is_active0/1
connected_at, last_sync_at, last_error_at, last_error_messageBookkeeping

Today's MVP: the owner pastes the resource names from GBP's web UI; Helm stores them; the OAuth refresh-token columns stay NULL with an "OAuth coming soon" banner. When OAuth ships: the production path stores the encrypted refresh token in Cloudflare's secret store and references it via refresh_token_secret_key. The Worker resolves the secret at call time and never echoes it back to the UI.

Endpoints (owner/sys_admin gated):

EndpointPurpose
GET /api/integrations/gbpCurrent state (all columns minus secret material)
PUT /api/integrations/gbpUpdate fields (manual config for now)
DELETE /api/integrations/gbpDisconnect (clears fields, sets is_active=0)

All three are audit-chained — before + after JSON written to audit_mutations.

custom_integrations (migration 037)

Owner-defined third-party integrations. Each row has an auth_type that drives which credential fields the UI renders. Five supported types:

  • api_key: single API key (a simple REST vendor)
  • api_key_secret: API key + signing secret (webhooks with HMAC signing)
  • oauth2_manual: client ID + client secret + refresh token (placeholder until real OAuth ships)
  • basic_auth: username + password + optional account ID + base URL (internal-network APIs)
  • dealer_portal: portal URL + username + password + dealer/account ID (scrape-only suppliers — Norco, Specialized, etc.)

The category column is one of: supplier, accounting, marketing, other. The Integrations modal in Settings groups by category.

Endpoints (owner/sys_admin gated):

EndpointPurpose
GET /api/integrations/customList all (secrets redacted)
POST /api/integrations/customCreate (slug + display_name + auth_type + credentials)
PUT /api/integrations/custom/:idUpdate fields
DELETE /api/integrations/custom/:idDelete

The secret-store pattern

Both tables follow the same pattern for handling credentials:

  • credentials_stub (custom) / refresh_token_stub (gbp) — JSON payload, dev-only. Never used in production.
  • secret_store_key (custom) / refresh_token_secret_key (gbp) — string reference into Cloudflare's secret store. The Worker fetches the secret at call time; the value never appears in a Worker log line and never round-trips to the UI.

The UI never echoes saved credentials back. Editing a credential field is a write-only operation — the form shows an empty input over a "currently set" indicator.

What this enables

For the Today dashboard:

  • The Reviews card pulls from GBP (will pull, once OAuth lands)
  • The Social card pulls from a future custom integration (Meta / Instagram once configured)

For Sales / Service:

  • Order-queue PO generation will call out to per-supplier dealer-portal integrations (when those ship)

For Marketing (slice 10):

  • Email + SMS providers (Resend, Twilio) configured as custom integrations rather than baked into the worker source

See also