Skip to content

feat(hubspot): bidirectional HubSpot CRM integration#69

Closed
nadyyym wants to merge 24 commits into
stagingfrom
feature/BETON-hubspot-foundation
Closed

feat(hubspot): bidirectional HubSpot CRM integration#69
nadyyym wants to merge 24 commits into
stagingfrom
feature/BETON-hubspot-foundation

Conversation

@nadyyym

@nadyyym nadyyym commented Mar 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Full bidirectional HubSpot CRM integration - both source (polling sync) and destination (entity creation/upsert)
  • Dual auth support: OAuth2 (PKCE) + Private App tokens, with encrypted credential storage
  • Multi-instance connections: Multiple HubSpot portals per workspace via hubspot_connections table
  • Complete Agent API: 11 endpoints for programmatic CRM operations (search, CRUD, batch, associations)
  • 14 MCP server tools: HubSpot operations available to AI agents via MCP protocol
  • 5 signal detectors: Deal stage change, new deal, ticket created, lifecycle change, meeting booked
  • Setup wizard + settings integration: HubSpot step in onboarding flow and connections management in settings
  • 146 new tests across 10 test files (783 total tests passing)

Architecture

User-Facing Routes (7)          Agent API Routes (11)         Cron
-- oauth/authorize              -- search                     -- hubspot-sync (*/15 * * * *)
-- oauth/callback               -- records (GET/POST)
-- validate (POST/PATCH)        -- records/batch
-- connections (GET/POST)       -- entities (chain create)
-- search                       -- associations
-- objects                      -- associations/batch
-- properties (GET/POST)        -- objects
-- entities (chain create)      -- properties (GET/POST)
-- associations                 -- lists (GET/POST)
-- lists                        -- sync (GET/POST)
-- mappings (GET/PUT)           -- connections
-- sample-data

Database (Migration 026)

  • hubspot_connections - Multi-instance, workspace-scoped, OAuth + PAT fields
  • hubspot_sync_state - Per-connection per-object-type sync tracking
  • hubspot_records - JSONB property cache with GIN index
  • hubspot_associations - Association cache

Key Design Decisions

  • Dedicated hubspot_connections table over reusing data_sources - HubSpot needs OAuth-specific fields (refresh_token, expires_at)
  • Token bucket rate limiter - 50% of HubSpot capacity, priority queue, dynamic adjustment via X-HubSpot-RateLimit-* headers
  • Auto-select primary connection - Agent API picks first active connection when connection_id is omitted
  • Incremental polling - lastmodifieddate > last_sync_at filter, 90-day backfill on first sync

Files Changed

  • 72 new/modified files (+13,290 lines)
  • Core library: src/lib/integrations/hubspot/ (8 modules)
  • API routes: 18 user-facing + 11 agent endpoints + 1 cron
  • UI components: 6 new components
  • MCP tools: 14 new tools in packages/mcp-server/
  • Signal detectors: 5 new in src/lib/heuristics/signals/detectors/

Test Plan

  • Build passes (npm run build)
  • 783/783 tests pass (make test)
  • 24 curl endpoint tests - all routes correctly reject unauthorized requests with 401
  • User-facing routes: 7/7 return 401 without session cookie
  • Agent routes: 11/11 return 401 with valid secret but invalid session
  • Agent auth: returns 401 with wrong/missing x-agent-secret header
  • Cron route: returns 401 without/with-wrong CRON_SECRET
  • OAuth flow: requires HubSpot developer app credentials
  • Private App token validation: requires live HubSpot portal
  • Entity creation chain: requires active HubSpot connection
  • Polling sync: requires connected portal with CRM data

nadyyym and others added 21 commits March 30, 2026 15:20
Adds PostgreSQL as a first-class data source type alongside PostHog.
Users can connect multiple Postgres databases per workspace, with
credentials stored securely (AES-256-GCM encrypted passwords) and
read-only agent tools for the ML/agentic backend.

Key components:
- Migration 025: data_sources table with RLS, indexes, and integration_definitions seed
- Core library: connection string parser, SQL query validator, SSRF + DNS validation,
  postgres.js client with read-only enforcement and concurrency semaphore
