use-case:developers

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 CI makes sure a property exists before the code that reads it goes live.

the cast, and the sheet it came from
{{{}}}renewalDate: string | nullKALUPFIG. 4 · THE CASTSCALE 1:1 · SHEET 4 OF 4
flow:daily

A property, from branch to production.

The same review as any other change: a branch, a diff, a pull request and a CI job.

  1. 01

    Add the property on a branch

    Add one line to kalup/objects/companies.ts. The app code that uses CompanyData.renewalDate type-checks at once. No generate step.
  2. 02

    Plan and apply to your sandbox

    $ kalup plan --target sandbox
    The plan shows one safe create. Apply it to the sandbox and test against a real portal.
  3. 03

    Open a pull request

    $ kalup plan --target production --json
    CI plans against production and posts the plan as a comment, so reviewers see the portal change next to the code change.
  4. 04

    Merge, then CI applies

    $ kalup apply plan.json
    A CI job that holds the production write key plans again, saves the plan and applies it, then deploys the app. The property exists before the code that needs it runs.
  5. 05

    Roll back with a revert

    There is no rollback command. Revert the commit and plan again. Recovery is always a new plan.
ci:recipe

The plan rides along with the pull request.

State lives on a kalup-state branch, checked out as a worktree. A rejected push means another apply ran at the same time.

kalup/objects/companies.ts
// kalup/objects/companies.ts
renewalDate: p.date('renewal_date', { label: 'Renewal date', group: 'billing', fieldType: 'date' }),
+
Create company property "Renewal date"
property:companies/renewal_date · public-api
safe
label"Renewal date"
typedate · fieldType date
groupgroup:companies/billing
expect: does not exist yet · re-checked right before the write
github actions · pull request #42
$ npx kalup plan --target production
Reading portal 2222222 (production) ... done

Plan for production: 1 step · 1 safe

  + Create company property "Renewal date"   safe
    property:companies/renewal_date

Nothing was written. Review, then apply.
$ 
.github/workflows/kalup.yml
# .github/workflows/kalup.yml, after merge to main
- run: git fetch origin kalup-state:kalup-state && git worktree add .kalup/state kalup-state
- run: npx kalup plan --target production --out plan.json
- run: npx kalup apply plan.json
- if: always()
  run: cd .kalup/state && git add -A && (git diff --cached --quiet || (git commit -m "apply $GITHUB_SHA" && git push origin kalup-state))

A typed client on the same files

@kalup/client reads, writes and searches records with the types from your object files. Aliases, required fields and read-only properties carry through, so 'PAST DUE' in the portal is 'past_due' in your code.

app/billing.ts
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
}
scope:out

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. The typed client reads and writes records for your app; it is not a migration engine.

  • 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.

    Recovery is a revert and a new plan. A partial apply exits 5, and you plan again from where the portal is.

kalup:init

Start with one portal.

kalup init pins a target, writes the files and runs the first pull. Nothing is written to the portal.