docs/guides/connecting-sources.md

Connecting sources (credentials)

How a connector gets the keys it needs: paste a token in the Sources UI, run the CLI, or do a one-time Google OAuth consent. Everything lands AES-GCM-encrypted in the credential vault — plaintext never touches the database or logs.

The model

Credentials are per-connector, per-project. One HubSpot private-app token serves every HubSpot source in the project (deals, contacts, companies alike). Each source tile on /dashboard/sources shows its connection state — Connected or Needs credentials — plus the object type it pulls and how many documents it has ingested.

Option 1 — the Sources UI

On /dashboard/sources, every source that needs auth has a Connect button (admins only). Paste the token, save — it's encrypted into the vault and the source is ready to sync. Use Update key to rotate.

ConnectorWhat to pasteWhere to get it
HubSpotPrivate-app token (pat-…)HubSpot → Settings → Integrations → Private Apps. Scopes: crm.objects.{contacts,deals,companies}.read (+.write for gated updates)
SlackBot token (xoxb-…)Slack app → OAuth & Permissions
Gmail / Drive / GA4Use the OAuth flow below (durable) or a short-lived access token (stopgap)

Option 2 — the CLI

For headless installs and first-boot seeding:

npm run creds:set -- --project <id|slug> --source hubspot --token pat-na1-… 
# extra connector fields ride --field:
npm run creds:set -- --project acme --source googleAds --token ya29… --field developerToken=abc

Re-running rotates: the newest non-revoked credential wins at sync time.

Option 3 — Google OAuth (durable, for Gmail / Drive / GA4)

Google access tokens expire in about an hour, so a pasted token dies after one sync. The durable path stores a refresh token; connectors mint a fresh access token per run automatically:

npm run google:oauth -- --project <id|slug> \
  --client-id <your-oauth-client-id> --client-secret <secret> \
  --sources gmail,drive

It prints a consent URL, listens on a localhost callback, exchanges the code, and stores { refreshToken, clientId, clientSecret } in the vault for each connector. Scopes requested: gmail.readonly + gmail.send (drafts/sends still gate through the review queue), drive.readonly, analytics.readonly for ga4.

Vault + security notes

  • Storage is envelope-encrypted: per-tenant DEK, AES-256-GCM per credential. Production should set VOCION_CREDENTIAL_VAULT=kms (+ VOCION_KMS_KEY_ARN) or at minimum a stable VOCION_CREDENTIAL_VAULT_KEY — an ephemeral dev key means credentials are unreadable after a restart.
  • Setting credentials is admin-only; reading them back out is not exposed anywhere — they decrypt only inside sync/action execution.
  • Backfills: npm run sync:source -- --project <id|slug> --source <slug> runs a sync with progress ticks (--full re-fetches).

Next

  • Sources — the connector catalog
  • Actions — gated writes that reuse the same credentials