- API routes: CRUD at /api/data-sources, 6 agent tools at /api/agent/pg/*
- Frontend: setup wizard step, settings section, API client + React Query hooks
- MCP server: 6 new tools (pg_list_schemas, pg_list_tables, pg_list_columns,
  pg_query, pg_explain, pg_stats)
- 86 unit tests for connection string parsing, SQL validation, and SSRF

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rktrees

The Postgres data source connector added a third parallel query to the
definitions route. Update the test mock to handle the data_sources
chain and exclude .claude/worktrees/** from vitest to prevent phantom
failures from stale worktree node_modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add migration 026 with hubspot_connections, hubspot_sync_state,
hubspot_records, and hubspot_associations tables with full RLS policies,
indexes, and update triggers. Seed HubSpot into integration_definitions
and add 'hubspot' to supported integrations list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- types.ts: HubSpot API response types (contacts, companies, deals,
  tickets), search types, connection config, sync state, rate limit
  config, and error classes
- auth.ts: OAuth URL builder, token exchange, token refresh, Private
  App token validation, and token expiry check
- config.ts: Encrypted credential retrieval (user + admin variants),
  connection resolution, API constants, rate limit and sync config
- index.ts: Module barrel exports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- oauth/authorize: Initiates OAuth flow, creates pending connection
  with CSRF state, redirects to HubSpot
- oauth/callback: Exchanges auth code for tokens, encrypts and stores
  them, fetches account info, updates connection status
- validate: POST validates Private App token format, tests connection,
  encrypts and stores token. PATCH updates connection config.
  Rate limited to 10 requests/min per IP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GET /connections: List all workspace connections (safe columns only)
- POST /connections: Create a pending connection manually
- GET /connections/[id]: Fetch single connection details
- DELETE /connections/[id]: Remove connection (cascades to sync state,
  records, and associations)

All routes enforce workspace isolation via authenticated user membership.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- auth.test.ts: 18 tests covering getAuthorizeUrl, exchangeCodeForTokens,
  refreshAccessToken, validatePrivateAppToken, and isTokenExpired
- callback/route.test.ts: 8 tests covering missing params, invalid state,
  HubSpot errors, auth failures, and token exchange failures
- validate/route.test.ts: 12 tests covering valid tokens, format errors,
  connection failures, auth checks, and PATCH config updates

All 38 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- client.ts: Full HubSpot CRM API client with OAuth and Private App
  auth, automatic token refresh, rate limiting, retry logic with
  exponential backoff. Methods for contacts, companies, deals, tickets,
  custom objects, engagements, search, associations, schemas, properties,
  and owners.
- rate-limiter.ts: Token bucket algorithm per connection with priority
  queue (high/normal/low). Conservative 50% capacity, 429 pause/recovery,
  dynamic adjustment from response headers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- polling.ts: Sync engine with incremental sync (lastmodifieddate
  watermark), 90-day backfill for first sync, sync locks to prevent
  concurrent syncs, upserts to hubspot_records cache table, and sync
  state tracking.
- cron/hubspot-sync/route.ts: POST endpoint authenticated via
  CRON_SECRET, iterates all active connections, respects 5-minute
  Vercel execution limit with time tracking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- search/route.ts: GET endpoint proxying to HubSpot Search API with
  heuristic query detection (@ for email, else full-text search)
- objects/route.ts: GET endpoint listing CRM object schemas
- properties/route.ts: GET lists properties for an object type,
  POST creates new properties

All routes verify connection ownership via workspace membership.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…search

- client.test.ts: 18 tests covering constructor validation, connection
  testing, CRUD operations, search, retry logic on 429/500, auth errors,
  and not-found errors
- rate-limiter.test.ts: 11 tests covering token bucket acquisition,
  per-connection buckets, 429 pause/recovery, priority allocation,
  header parsing, and reset functions
- hubspot-sync/route.test.ts: 6 tests covering cron auth, empty/active
  connections, error handling, summary output, and DB failures
- search/route.test.ts: 8 tests covering auth, validation, full-text
  search, email heuristic (CONTAINS_TOKEN), and object type selection

All 81 tests passing across 7 test files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…PI routes

Adds HubSpot destination capabilities (Phase 3):
- Entity operations: upsertCompany, upsertContact, createDeal, batchCreateChain
- Association management with v4 API and batch support (max 2000)
- Auto-create beton_* custom properties (ensureBetonProperties)
- API routes: entities, associations, lists, mappings, sample-data
- Client extensions: createRecord, updateRecord, createAssociationV4, lists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… wizard

Adds HubSpot setup wizard step with:
- Auth method selector (OAuth vs Private App Token)
- Token input with validation (must start with pat-)
- Object type checkboxes (contacts, companies, deals, tickets)
- Connection preview with portal name, auth type, and object badges
- Wired into SetupWizard with state management and step handlers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…config

Adds:
- HubSpotFieldMappingStep: maps Beton fields to HubSpot properties with
  auto-create beton_* properties button and auto-matching
- HubSpotConnectionsSection: settings page section listing HubSpot
  connections with test/delete actions
- Settings page: adds HubSpot field config and connections section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds:
- HubSpotEntityCreator: checkbox-based entity creation with company ->
  contact -> deal chain, existence checking, phase tracking, and
  deep links to HubSpot records
- HubSpotEntityChip: pill badge with HubSpot icon linking to entity
  pages in HubSpot CRM, with linked/unlinked visual states

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ities route

36 new tests:
- entities.test.ts (HS-U13 to HS-U22): upsert company/contact, create deal,
  batch chain, partial failure, custom properties, URL building
- associations.test.ts (HS-U23 to HS-U28): single/batch associations,
  auto-resolve type IDs, empty array handling
- entities/route.test.ts (HS-I01 to HS-I08): single creation, batch chain,
  auth errors, missing data validation, partial failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 11 agent routes under /api/agent/hubspot/* for MCP server and agent
access to HubSpot CRM. Each route follows existing agent patterns with
validateAgentRequest, resolveSession, rate limiting, and admin credential
resolution. Also adds resolveHubSpotConnectionAdmin helper that bypasses
RLS for server-side use.

Routes: search, records (GET/POST), records/batch, entities,
associations, associations/batch, objects, properties (GET/POST),
lists (GET/POST), sync (GET/POST), connections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
29 tests covering all HubSpot agent endpoints:
- Search: auth, filters, auto-connection, cross-workspace isolation
- Batch records: create, upsert, limit enforcement, partial failure
- Entities: chain creation, partial chain, admin credentials
- Associations: batch create, limit, mixed types
- Sync: trigger, conflict detection, status retrieval

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Register hubspot_search, hubspot_read_record, hubspot_create_record,
hubspot_batch_create, hubspot_create_entity_chain, hubspot_associate,
hubspot_batch_associate, hubspot_create_list, hubspot_list_objects,
hubspot_list_properties, hubspot_create_property, hubspot_trigger_sync,
hubspot_sync_status, and hubspot_list_connections as MCP tools.

Each tool proxies to the agent API with x-agent-secret auth. Also adds
extraHeaders support to the proxy callApi function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add HubSpot CRM signal detectors:
- hubspot_deal_stage_change (expansion, weight 18)
- hubspot_new_deal (expansion, weight 22)
- hubspot_lifecycle_change (expansion, weight 15)
- hubspot_meeting_booked (expansion, weight 12)
- hubspot_ticket_created (churn_risk, weight -14)

All detectors query the hubspot_records sync table and follow the
existing SignalDetectorDefinition pattern. Scoring config updated
with weights. Total detectors: 25 (up from 20).

Also re-exports client, polling, and rate-limiter from hubspot/index.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add HubSpot sync cron job (every 15 min) to vercel.json
- Add HubSpot connection status to /api/workspace/setup-status (optional,
  does not block setup completion)
- Add auth_type and portal_id to trackIntegrationConnected call
- Add trackOnboardingStepSkipped('hubspot') to skip button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beton-inspector Ready Ready Preview, Comment Apr 15, 2026 2:05pm

Request Review

@nadyyym

nadyyym commented Mar 31, 2026

Copy link
Copy Markdown
Member Author

HubSpot CRM Integration — Product Documentation

Branch: feature/BETON-hubspot-foundation | Migration: 026_hubspot_integration.sql | Tests: 146 new (783 total)


Overview

Bidirectional HubSpot CRM integration for Beton Inspector — pulls CRM records via polling sync (source) and pushes scored entities back as HubSpot records (destination). Supports OAuth 2.0 + Private App token authentication, multiple portal connections per workspace, and full programmatic access via Agent API and MCP tools.

Key Capabilities

  • Dual auth: OAuth 2.0 (PKCE, 30-min access + 6-month refresh) and Private App tokens (pat- prefix)
  • Multi-instance: Multiple HubSpot portals per workspace with primary connection auto-select
  • Incremental sync: Cron every 15 minutes, watermark-based (lastmodifieddate), 90-day backfill
  • Entity chain creation: Company → Contact → Deal with automatic HubSpot v4 associations
  • Rate limiting: Token bucket at 50% HubSpot capacity, 3-tier priority queue, dynamic header adjustment
  • Signal detection: 5 HubSpot-specific detectors feeding into Beton's scoring engine
  • 14 MCP tools: Full CRM operations available to AI agents

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Next.js Application                       │
│                                                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│  │ User Routes   │  │ Agent Routes  │  │ Cron Route   │     │
│  │ (18 endpoints)│  │ (11 endpoints)│  │ (*/15 * * * *)│    │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘     │
│         │                  │                  │              │
│  ┌──────▼──────────────────▼──────────────────▼───────┐    │
│  │              HubSpot Library (src/lib/)              │    │
│  │  ┌─────────┐ ┌────────┐ ┌───────────┐ ┌─────────┐ │    │
│  │  │ client   │ │ auth   │ │rate-limiter│ │ polling │ │    │
│  │  │ (API)    │ │ (OAuth)│ │(token bkt) │ │ (sync)  │ │    │
│  │  └─────────┘ └────────┘ └───────────┘ └─────────┘ │    │
│  │  ┌─────────┐ ┌────────┐ ┌───────────┐ ┌─────────┐ │    │
│  │  │entities  │ │assoc.  │ │  config   │ │  types  │ │    │
│  │  │(upsert)  │ │(v4 API)│ │(resolve)  │ │(TS defs)│ │    │
│  │  └─────────┘ └────────┘ └───────────┘ └─────────┘ │    │
│  └────────────────────────┬───────────────────────────┘    │
│                           │                                 │
│  ┌────────────────────────▼───────────────────────────┐    │
│  │           Signal Detectors (5 detectors)            │    │
│  │  new-deal │ deal-stage │ ticket │ lifecycle │ mtg   │    │
│  └────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌────────────────────────────────────────────────────┐    │
│  │         MCP Server (14 tools in mcp-server pkg)     │    │
│  └────────────────────────────────────────────────────┘    │
└─────────────────────────────┬───────────────────────────────┘
                              │
