mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-05 08:01:23 +00:00
Adds OAuth-backed integration connection management across the API, repository, event dispatch, migrations, docs, and dashboard UI. Includes realtime invalidation and small dashboard type compatibility fixes needed for the web typecheck gate.
150 lines
6.9 KiB
Markdown
150 lines
6.9 KiB
Markdown
# Integrations — OAuth provider setup
|
|
|
|
Warmbly's integrations connect via **real OAuth 2.0** wherever the provider
|
|
supports it (HubSpot, Slack, Google Sheets, Pipedrive, Salesforce). The connect
|
|
flow, CSRF/PKCE handling, token exchange, encrypted-at-rest token storage, and
|
|
automatic refresh are all built in. The only thing the platform operator must
|
|
supply is **one developer app per provider**, registered in that provider's
|
|
console, plus its client ID/secret as environment variables.
|
|
|
|
Until a provider's credentials are present, it renders in the dashboard catalog
|
|
as **"Coming soon"** (the Connect button is disabled) — no code change is needed
|
|
to light it up; just add the env vars and restart the backend.
|
|
|
|
## How it works (no code changes required to enable a provider)
|
|
|
|
1. User clicks **Connect with X** in the dashboard.
|
|
2. SPA calls `POST /integrations/oauth/start` → backend mints a CSRF `state`
|
|
(+ PKCE verifier where supported), stores it in `integration_oauth_states`,
|
|
and returns the provider authorization URL.
|
|
3. SPA opens that URL in a popup. The user authorizes in the provider's UI.
|
|
4. The provider redirects to `GET /integrations/oauth/callback` (a public
|
|
bouncer page) which `postMessage`s `{code, state}` back to the SPA opener.
|
|
5. SPA calls `POST /integrations/oauth/finish` → backend validates+consumes the
|
|
`state`, exchanges the code for tokens, resolves the connected account, and
|
|
stores the access/refresh tokens **sealed with the connecting user's
|
|
envelope-encryption DEK** (KMS → per-user DEK → AES-GCM, the same path used
|
|
for mailbox OAuth tokens). Plaintext tokens never touch a database column.
|
|
|
|
Access tokens are refreshed automatically (60s before expiry) using the stored
|
|
refresh token; if a refresh fails the connection flips to `reauth_required` and
|
|
the user is prompted to reconnect from the connection drawer.
|
|
|
|
## Required environment variables
|
|
|
|
Set these on the **backend** service. The redirect/callback URL must be allow-
|
|
listed in each provider's app config.
|
|
|
|
```
|
|
# Shared callback URL the providers redirect to. Defaults to
|
|
# $BACKEND_PUBLIC_URL/integrations/oauth/callback, else http://localhost:8080/...
|
|
INTEGRATIONS_OAUTH_REDIRECT_URL=https://api.yourdomain.com/integrations/oauth/callback
|
|
|
|
# HubSpot — https://developers.hubspot.com/ (create a public app)
|
|
HUBSPOT_OAUTH_CLIENT_ID=
|
|
HUBSPOT_OAUTH_CLIENT_SECRET=
|
|
|
|
# Slack — https://api.slack.com/apps (OAuth & Permissions → bot scopes:
|
|
# chat:write, channels:read, groups:read)
|
|
SLACK_OAUTH_CLIENT_ID=
|
|
SLACK_OAUTH_CLIENT_SECRET=
|
|
|
|
# Google Sheets — https://console.cloud.google.com/ (OAuth client, enable the
|
|
# Google Sheets API; scopes: spreadsheets, userinfo.email)
|
|
GOOGLE_SHEETS_OAUTH_CLIENT_ID=
|
|
GOOGLE_SHEETS_OAUTH_CLIENT_SECRET=
|
|
|
|
# Pipedrive — https://developers.pipedrive.com/ (Marketplace Manager app;
|
|
# scopes: contacts:full, deals:full)
|
|
PIPEDRIVE_OAUTH_CLIENT_ID=
|
|
PIPEDRIVE_OAUTH_CLIENT_SECRET=
|
|
|
|
# Salesforce — https://developer.salesforce.com/ (Connected App;
|
|
# scopes: api, refresh_token)
|
|
SALESFORCE_OAUTH_CLIENT_ID=
|
|
SALESFORCE_OAUTH_CLIENT_SECRET=
|
|
```
|
|
|
|
The env var prefix is `<PROVIDER>_OAUTH_CLIENT_ID` / `_CLIENT_SECRET` — wiring a
|
|
new OAuth provider is just registering it in `internal/app/integration/oauth.go`
|
|
(`NewOAuthManager`) and adding the matching env vars.
|
|
|
|
## Redirect URL to register with each provider
|
|
|
|
```
|
|
<INTEGRATIONS_OAUTH_REDIRECT_URL>
|
|
# e.g. https://api.yourdomain.com/integrations/oauth/callback
|
|
```
|
|
|
|
For local development the default is `http://localhost:8080/integrations/oauth/callback`.
|
|
|
|
## Providers that do NOT use OAuth
|
|
|
|
- **Close** — no public OAuth app; the user pastes a Close API key.
|
|
- **Zapier / Make / n8n** — authenticate *into Warmbly* using a scoped Warmbly
|
|
API key the user generates under Settings → API keys.
|
|
- **Discord** — the user pastes a channel webhook URL (outbound only).
|
|
- **Calendly / Cal.com** — inbound only; Warmbly mints a signed inbound URL the
|
|
user pastes into the provider's webhook config.
|
|
|
|
All pasted secrets are sealed with the same envelope encryption as OAuth tokens
|
|
before they are stored — nothing sensitive is ever persisted in plaintext.
|
|
|
|
## How a user actually uses an integration
|
|
|
|
Connecting is step one; the value is the **automations** a user builds on a
|
|
connection. After connecting, the user opens the connection's management drawer
|
|
(click any connected card on the Integrations page) and adds rules:
|
|
|
|
> **When** a prospect replies — **only** positive replies, **≥60%** confidence —
|
|
> **notify** `#sales` with “🔥 {{contact_email}} is interested — {{subject}}”.
|
|
|
|
Each rule is fully customizable in the UI (no code, no API keys to paste):
|
|
|
|
- **Trigger** — which Warmbly event fires the rule (reply, bounce, unsubscribe,
|
|
warmup-health, complaint, meeting booked).
|
|
- **Filters** (reply triggers) — restrict to specific reply intents
|
|
(`positive`, `question`, `negative`, …) and a minimum classifier confidence.
|
|
- **Destination** — Slack channel, Google Sheet ID, or an outbound URL,
|
|
depending on the provider.
|
|
- **Message template** — a custom string with `{{placeholder}}` substitution
|
|
over the event payload (`{{contact_email}}`, `{{subject}}`, `{{intent}}`,
|
|
`{{campaign_id}}`, `{{reason}}`, …).
|
|
|
|
Rules are stored in `integration_event_subscriptions.config` (JSONB) and applied
|
|
at dispatch time by `internal/app/integration/dispatch.go`
|
|
(`subscriptionMatchesFilter`, `renderTemplate`).
|
|
|
|
## Event-driven actions — the wiring
|
|
|
|
Examples of trigger → action pairs:
|
|
|
|
- `campaign.reply_received` → `slack.notify` (ping #sales)
|
|
- `campaign.reply_received` → `hubspot.upsert_contact` (create/update + log note)
|
|
- `campaign.email_bounced` → `discord.notify`
|
|
- meeting booked (Calendly/Cal.com) → Slack / CRM
|
|
|
|
Events reach integration actions through the webhook dispatch sink
|
|
(`webhook.Service.WireDispatchSink`), so any event already delivered to customer
|
|
webhooks also drives integration actions. The sink is wired in **both** binaries:
|
|
|
|
- **`cmd/backend`** — deliverability ingest (bounce/complaint/unsubscribe),
|
|
meeting-booked, email-account lifecycle, warmup health.
|
|
- **`cmd/consumer`** — inbound campaign replies. The consumer is where replies
|
|
are detected (`advanced.ProcessIncomingReply`), which now emits
|
|
`campaign.reply_received` with `intent`/`confidence` in the payload. This is
|
|
what makes "notify me when a prospect replies → Slack/CRM" fire for real.
|
|
|
|
`advanced.Service` exposes `WireDispatcher(EventDispatcher)` (see
|
|
`internal/app/advanced/events.go`); `ProcessIncomingReply` and
|
|
`IngestDeliverabilityEvent` call `emit(...)` to fan their events out.
|
|
|
|
Each executed action is recorded as an `integration_sync_runs` row for
|
|
observability and surfaced in the connection drawer's "Recent activity".
|
|
|
|
## Access / gating
|
|
|
|
Integrations are a **paid-plan** feature (enforced in the integration handlers
|
|
via `FeatureGateService.IsPaidOrganization`). Browsing the catalog is open so
|
|
non-paid orgs see what's available; connecting requires an active paid plan.
|