Getting started
Install the CLI, pull a portal into files, validate them and commit
This page takes you from an empty directory to a committed set of config files. Everything on it is read-only: this version of Kalup reads a portal and never writes to it.
Before you start
- Node 22 or newer.
- The portal's Hub ID, from the account menu in HubSpot.
- A service key for that portal with the read scopes for the objects you want.
initprints the exact list. A key can only hold scopes its creator has, so a Super Admin should create it.
Install
npm install --save-dev kalup
npm install @kalup/corekalup is the CLI. @kalup/core is the runtime your app imports. It has no dependencies and never touches the network or the file system.
Put the key in .env in the project directory, or in the environment:
HUBSPOT_SERVICE_KEY=...The CLI reads the variable a target names in credentials.read.env, or HUBSPOT_SERVICE_KEY when the target names none. It never prints a key, not even in an error. Keep .env out of git.
Create the project
npx kalup init --portal 1111111init reads the account behind the key first. When that account is not portal 1111111, it stops with exit 4 and writes nothing. Otherwise it writes:
kalup.config.tswith one target. The target is namedsandboxfor a sandbox or developer test account andproductionfor a standard account, which also getsprotected: true.--target <name>picks another name.kalup/index.ts, the barrel your app imports.- The line
.kalup/in.gitignore, created when missing. - A formatter ignore for
kalup/:!kalup/**inbiome.jsonwhen that file exists, else a line in.prettierignorewhen a prettier config exists, else a printed note. The writer's format is the only format for those files. AGENTS.mdwith the rules an AI agent follows in this project, appended when the file exists.CLAUDE.mdwith the one line@AGENTS.md, so Claude Code reads the same rules. It is created when missing and appended when it exists, unless it already points atAGENTS.md.
Then it runs the first pull. --objects contacts,companies,deals is the default scope. A custom object goes in by its name, for example --objects companies,subscription. The output starts with the account type, the UI domain and the time zone, the target and its objects, the read scopes the key needs, one per object, with the link to the service key settings, and the files written. The pull summary follows.
init refuses to run when kalup.config.ts already exists. Run pull instead.
Pull
npx kalup pull --target sandboxpull reads the target and writes one file per object under kalup/objects/. A second pull with no change in the portal writes nothing. Run it whenever someone changed the portal in the HubSpot UI, so the files catch up. Before a file is overwritten, the old one is copied to .kalup/history/.
A pulled company file:
import { defineObject, type InferProperties, p } from '@kalup/core'
export const Company = defineObject('companies', {
groups: {
billing: { label: 'Billing' },
},
properties: {
billingStatus: p.enum('billing_status', {
label: 'Billing status',
group: 'billing',
fieldType: 'select',
options: [
{ value: 'active', label: 'Active' },
{ value: 'PAST DUE', label: 'Past due' },
],
}),
domain: p.string('domain'),
name: p.string('name'),
renewalDate: p.date('renewal_date', {
label: 'Renewal date',
group: 'billing',
fieldType: 'date',
}),
},
})
export type CompanyData = InferProperties<typeof Company.properties> & { id: string }Each property gets a camelCase key, the builder that matches its HubSpot type, and its full definition when the portal lets you own it. HubSpot-defined properties such as name are references: no definition, never created, changed or removed. Config files explains the grammar, Property builders each builder, and kalup pull the scope, the merge rules and the flags.
Validate
npx kalup validatevalidate runs offline. It checks kalup.config.ts and every file under kalup/ and reports each problem with the file, the line and a fix. Exit 0 means valid, 3 means invalid. Every other command runs the same checks first, so an invalid project stops before anything is read or written.
Add --json to any command for one envelope/1 document on stdout and nothing else:
{ "format": "envelope/1", "ok": false, "issues": [{ "code": "E_UNKNOWN_GROUP", "message": "...", "file": "kalup/objects/companies.ts", "line": 12, "fix": "..." }] }That is what a script or an AI agent reads. issues[].fix says what to do.
Use the files in your app
import { Company } from './kalup'
const status = Company.properties.billingStatus.get(record.properties)
// 'active' | 'PAST DUE' | nullThe same file types the property bag, with no generate step. Types and codecs has the details.
What to commit
Commit kalup.config.ts, everything under kalup/, AGENTS.md and CLAUDE.md. The files are the source of truth for the portal, and a change to them is a normal pull request.
Do not commit .kalup/ (history copies, and state in a later version), .env or any other file that holds a key, or the IR. npx kalup ir derives the IR from the files whenever something needs it.
The other commands
| Command | What it does |
|---|---|
kalup ir | Print the ir/1 document derived from the files. --check validates it and prints only issues |
kalup fmt | Rewrite the files in canonical form and regenerate kalup/index.ts. --check lists the files that would change, exit 2 with --exit-code |
kalup status | The config summary, then for each target: key set, portal matches the pin, account type, read scopes present, state |
Next steps
- How Kalup works: targets, the files, the IR and the portal, and how they fit.
- kalup.config.ts: narrow the pull scope and add a second target.
- Property builders: add a property by hand and see its type change in your editor.
- Working with AI agents: let Claude Code or another agent drive the CLI safely.
- Errors: every error code, what causes it and the fix.
Kalup is an independent open-source project maintained by Scopious. It is not affiliated with, endorsed by, or sponsored by HubSpot, Inc. HubSpot is a registered trademark of HubSpot, Inc.