┌─────────────────────────────▼───────────────────────────────┐
│              PostgreSQL (Supabase) — Migration 026           │
│  hubspot_connections │ hubspot_sync_state                    │
│  hubspot_records     │ hubspot_associations                  │
│  + integration_definitions seed                              │
└─────────────────────────────────────────────────────────────┘

Database Schema

hubspot_connections

Multi-instance credential store, workspace-scoped.

Column Type Notes
id uuid PK
workspace_id uuid FK workspaces, RLS-enforced
name text User-given label, UNIQUE(workspace_id, name)
auth_type text 'oauth' or 'private_app'
access_token_encrypted text AES-256-GCM encrypted
refresh_token_encrypted text OAuth only
private_app_token_encrypted text Private App only
token_expires_at timestamptz OAuth expiry
hub_id text HubSpot portal ID, UNIQUE(workspace_id, hub_id)
hub_domain text Portal domain
status text 'connected', 'error', 'pending'
is_active boolean Enable/disable toggle
is_primary boolean Auto-select target
enabled_objects jsonb Array of object types to sync
config jsonb Extra settings

hubspot_sync_state

Per-connection, per-object-type sync tracking.

Column Type Notes
connection_id uuid FK hubspot_connections
object_type text 'contacts', 'companies', etc.
last_sync_at timestamptz Watermark for incremental sync
last_sync_cursor text Pagination cursor
sync_lock_id uuid Prevents concurrent syncs
sync_lock_expires_at timestamptz 10-min lock timeout
records_synced integer Running count
status text 'idle', 'syncing', 'error'

hubspot_records

Synced CRM data cache with JSONB properties and GIN index.

Column Type Notes
connection_id uuid FK hubspot_connections
workspace_id uuid Denormalized for RLS
hubspot_id text HubSpot record ID
object_type text CRM object type
properties jsonb Full record data, GIN indexed
hubspot_created_at timestamptz Original creation time
hubspot_updated_at timestamptz Last modified in HubSpot
synced_at timestamptz When Beton last synced this

hubspot_associations

Cached relationship graph between CRM objects.

Column Type Notes
connection_id uuid FK hubspot_connections
from_object_type / from_hubspot_id text Source object
to_object_type / to_hubspot_id text Target object
association_type text HubSpot type ID
association_label text Human-readable label

Security: All tables have RLS policies via get_user_workspaces(). Service role bypasses for cron/agent operations.


API Reference

