Problem
Current crates/screenpipe-connect/src/connections/hubspot.rs uses a manual Private App token field (api_token, secret, pasted by the user). Friction:
- User has to navigate to HubSpot Settings → Integrations → Private Apps → create app → enable scopes (contacts/companies/deals read+write) → copy token → paste into screenpipe. ~90 seconds even for experienced HubSpot admins.
- Token rotation = user has to redo the whole flow.
- We lose half the prospects who'd otherwise connect — the friction is the killer.
- Internal agents (Claude Code sessions, scheduled pipes, etc.) hit the same wall — can't write to HubSpot without a human pasting a token first.
Surfaced today while trying to auto-update HubSpot from a Claude Code session — wanted to create a contact + company + update lifecycle stages, hit the no-token wall, ended up giving the user a paste-ready table instead of just doing it. Same friction every customer hits.
What's already there
crates/screenpipe-connect/src/connections/hubspot.rs — Bearer-token impl, works, just manual
crate::oauth module — already used by other integrations (notion, slack, calcom, gmail, etc.)
crates/screenpipe-connect/src/connections/notion.rs is the cleanest reference impl (~30 LoC for the OAuth wiring)
Proposed change
Mirror the notion.rs pattern. Concretely:
- Register a HubSpot OAuth app in our screenpipe HubSpot dev account → get
client_id (and store client_secret in the proxy server's env).
- Add
OAuthConfig to hubspot.rs:
static OAUTH: OAuthConfig = OAuthConfig {
auth_url: "https://app.hubspot.com/oauth/authorize",
client_id: "<our hubspot oauth app client id>",
extra_auth_params: &[],
redirect_uri_override: None,
};
- Implement
oauth_config() returning Some(&OAUTH), drop the manual api_token field from IntegrationDef.
- Update
proxy_config() so ProxyAuth::Bearer { credential_key } reads the OAuth access token (same shape as notion).
- Update
test() to call oauth::read_oauth_token_instance(secret_store, "hubspot", None) (mirror of notion).
- Token refresh: HubSpot OAuth tokens expire after 30 min — needs to plug into the existing refresh scheduler (
OAuthRefreshScheduler) the same way Google Calendar / Notion do. HubSpot returns refresh_token with the standard OAuth flow.
- Required HubSpot scopes (start with these — minimum useful for sales-ops automation):
crm.objects.contacts.read + crm.objects.contacts.write
crm.objects.companies.read + crm.objects.companies.write
crm.objects.deals.read + crm.objects.deals.write
crm.schemas.contacts.read + crm.schemas.companies.read + crm.schemas.deals.read
oauth (always required)
- Keep
api_token field as a fallback for power users who'd rather paste a Private App token (some enterprises ban OAuth apps for compliance). Make the OAuth flow the primary CTA in the UI.
Acceptance criteria
Why this matters now
Sales-ops automation (auto-log calls, auto-create contacts from new Pro signups, auto-update lifecycle stages when a customer expands, etc.) is the killer use case for screenpipe → HubSpot. The Private App token friction is the only thing in the way. Every B2B-shape customer we have (Wildpack, OFF, Pattern, NMedtech, MatcHR, etc.) would benefit from the same automations.
Reference impl
crates/screenpipe-connect/src/connections/notion.rs — copy-paste-adapt the OAuth wiring, swap URLs + scopes.
Related
Problem
Current
crates/screenpipe-connect/src/connections/hubspot.rsuses a manual Private App token field (api_token, secret, pasted by the user). Friction:Surfaced today while trying to auto-update HubSpot from a Claude Code session — wanted to create a contact + company + update lifecycle stages, hit the no-token wall, ended up giving the user a paste-ready table instead of just doing it. Same friction every customer hits.
What's already there
crates/screenpipe-connect/src/connections/hubspot.rs— Bearer-token impl, works, just manualcrate::oauthmodule — already used by other integrations (notion, slack, calcom, gmail, etc.)crates/screenpipe-connect/src/connections/notion.rsis the cleanest reference impl (~30 LoC for the OAuth wiring)Proposed change
Mirror the notion.rs pattern. Concretely:
client_id(and storeclient_secretin the proxy server's env).OAuthConfigto hubspot.rs:oauth_config()returningSome(&OAUTH), drop the manualapi_tokenfield fromIntegrationDef.proxy_config()soProxyAuth::Bearer { credential_key }reads the OAuth access token (same shape as notion).test()to calloauth::read_oauth_token_instance(secret_store, "hubspot", None)(mirror of notion).OAuthRefreshScheduler) the same way Google Calendar / Notion do. HubSpot returnsrefresh_tokenwith the standard OAuth flow.crm.objects.contacts.read+crm.objects.contacts.writecrm.objects.companies.read+crm.objects.companies.writecrm.objects.deals.read+crm.objects.deals.writecrm.schemas.contacts.read+crm.schemas.companies.read+crm.schemas.deals.readoauth(always required)api_tokenfield as a fallback for power users who'd rather paste a Private App token (some enterprises ban OAuth apps for compliance). Make the OAuth flow the primary CTA in the UI.Acceptance criteria
/connectionsshows a "Connect with HubSpot" button (OAuth flow), not a token paste field as primarytest()succeedsOAuthRefreshSchedulerso users don't get logged out after 30 min/connections/hubspot/proxy/crm/v3/objects/contacts) can create/update contacts and companies using the OAuth-acquired tokenWhy this matters now
Sales-ops automation (auto-log calls, auto-create contacts from new Pro signups, auto-update lifecycle stages when a customer expands, etc.) is the killer use case for screenpipe → HubSpot. The Private App token friction is the only thing in the way. Every B2B-shape customer we have (Wildpack, OFF, Pattern, NMedtech, MatcHR, etc.) would benefit from the same automations.
Reference impl
crates/screenpipe-connect/src/connections/notion.rs— copy-paste-adapt the OAuth wiring, swap URLs + scopes.Related
crates/screenpipe-connect/src/oauth.rs— the shared OAuth helper