Skip to main content
The HubSpot template (apps/hubspot-clone, name hubspot) is an API-only clone of the HubSpot CRM API: a NestJS backend on Postgres that models the CRM the way HubSpot does — every object (contacts, companies, deals, tickets) is a generic record carrying a free-form properties map, wired together by an associations graph, with properties metadata, pipelines + stages, and owners on the side. It’s faithful at the path + wire-format level: HubSpot’s CRM API is REST with stable routes, so the clone matches GET /crm/v3/objects/contacts path-for-path, uses HubSpot’s { results, paging: { next: { after } } } collection envelope, the { total, results } search envelope, the { status, message, correlationId, category } error envelope, and returns numeric-string ids. Associations are on the v4 surface (/crm/v4/…), and requests are metered by a HubSpot-accurate rate limiter (see below).
asymmetric spin hubspot

What’s inside

PieceNotes
backend/NestJS REST API, Postgres via pg. Health at /api/health. Port 3005.
supabase/migrations/001_initial_schema.sql — the single DDL migration (schema only), run on spin and replayed on reset.
seeds/acme.sql — the bundled deterministic fixture (stock properties, pipelines, stages, owners, token, sample records).

Modes

ModeServicesUse it for
api (default)backendAgents that talk to the HubSpot-style REST API.
This is an API-only template — there is no frontend.

Authentication — private-app tokens

Real HubSpot apps authenticate with a private-app access token (pat-na1-…) in the Authorization header. The clone mirrors that: the token is the canonical credential, sha256-hashed and resolved to its owning portal (tenant). Every spin applies the acme fixture, which seeds a deterministic token:
pat-na1-00000000-0000-4000-8000-000000000001
Use it as a bearer token against the REST API — no signup/login step:
curl -s http://127.0.0.1:3005/api/crm/v3/objects/contacts \
  -H "authorization: Bearer pat-na1-00000000-0000-4000-8000-000000000001"
The clone also implements HubSpot’s real OAuth token-exchange route, POST /api/oauth/v1/token, and a parity POST /api/auth/login (admin@acme.test / password123) that issues a portal-scoped JWT so the web/CLI path works — HubSpot itself has no email/password login route. Mint additional private-app tokens via POST /api/account-info/v3/api-keys (plaintext shown once).

Rate limiting

Requests pass through a HubSpot-accurate limiter (a global interceptor behind the auth guard). Successful responses carry the exact five HubSpot headers:
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Daily
X-HubSpot-RateLimit-Daily-Remaining
Tiers match HubSpot’s published limits: private-app 100 / 190 / 250 requests per 10 s (Free·Starter / Pro·Enterprise / API-add-on), public OAuth 110 / 10 s, with daily caps of 250k / 625k / 1M. Tripping the window returns a real 429 whose body uses HubSpot’s documented policyName values (TEN_SECONDLY_ROLLING, DAILY):
{
  "status": "error",
  "message": "You have reached your ten-secondly rolling limit.",
  "category": "RATE_LIMIT",
  "policyName": "TEN_SECONDLY_ROLLING"
}

Seeding

asymmetric spin hubspot --seed acme     # deterministic fixture on create
asymmetric seed hubspot-a1b2 --ai       # realistic data via the REST API
AI seeding generates contacts, companies, and deals (the template’s declared entities) through real CRM v3 create calls. See Seeding.

The object model — read this before pointing an agent at it

Everything in HubSpot’s CRM is a record with a property bag. The clone keeps that shape exactly:
# create a contact
curl -s -X POST http://127.0.0.1:3005/api/crm/v3/objects/contacts \
  -H "authorization: Bearer pat-na1-…" -H 'content-type: application/json' \
  -d '{"properties":{"email":"jane@example.com","firstname":"Jane"}}'

# search with HubSpot filterGroups → { total, results }
curl -s -X POST http://127.0.0.1:3005/api/crm/v3/objects/contacts/search \
  -H "authorization: Bearer pat-na1-…" -H 'content-type: application/json' \
  -d '{"filterGroups":[{"filters":[{"propertyName":"lastname","operator":"EQ","value":"Doe"}]}]}'

# associate a deal with a company (v4 default association)
curl -s -X PUT \
  http://127.0.0.1:3005/api/crm/v4/objects/deals/1003/associations/default/companies/1001 \
  -H "authorization: Bearer pat-na1-…"
Beyond plain CRUD, objects also expose batch/{read,create,update,archive,upsert}, merge, and gdpr-delete. Associations are the full v4 surface — default and labeled PUT (the label body array is honored), DELETE, the four batch/{create,read,archive,labels/archive} endpoints, and GET …/labels. Properties carry CRUD plus batch/{read,create,archive} and property groups CRUD; pipelines and their stages have full read and write paths.

Fidelity

The template ships an API_PARITY.md re-audited 2026-06-28 against HubSpot’s live docs. Scope is the CRM resource groups the clone targets (objects, associations, properties, pipelines, owners) plus the OAuth token endpoint — 55 endpoints:
StatusCount
✅ exact (path + shape align)50
⚠️ shape-mismatch0
❌ missing5
That’s 91% (50/55) on both capability and shape — they’re equal because every implemented endpoint is also shape-exact. The 5 missing are narrow: association label-definition writes (POST / PUT / DELETE …/associations/{ft}/{tt}/labels) and the two property-group batch endpoints. Seven extra routes are clone-local control-plane (/auth/*, /account-info/v3/api-keys, /health); an eighth is a deprecated v3 association-read alias retained for back-compat. On the strict dimensions the audit grades: id-format passes (numeric-string record ids, plus HubSpot’s own mixed string-id / numeric-toObjectId convention inside association payloads) and rate-limits passes (the limiter above). Webhooks/events and an MCP server are unimplemented, so those two dimensions are N/A — the clone never claims to emit events or expose MCP. Whole HubSpot families outside the core — engagements/timeline, workflows, marketing, CMS, conversations, files, quotes, custom object schemas — are unimplemented by design. See apps/hubspot-clone/API_PARITY.md for the per-endpoint breakdown.
The verify command will fold this kind of live fidelity scoring into the CLI. Until then, API_PARITY.md and the /verify-api workflow are how fidelity is tracked.

Inspect a running clone

asymmetric query hubspot-a1b2 "select object_type, count(*) from crm_objects group by 1"
asymmetric db hubspot-a1b2          # prints a psql shell command
asymmetric logs hubspot-a1b2 -f     # stream the backend
The schema (crm_objects, property_defs, property_groups, pipelines, pipeline_stages, associations, owners, …) comes straight from 001_initial_schema.sql — read it to know what’s queryable.