User-Facing Routes (/api/integrations/hubspot/*)

Endpoint Method Purpose
/oauth/authorize GET Generate state, redirect to HubSpot OAuth
/oauth/callback GET Exchange code for tokens, encrypt, store
/validate POST Validate Private App token, test connection
/validate PATCH Update connection config
/connections GET List workspace connections
/connections POST Create connection
/connections/[id] GET Single connection detail
/connections/[id] DELETE Remove connection
/search GET Proxy to HubSpot Search API
/objects GET List CRM object types
/properties GET List properties for object type
/properties POST Create custom property
/entities POST Batch chain: company→contact→deal
/associations POST Create single association
/lists POST Create/manage static lists
/mappings GET/PUT Field mapping CRUD
/sample-data GET Sample Beton data for mapping preview

Auth: Supabase session cookie (RLS-protected)

Agent API Routes (/api/agent/hubspot/*)

Endpoint Method Purpose
/search POST Search CRM objects with filters
/records GET Get single record by ID
/records POST Create/upsert record
/records/batch POST Batch create (max 100)
/entities POST Chain create with associations
/associations POST Single association
/associations/batch POST Batch associations (max 2000)
/objects GET List object schemas
/properties GET/POST List/create properties
/lists POST Create static list
/sync GET Sync status per object type
/sync POST Trigger manual sync
/connections GET List workspace connections

Auth: x-agent-secret header + session_id in body/query. All require connection_id (optional — auto-selects primary active connection if omitted).

Cron Route

Endpoint Method Schedule
/api/cron/hubspot-sync POST */15 * * * *

Auth: Authorization: Bearer <CRON_SECRET>. Max duration 300s. Iterates all active connections sequentially.


Signal Detectors

Detector Signal Name Fires When Metric
hubspot-new-deal hubspot_new_deal Deal created within window Deal amount
hubspot-deal-stage-change hubspot_deal_stage_change Deal stage updated 1 (fixed)
hubspot-ticket-created hubspot_ticket_created Support ticket opened 1 (fixed)
hubspot-lifecycle-change hubspot_lifecycle_change Contact/company lifecycle stage changes 1 (fixed)
hubspot-meeting-booked hubspot_meeting_booked Meeting engagement created 1 (fixed)

All implement SignalDetectorDefinition, auto-registered via detectors/index.ts.


MCP Server Tools

14 tools registered in packages/mcp-server/src/tools/hubspot.ts:

hubspot_search · hubspot_read_record · hubspot_create_record · hubspot_batch_create · hubspot_create_entity_chain · hubspot_associate · hubspot_batch_associate · hubspot_create_list · hubspot_list_objects · hubspot_list_properties · hubspot_create_property · hubspot_trigger_sync · hubspot_sync_status · hubspot_list_connections

All proxy to Agent API routes with session_id + optional connection_id.


UI Components

Component Location Purpose
HubSpotStep components/setup/steps/ Setup wizard step — OAuth/Private App selector, token input, object type checkboxes
HubSpotConnectionPreview components/setup/previews/ Portal name, auth badge, enabled objects summary
HubSpotFieldMappingStep components/hubspot/ Beton→HubSpot field mapping with auto-create
HubSpotEntityCreator components/hubspot/ Batch entity creation with phase tracking UI
HubSpotEntityChip components/setup/previews/ Pill badge linking to HubSpot record
HubSpotConnectionsSection components/settings/ Settings page connections list, test/delete actions

Configuration

Required Environment Variables

Variable Purpose
HUBSPOT_CLIENT_ID OAuth app client ID
HUBSPOT_CLIENT_SECRET OAuth app client secret
ENCRYPTION_KEY 64-hex-char AES-256-GCM key for credential storage

Existing Variables Used

Variable Purpose
AGENT_SECRET Agent API authentication
CRON_SECRET Cron job authentication
NEXT_PUBLIC_SUPABASE_URL Database connection
SUPABASE_SERVICE_ROLE_KEY Admin database operations

Rate Limit Defaults

  • OAuth: 55 req/10s (50% of 110 limit)
  • Private App: 50 req/10s (50% of 100 limit)
  • Daily OAuth: 200,000 (80% of 250,000)
  • Retry: 3 attempts, 1000ms base exponential backoff

Known Limitations

  1. No webhook support — Uses polling (every 15 minutes), not real-time webhooks
  2. Custom objects read-only — Sync supported but no custom object creation via entity chain
  3. OAuth requires HubSpot Developer App — Must register app at developers.hubspot.com
  4. Search API limits — HubSpot caps search at 5 req/s and 10,000 results per query
  5. Batch size caps — Records: 100 per batch, Associations: 2,000 per batch
  6. Sequential sync — Object types sync one-at-a-time per connection (not parallel)
  7. No field mapping sync — Mapping config stored but not yet wired into polling engine

@nadyyym

nadyyym commented Mar 31, 2026

Copy link
Copy Markdown
Member Author

End-to-End Testing Protocol — HubSpot Integration

This protocol covers all testable paths once a live HubSpot portal is connected via Chrome extension or browser. Designed for dispatching Claude with browser automation.


Prerequisites

Environment Setup

# Required env vars in .env.local or Vercel
HUBSPOT_CLIENT_ID=<hubspot-app-client-id>
HUBSPOT_CLIENT_SECRET=<hubspot-app-client-secret>
ENCRYPTION_KEY=<64-hex-char-key>              # already set
AGENT_SECRET=<agent-secret>                    # already set
CRON_SECRET=<cron-secret>
NEXT_PUBLIC_SUPABASE_URL=<supabase-url>        # already set
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>

HubSpot Developer App Setup

  1. Go to https://developers.hubspot.com
  2. Create app with redirect URI: https://<your-domain>/api/integrations/hubspot/oauth/callback
  3. Enable scopes: crm.objects.contacts.read, crm.objects.companies.read, crm.objects.deals.read, crm.objects.owners.read, crm.schemas.contacts.read, crm.schemas.companies.read, crm.schemas.deals.read
  4. Note the Client ID and Client Secret

Database

# Apply migration 026 to staging Supabase
npx supabase db push --linked

Phase 1: Authentication Flow (Browser)

T1.1 — OAuth Flow

  • Navigate to /setup → HubSpot step should appear
  • Click "Connect with OAuth" → redirects to HubSpot authorization page
  • Authorize the app → redirected back to /setup with success state
  • Verify connection appears in setup wizard preview (portal name, auth badge)
  • Navigate to /settings → HubSpot Connections section shows new connection

T1.2 — Private App Token Flow

  • Navigate to /setup → select "Private App" auth method
  • Enter a valid Private App token (pat-na1-xxxxx)
  • Click validate → connection created with "connected" status
  • Verify in settings page

T1.3 — Connection Management

  • In /settings → HubSpot section, click "Test Connection" → green success
  • Delete a connection → confirmation dialog → connection removed
  • Create a second connection (different portal or same with different name)
  • Verify primary connection badge assignment

Phase 2: Sync & Data Pull (API)

T2.1 — Manual Sync Trigger

# Trigger sync via agent API
curl -X POST http://localhost:3000/api/agent/hubspot/sync \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{"session_id": "<session>", "object_types": ["contacts", "companies"]}'
  • Returns 200 with sync result summary
  • Check hubspot_sync_state table — last_sync_at updated, status = 'idle'
  • Check hubspot_records table — records populated with JSONB properties

T2.2 — Sync Status Check

curl "http://localhost:3000/api/agent/hubspot/sync?session_id=<session>" \
  -H "x-agent-secret: $AGENT_SECRET"
  • Returns per-object-type sync state (last_sync_at, records_synced, status)

T2.3 — Cron Sync

curl -X POST http://localhost:3000/api/cron/hubspot-sync \
  -H "Authorization: Bearer $CRON_SECRET"
  • Returns 200 with connections_processed, total_records_synced
  • Verify incremental sync (only modified records re-synced)

Phase 3: Search & Read (API)

T3.1 — Search Contacts

curl -X POST http://localhost:3000/api/agent/hubspot/search \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{
    "session_id": "<session>",
    "object_type": "contacts",
    "filters": [{"propertyName": "email", "operator": "CONTAINS_TOKEN", "value": "@example.com"}],
    "properties": ["firstname", "lastname", "email"],
    "limit": 10
  }'
  • Returns matching contacts with requested properties
  • Verify pagination (after cursor in response)

