> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getasym.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# HubSpot template

> A real HubSpot CRM backend — generic objects, v4 associations, properties, pipelines, owners, OAuth tokens, and HubSpot-accurate rate limiting.

The HubSpot template (`apps/hubspot-clone`, name `hubspot`) is an **API-only**
clone of the [HubSpot CRM API](https://developers.hubspot.com/docs/api/crm/understanding-the-crm):
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).

```bash theme={null}
asymmetric spin hubspot
```

## What's inside

| Piece                  | Notes                                                                                                                |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `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

| Mode            | Services  | Use it for                                      |
| --------------- | --------- | ----------------------------------------------- |
| `api` (default) | `backend` | Agents 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:

```bash theme={null}
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`):

```json theme={null}
{
  "status": "error",
  "message": "You have reached your ten-secondly rolling limit.",
  "category": "RATE_LIMIT",
  "policyName": "TEN_SECONDLY_ROLLING"
}
```

## Seeding

```bash theme={null}
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](/concepts/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:

```bash theme={null}
# 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**:

| Status                       | Count |
| ---------------------------- | ----- |
| ✅ exact (path + shape align) | 50    |
| ⚠️ shape-mismatch            | 0     |
| ❌ missing                    | 5     |

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.

<Info>
  The [`verify`](/cli/login#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.
</Info>

## Inspect a running clone

```bash theme={null}
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.
