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.
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:
| Column | Notes |
|---|---|
account_resource_name | accounts/NNN form, from GBP |
location_resource_name | locations/NNN form, from GBP |
business_name_cached | Snapshot at connect time so the UI shows the connected business name without re-fetching |
verified_state | VERIFIED / UNVERIFIED / SUSPENDED / UNKNOWN |
scopes_granted_json | JSON array of OAuth scopes (when OAuth lands) |
refresh_token_secret_key | Reference into Cloudflare secret store (prod) |
refresh_token_stub | Dev-only stub; never used in prod |
is_active | 0/1 |
connected_at, last_sync_at, last_error_at, last_error_message | Bookkeeping |
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):
| Endpoint | Purpose |
|---|---|
GET /api/integrations/gbp | Current state (all columns minus secret material) |
PUT /api/integrations/gbp | Update fields (manual config for now) |
DELETE /api/integrations/gbp | Disconnect (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):
| Endpoint | Purpose |
|---|---|
GET /api/integrations/custom | List all (secrets redacted) |
POST /api/integrations/custom | Create (slug + display_name + auth_type + credentials) |
PUT /api/integrations/custom/:id | Update fields |
DELETE /api/integrations/custom/:id | Delete |
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
- Today dashboard — Reviews and Social cards depend on integrations
- Slice 10 — Marketing — outbound channels via custom integrations
- Security model — secret store handling matches the per-shop secrets pattern
- C4 — Context — external systems Helm talks to