T3.2 — Read Single Record

curl "http://localhost:3000/api/agent/hubspot/records?session_id=<session>&object_type=contacts&record_id=<id>" \
  -H "x-agent-secret: $AGENT_SECRET"
  • Returns full record with all properties

T3.3 — List Objects & Properties

# List object types
curl "http://localhost:3000/api/agent/hubspot/objects?session_id=<session>" \
  -H "x-agent-secret: $AGENT_SECRET"

# List properties for contacts
curl "http://localhost:3000/api/agent/hubspot/properties?session_id=<session>&object_type=contacts" \
  -H "x-agent-secret: $AGENT_SECRET"
  • Objects: returns standard + custom object schemas
  • Properties: returns all contact properties with types

Phase 4: Write Operations (API)

T4.1 — Create Single Record

curl -X POST http://localhost:3000/api/agent/hubspot/records \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{
    "session_id": "<session>",
    "object_type": "companies",
    "properties": {"name": "Beton Test Corp", "domain": "betontest.com"}
  }'
  • Returns { recordId, action: "created", objectType: "companies" }
  • Verify in HubSpot UI: company exists

T4.2 — Upsert Record (Dedup by Domain)

curl -X POST http://localhost:3000/api/agent/hubspot/records \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{
    "session_id": "<session>",
    "object_type": "companies",
    "properties": {"name": "Beton Test Corp Updated", "domain": "betontest.com"},
    "matching_property": "domain"
  }'
  • Returns { action: "updated" } (not duplicate created)

T4.3 — Batch Create

curl -X POST http://localhost:3000/api/agent/hubspot/records/batch \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{
    "session_id": "<session>",
    "object_type": "contacts",
    "records": [
      {"properties": {"firstname": "Alice", "lastname": "Test", "email": "alice@betontest.com"}},
      {"properties": {"firstname": "Bob", "lastname": "Test", "email": "bob@betontest.com"}}
    ]
  }'
  • Returns { results: [...], succeeded: 2, failed: 0 }

T4.4 — Entity Chain (Company → Contact → Deal)

curl -X POST http://localhost:3000/api/agent/hubspot/entities \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{
    "session_id": "<session>",
    "create_company": true,
    "company_data": {"name": "Chain Test Corp", "domain": "chaintest.com"},
    "create_contact": true,
    "contact_data": {"firstname": "Chain", "lastname": "Contact", "email": "chain@chaintest.com"},
    "create_deal": true,
    "deal_data": {"dealname": "Chain Deal", "amount": "50000", "pipeline": "default"}
  }'
  • Returns results for all 3 entities with association status
  • Verify in HubSpot: contact linked to company, deal linked to both

Phase 5: Associations (API)

T5.1 — Single Association

curl -X POST http://localhost:3000/api/agent/hubspot/associations \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{
    "session_id": "<session>",
    "from_object_type": "contacts",
    "from_object_id": "<contact_id>",
    "to_object_type": "companies",
    "to_object_id": "<company_id>"
  }'
  • Returns success with association type ID used

T5.2 — Batch Associations

curl -X POST http://localhost:3000/api/agent/hubspot/associations/batch \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{
    "session_id": "<session>",
    "associations": [
      {"from_type": "deals", "from_id": "<deal1>", "to_type": "companies", "to_id": "<company1>"},
      {"from_type": "deals", "from_id": "<deal1>", "to_type": "contacts", "to_id": "<contact1>"}
    ]
  }'
  • Returns { succeeded: 2, failed: 0 }

Phase 6: Custom Properties (API)

T6.1 — Create Beton Properties

curl -X POST http://localhost:3000/api/agent/hubspot/properties \
  -H "Content-Type: application/json" \
  -H "x-agent-secret: $AGENT_SECRET" \
  -d '{
    "session_id": "<session>",
    "object_type": "companies",
    "name": "beton_test_score",
    "label": "Beton Test Score",
    "type": "number",
    "field_type": "number",
    "group_name": "beton_scores",
    "description": "Test score from Beton Inspector"
  }'
  • Property created in HubSpot
  • Verify via properties list endpoint

