Ship the property before the code that needs it.
You have an app, a sandbox and a production portal. Kalup keeps the portal's properties and objects in TypeScript files next to your code. The same files give the app its types, and apply writes a reviewed plan to your sandbox. A CI recipe, documented but not yet run in a real CI, applies production after the merge, so a property exists before the code that reads it goes live.
A property, from branch to production.
The same review as any other change: a branch, a diff, a pull request and a CI job.
- 01
Add the property on a branch
0.2Add one line tokalup/objects/companies.ts. The app code that usesCompanyData.renewalDatetype-checks at once. No generate step. - 02
Plan against your sandbox
0.2$ kalup plan --target sandbox --out plan.jsonThe plan shows one safe create and saves it for review. Nothing is written to the portal. - 03
Apply to your sandbox
0.2$ kalup apply plan.jsonApply writes the saved plan to the sandbox once you type its name, so you can test against a real portal. - 04
Open a pull request
Recipe$ kalup plan --target production --out plan.jsonIn the CI recipe, a job plans against production and posts the plan and its writesHash as a comment, so reviewers see the portal change next to the code change. - 05
Merge, then CI applies
Recipe$ kalup apply plan.json --approve <writesHash>The job that alone holds the production write key plans again and applies with the reviewed hash, then the app deploys. If anything moved since the review, apply stops and writes nothing. - 06
Undo with a revert
0.2There is no rollback command. Revert the commit and plan again: the plan proposes the reverse of what config owns. The property this change created stays, since absence never deletes.
The plan rides along with the pull request.
Recipe The CI recipe, a documented design not yet run in a real CI. State lives on a branch per portal, checked out as a worktree, and one job per portal writes at a time. The plan below is what 0.1.0 prints.
// kalup/objects/companies.ts
renewalDate: p.date('renewal_date', { label: 'Renewal date', group: 'billing', fieldType: 'date' }),$ npx kalup plan --target production Plan pl_cb9ea21f2c5b for target production, portal 2222222 (STANDARD, protected) s1 safe Adopt property group "Billing" (billing) on companies s2 safe Adopt property "Billing status" (billing_status) on companies s3 safe Create property "Renewal date" (renewal_date) on companies 3 safe, 0 risky, 0 destructive, 0 blocked, 0 manual; 0 held Coverage: complete; 0 unsupported, 0 excluded. About 11 API calls; 999991 left today. 1 internal name created here can never be renamed. Not copied, HubSpot has no API: conditional property logic, field-level permissions. $
# The apply job after merge to main, abridged. A design, not yet run in a real CI.
concurrency: { group: kalup-portal-2222222, cancel-in-progress: false }
env: { KALUP_STATE_DIR: .kalup-state/state }
steps:
- run: git fetch origin kalup-state/portal-2222222 && git worktree add -B kalup-state/portal-2222222 .kalup-state FETCH_HEAD
- run: npx kalup plan --target production --out plan.json
- run: npx kalup apply plan.json --approve "$REVIEWED_HASH" # the writesHash posted on the pull request
- if: always()
run: cd .kalup-state && git add state && git commit -m "Apply $GITHUB_SHA" && git push origin HEAD:kalup-state/portal-2222222A typed client on the same files
LaterLater, after the next work on the roadmap: @kalup/client to read, write and search records with the types from your object files. The types and codecs already work without it, so 'PAST DUE' in the portal is 'past_due' in your code today.
import { createClient } from '@kalup/client'
import { Company } from './kalup'
const crm = createClient({ accessToken: process.env.HUBSPOT_TOKEN, objects: { companies: Company } })
const company = await crm.companies.get('1234')
company.billingStatus // 'active' | 'past_due'
for await (const c of crm.companies.searchAll({ where: { billingStatus: 'past_due' } })) {
// every past-due company, past the 10,000-result cap
}What Kalup does not do for you.
Knowing where it stops is part of trusting what it does.
- It does not move records.
Kalup moves configuration, never contacts, companies or deals. A later typed client would read and write records for your app, not migrate them.
- It does not replace the hs CLI.
Apps, CMS themes and serverless functions stay with HubSpot's own CLI. Use hs for the app and Kalup for the portal.
- It has no rollback button.
Reverting config and planning again proposes the reverse of the changes config owns. Absence never deletes, so a created property stays, and so does an added option, since options are additive by default. A partial apply exits 5, and you plan again from where the portal is.
Start with one portal.
kalup init pins a target, writes the files and runs the first pull. Nothing is written to the portal.