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.
| Connector | What to paste | Where to get it |
|---|---|---|
| HubSpot | Private-app token (pat-…) | HubSpot → Settings → Integrations → Private Apps. Scopes: crm.objects.{contacts,deals,companies}.read (+.write for gated updates) |
| Slack | Bot token (xoxb-…) | Slack app → OAuth & Permissions |
| Gmail / Drive / GA4 | Use 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 stableVOCION_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 (--fullre-fetches).