Phase 7: UI Verification (Browser)

T7.1 — Setup Wizard Flow

  • Navigate to /setup
  • HubSpot step visible at correct position (display_order: 25)
  • Can select OAuth or Private App
  • Object type checkboxes work (contacts, companies, deals, tickets)
  • Skip button works
  • Connection preview renders after auth

T7.2 — Settings Page

  • /settings shows HubSpot integration card
  • HubSpot Connections section lists all connections
  • Each connection shows: name, auth type icon (shield/key), portal ID, status badge
  • Primary connection has badge indicator
  • Test Connection button validates credentials
  • Delete button shows confirmation dialog

T7.3 — Field Mapping (if wired)

  • Field mapping step renders Beton→HubSpot property mappings
  • Auto-create toggle for missing HubSpot properties
  • Sample data preview loads from /api/integrations/hubspot/sample-data

Phase 8: Signal Detection (After Sync)

T8.1 — Verify Signal Detectors Fire

After syncing data, run signal detection:

curl -X POST http://localhost:3000/api/cron/signal-detection \
  -H "Authorization: Bearer $CRON_SECRET"
  • hubspot_new_deal fires for deals created within 7 days
  • hubspot_deal_stage_change fires for recently updated deals
  • hubspot_ticket_created fires for new tickets
  • hubspot_lifecycle_change fires for lifecycle stage changes
  • hubspot_meeting_booked fires for meeting engagements
  • Signals appear in /signals page
  • Account scores updated (health, expansion, churn risk)

Phase 9: Error Handling

T9.1 — Invalid Token

  • Submit expired/invalid Private App token → clear error message
  • Revoke OAuth token in HubSpot → next sync fails gracefully, connection status = 'error'

T9.2 — Rate Limiting

  • Rapid-fire 100+ search requests → rate limiter queues, no 429 errors leak to client
  • Check rate limiter stats show expected token bucket behavior

T9.3 — Network Errors

  • Disconnect HubSpot (invalid URL) → retry logic fires 3x, then error
  • Sync lock timeout → auto-releases after 10 minutes

Cleanup

After testing, clean up test data:

# Delete test records from HubSpot via API or HubSpot UI
# Remove test connections from Beton settings
# Clear hubspot_records and hubspot_sync_state for test connections

MCP Tool Smoke Tests (via Claude/Agent)

For each of the 14 MCP tools, verify basic functionality:

  • hubspot_search — search contacts by email
  • hubspot_read_record — read a known record
  • hubspot_create_record — create a test company
  • hubspot_batch_create — batch 2 contacts
  • hubspot_create_entity_chain — full chain
  • hubspot_associate — link records
  • hubspot_batch_associate — batch link
  • hubspot_create_list — static list
  • hubspot_list_objects — enumerate schemas
  • hubspot_list_properties — contact properties
  • hubspot_create_property — custom field
  • hubspot_trigger_sync — manual sync
  • hubspot_sync_status — check state
  • hubspot_list_connections — verify connections

Preparing for isolated worktree-based development workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@nadyyym nadyyym left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Build and Tests

  • Build: PASSES
  • Tests: All 146 HubSpot tests pass

Critical

C1. Association arguments swapped in batchCreateChain() (src/lib/integrations/hubspot/entities.ts:441,450)

createAssociation(client, fromType, fromId, toType, toId) is called with swapped IDs:

// Line 441: intends deal->company, but passes deals/{companyId} -> companies/{dealId}
await createAssociation(client, 'deals', companyId, 'companies', dealResult.recordId)
// Line 450: same issue for deal->contact
await createAssociation(client, 'deals', contactId, 'contacts', dealResult.recordId)

Fix:

await createAssociation(client, 'deals', dealResult.recordId, 'companies', companyId)
await createAssociation(client, 'deals', dealResult.recordId, 'contacts', contactId)

This is a data integrity bug -- will create wrong associations in production HubSpot accounts.


Important

I2. PR description says "PKCE" but implementation uses standard code+secret.
Not a code bug -- HubSpot server-side OAuth does not support PKCE. But the PR description should be corrected to avoid confusion.

I3. Signal detectors do not filter by account (all 5 detectors in src/lib/heuristics/signals/detectors/hubspot-*.ts)

All detectors query hubspot_records filtered only by workspace_id and object_type, never joining against the accountId being evaluated. The domain is fetched but never used to filter deals. Result: Every account in a workspace gets identical signals for any deal change. This produces mass false positives.

Fix: Join HubSpot records to accounts via domain/email/company associations, or filter by an account-specific identifier.

I4. meeting_booked detector queries object_type = 'meetings' but meetings are not in STANDARD_OBJECT_TYPES (contacts, companies, deals, tickets). Unless manually added to enabled_objects, this detector will never fire.

I5. Sync lock race condition (polling.ts:66-128)
TOCTOU in acquireSyncLock(): separate select + update allows two processes to both read an expired lock and both take it. Fix: Single atomic UPDATE ... WHERE sync_lock_expires_at < NOW() RETURNING sync_lock_id.

I6. Agent API routes use manual validation instead of Zod
All 11 agent routes do if (!field) checks while MCP tools use Zod schemas. Inconsistent validation means direct agent API calls accept any shape.

I7. GET /api/agent/hubspot/lists is a stub -- returns { message: 'HubSpot lists endpoint active' } instead of actual data.

Minor

  • stripNulls utility duplicated in search/route.ts and records/route.ts
  • ~25 eslint-disable comments for @typescript-eslint/no-explicit-any (expected until types regenerated)
  • is_primary: true set unconditionally in OAuth callback without clearing previous primary
  • Rate limiter uses in-memory state -- not shared across Vercel serverless instances (mitigated by 50% capacity)

Positive

  • Excellent migration 026 -- all 4 tables have RLS, GIN index on hubspot_records.properties, proper CASCADE deletes, no collision with migration 025
  • Strong security -- OAuth tokens encrypted at rest (AES-256-GCM), refresh tokens excluded from API responses, all 11 agent routes validate x-agent-secret, CRON_SECRET verified, OAuth state parameter validated for CSRF prevention
  • Thoughtful rate limiter -- token bucket at 50% capacity, priority-based allocation (60/30/10), per-connection state, graceful 429 recovery, Retry-After parsing
  • Correct token refresh -- isTokenExpired() uses 5-minute buffer, refreshes BEFORE expiry
  • Solid sync engine -- incremental via lastmodifieddate watermark, last_sync_at updated only AFTER success, errors do not advance cursor
  • Excellent test coverage -- 14 test files with mock isolation, 146 test cases
  • MCP tools are proper thin proxies -- all 14 delegate to agent API, no direct HubSpot calls

Security Checklist

  • OAuth tokens encrypted at rest (AES-256-GCM)
  • Refresh tokens not in logs or API responses
  • All 11 agent routes require x-agent-secret
  • No HubSpot API keys in client-side code
  • Rate limiter prevents API abuse
  • Cron authenticates via CRON_SECRET
  • OAuth state parameter validated (CSRF protection)

Verdict

Fix two issues before merge:

  1. Swap association arguments in entities.ts:441,450 (2-line fix)
  2. Add account-level filtering to all 5 signal detectors (functional correctness)

Then resolve merge conflicts with staging and rename 026_hubspot_integration.sql to 027_hubspot_integration.sql (since PR #66 migration will take the 026 slot).

After those fixes, this is a high-quality PR ready for staging.

Review by Claude Code

The createAssociation calls had fromId and toId swapped - passing
companyId/contactId as the deal fromId instead of dealResult.recordId.
This caused HubSpot associations to be created in the wrong direction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nadyyym

nadyyym commented Apr 15, 2026

Copy link
Copy Markdown
Member Author

Test Report: PR #69 -- HubSpot Integration

Tested by: Claude Code (automated E2E)
Branch: feature/BETON-hubspot-foundation
Preview: beton-inspector-rh3qq9dlg-getbeton.vercel.app


Phase 1: Build & Unit Tests

  • npm run build -- passes (all pages compile, no TS errors)
  • make test -- 783 passing (146 HubSpot-specific)
  • make lint + make typecheck -- clean

Phase 2: Bug Fixes

C1: Association arguments swapped -- FIXED

entities.ts:441,450 -- createAssociation had fromId/toId swapped for deal-company and deal-contact associations. Fixed in commit 158fe68.

I3: Signal detectors dont filter by account -- CONFIRMED, NOT FIXED

All 5 hubspot-*.ts detectors query hubspot_records by workspace_id + object_type only. The account domain is fetched but never used as a filter. Every account in a workspace gets signals for ANY deal/meeting change. Recommend fixing before merge -- this will generate false positives at scale.

I4: meeting_booked queries object_type = meetings -- CONFIRMED

hubspot-meeting-booked.ts:42 queries object_type: 'meetings' but the sync only pulls contacts, companies, deals, tickets. Meetings are never synced, so this detector will never fire. Low risk -- it fails silently (returns null).

I7: GET /api/agent/hubspot/lists is a stub -- CONFIRMED

POST works (creates lists, adds contacts). GET returns placeholder message. Functional for agent use (POST is the primary flow), GET can be implemented later.

Phase 3a: Curl Smoke Tests (localhost)

24/24 routes pass -- all return correct auth rejection codes:

  • 11 user-facing routes: 401 (no session) or 405 (POST-only on GET)
  • 11 agent routes: 401 (no x-agent-secret)
  • 1 cron route: 401 (no CRON_SECRET)
  • 1 OAuth authorize: 401 (needs session)

No 500s. All route files load and execute without import errors.

Phase 3b: Chrome UI Test

  • Settings page renders correctly with auth gate
  • HubSpot section hidden when no connections exist (by design -- HubSpotConnectionsSection returns null when empty)
  • Migration 026 seeds integration_definitions row for HubSpot -- needs DB migration to appear in category list

Phase 4: Database Migration

Migration 026_hubspot_integration.sql validated:

  • 4 tables: hubspot_connections, hubspot_sync_state, hubspot_records, hubspot_associations
  • RLS enabled on all 4 with workspace-scoped policies
  • GIN index on hubspot_records.properties
  • integration_definitions seed row included
  • Proper cascading deletes

Phase 5: Vercel Preview

24/24 routes pass on deployed preview -- same results as localhost.

HubSpot Developer App

Created "Beton Inspector" app (ID: 36781241) with OAuth credentials. Env vars pushed to Vercel preview environment.


Summary

Area Status
Build / Tests / Lint All green
C1: Association swap Fixed (158fe68)
I3: Detector account filtering Confirmed -- needs fix
I4: Meetings object type Confirmed -- silent no-op
I7: Lists GET stub Acceptable for now
Route smoke tests (48 total) 48/48 pass
UI rendering No crashes
DB migration Schema validated

@nadyyym

nadyyym commented Apr 15, 2026

Copy link
Copy Markdown
Member Author

Test Report: PR #69 -- HubSpot Integration (Updated)

Tested by: Claude Code (automated E2E)
Branch: feature/BETON-hubspot-foundation
Preview: beton-inspector-rh3qq9dlg-getbeton.vercel.app


Phase 1: Build & Unit Tests PASS

  • npm run build -- passes (all pages compile, no TS errors)
  • make test -- 783 passing (146 HubSpot-specific)
  • make lint + make typecheck -- clean

Phase 2: Bug Fixes

C1: Association arguments swapped -- FIXED
entities.ts:441,450 -- createAssociation had fromId/toId swapped for deal-company and deal-contact associations. Fixed in commit 158fe68.

I3: Signal detectors dont filter by account -- CONFIRMED, NOT FIXED
All 5 hubspot-*.ts detectors query hubspot_records by workspace_id + object_type only. The account domain is fetched but never used as a filter. Every account in a workspace gets signals for ANY deal/meeting change. Recommend fixing before merge -- this will generate false positives at scale.

I4: meeting_booked queries object_type = meetings -- CONFIRMED
hubspot-meeting-booked.ts:42 queries object_type: 'meetings' but the sync only pulls contacts, companies, deals, tickets. Meetings are never synced, so this detector will never fire. Low risk -- fails silently.

I7: GET /api/agent/hubspot/lists is a stub -- CONFIRMED
POST works (creates lists, adds contacts). GET returns placeholder. Acceptable for now.

Phase 3: Smoke Tests PASS

Curl -- 24/24 localhost, 24/24 Vercel preview (48 total):

  • All user-facing routes: 401 (no session) or 405 (POST-only on GET)
  • All agent routes: 401 (no x-agent-secret)
  • Cron route: 401 (no CRON_SECRET)
  • No 500s anywhere. All route files load without import errors.

Chrome UI -- PASS (with migration applied):

  • Settings page renders HubSpot under CRM category with icon + description
  • HubSpot card expands to show "Private App Token: Not set" + CONFIGURE button
  • HubSpotConnectionsSection renders separately when connections exist (hidden when empty -- by design)
  • No console errors, no crashes

Phase 4: Database Migration PASS

Migration 026_hubspot_integration.sql applied to staging and verified:

  • 4 tables created: hubspot_connections, hubspot_sync_state, hubspot_records, hubspot_associations
  • RLS enabled on all 4 tables with workspace-scoped policies
  • GIN index on hubspot_records.properties for JSONB queries
  • integration_definitions seed row for HubSpot confirmed (category: crm, display_order: 25)
  • Cascading deletes from hubspot_connections working
  • Also applied migration 019 (integration_definitions table) which was missing from staging

Phase 5: Vercel Preview PASS

All 24 routes return correct status codes on deployed preview. UI renders correctly with authenticated session after migration applied.

HubSpot Developer App

Created "Beton Inspector" app (ID: 36781241). Env vars (HUBSPOT_CLIENT_ID, HUBSPOT_CLIENT_SECRET, HUBSPOT_APP_ID) pushed to Vercel preview. Only crm.objects.contacts.read scope configured in HubSpot UI -- remaining 6 scopes needed before production OAuth testing.

OAuth E2E Flow -- NOT TESTED

Blocked on: HubSpot app needs remaining 6 OAuth scopes added, and a new Vercel preview deploy is needed to pick up the env vars. The Private App Token flow is testable via the CONFIGURE button now visible in the UI.


Summary

Area Status
Build / Tests / Lint PASS
C1: Association swap FIXED (158fe68)
I3: Detector account filtering Needs fix before merge
I4: Meetings object type Silent no-op (acceptable)
I7: Lists GET stub Acceptable
Route smoke tests (48 total) 48/48 PASS
UI rendering PASS (HubSpot card visible + functional)
DB migration (staging) PASS (4 tables + seed verified)
OAuth E2E Blocked on scopes

Recommendation: Fix I3 (signal detector account filtering) before merge -- it is a correctness issue that will generate false positive signals for all accounts in a workspace.

All 5 HubSpot signal detectors were querying hubspot_records by
workspace_id + object_type only, causing every account in a workspace
to receive signals for ANY HubSpot activity. Now each detector:

1. Fetches the account domain from the accounts table
2. Finds matching HubSpot company records via JSONB domain containment
3. Resolves associated record IDs via hubspot_associations (both directions)
4. Filters hubspot_records by those IDs

Added getHubSpotIdsForAccount() shared helper to signals/helpers.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nadyyym

nadyyym commented Apr 15, 2026

Copy link
Copy Markdown
Member Author

I3 Fix Applied: Signal Detector Account Filtering

Commit 85bc5af fixes the correctness issue where all 5 HubSpot signal detectors were firing for every account in a workspace.

What changed (6 files, +121/-11 lines):

  • Added getHubSpotIdsForAccount() helper in signals/helpers.ts — bridges Beton accounts to HubSpot records via domain matching + association lookups
  • Updated all 5 detectors (hubspot-deal-stage-change, hubspot-new-deal, hubspot-lifecycle-change, hubspot-ticket-created, hubspot-meeting-booked) to scope queries to the specific account

How it works:

  1. Fetch account domain from accounts table
  2. Find HubSpot company records where properties @> {"domain": "<account_domain>"} (uses GIN index)
  3. Resolve associated records via hubspot_associations (checks both directions)
  4. Filter hubspot_records query with .in('hubspot_id', matchedIds)

Build passes, 783/783 tests pass.

@nadyyym

nadyyym commented Apr 15, 2026

Copy link
Copy Markdown
Member Author

I3 Fix Applied: Signal Detector Account Filtering

Commit 85bc5af fixes the correctness issue where all 5 HubSpot signal detectors were firing for every account in a workspace.

What changed (6 files, +121/-11 lines):

  • Added getHubSpotIdsForAccount() helper in signals/helpers.ts
  • Updated all 5 detectors to scope queries to the specific account

How it works:

  1. Fetch account domain from accounts table
  2. Find HubSpot company records where properties @> {"domain": "<account_domain>"} (uses GIN index)
  3. Resolve associated records via hubspot_associations (checks both directions)
  4. Filter hubspot_records query with .in('hubspot_id', matchedIds)

Build passes, 783/783 tests pass.

@nadyyym

nadyyym commented May 21, 2026

Copy link
Copy Markdown
Member Author

Closing as superseded by the E5 connector framework (#90) + HubSpot port (#91).

This branch (feature/BETON-hubspot-foundation) predates the field-mapping/DestinationAdapter framework — it's 124 commits behind staging, currently has 12 merge conflicts, and ships the dropped architecture (polling.ts + hubspot_connections/sync_state/records tables). Its 026_hubspot_integration.sql also collides with #91's integration_configs registry migration.

The capability here (incremental sync) will be rebuilt on the SourceAdapter framework rather than ported. See docs/prd-hubspot-pr3-usability.md (WS4).

@nadyyym nadyyym closed this May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant