kalup

kalup.config.ts

Every field defineConfig accepts, its default, and what uses it in this version

kalup.config.ts names the project, sets the pull scope and declares the targets. It holds one export default defineConfig({ ... }) and nothing else. The CLI parses it and never runs it; the defineConfig import from @kalup/core exists so your editor type-checks the file and shows each field's docs and default on hover. A file that still imports from kalup loads the same. kalup fmt rewrites the import, as does a command that rewrites the file, such as kalup target rebind or a kalup pull that changes an override. A plain kalup pull leaves it alone.

A field the reader does not know is E_NOT_DATA with a list of the fields it does know. A setting at a level that does not take it, such as allowDestroy under objects, is E_SETTING_LEVEL, whose fix lists the levels that do. A value a setting does not take is E_SETTING_VALUE, with the nearest allowed value. The file follows the same grammar as the object files: Config files.

A complete example

import { defineConfig } from '@kalup/core'

export default defineConfig({
  name: 'acme-crm',
  prefix: 'acme_',
  defaultTarget: 'sandbox',
  objects: {
    companies: { include: ['name', 'domain'], exclude: ['zi_*'] },
    deals: { custom: false, include: ['dealname', 'amount'] },
    subscription: {},
  },
  targets: {
    sandbox: {
      portalId: 1111111,
      allowDestroy: true,
      yesLimit: 50,
      credentials: { read: { env: 'HUBSPOT_SANDBOX_KEY' } },
    },
    production: {
      portalId: 2222222,
      protected: true,
      adopt: 'overwrite',
      credentials: { read: { env: 'HUBSPOT_PROD_READ_KEY' }, write: { env: 'HUBSPOT_PROD_WRITE_KEY' } },
      overrides: {
        'property:subscription/status': { name: 'subscriptionstatus' },
      },
    },
  },
})
  • name is the project name used in the IR and the data dictionary. It defaults to the name in the nearest package.json up to the repository root, else the directory name.
  • prefix is optional. validate warns when a managed property's internal name lacks it.
  • defaultTarget is the target pull, plan and snapshot use when you pass no --target.
  • objects is the pull scope: which objects, and which of their properties, the files hold. exclude keeps an integration's zi_ properties out.
  • targets lists the portals this project works with. A target is a named portal, never an environment. Here the sandbox allows deletes and a larger --yes; production takes config's side of values it never agreed on, and writes with a key only CI holds.

A comment inside defineConfig is E_NOT_DATA. Only a comment block above the imports is allowed in this file.

init writes a smaller version of this file with one target and no defaultTarget. See kalup init.

Top level

FieldTypeDefaultWhat it does
namestringthe nearest package.json name up to the repository root, else the project directory's nameThe project name. It appears as project in the IR and heads the data dictionary
dirstring'hubspot'The folder of object files, relative to kalup.config.ts, such as 'lib/config/hubspot'. It holds objects/, index.ts, removed.ts and the blueprints lock. A path outside the project is E_SETTING_VALUE
state'local' or 'repo''local'Where state lives. See state
prefixstringnoneWhen set, validate warns (W_PREFIX) for every managed property whose internal name does not start with it. A warning never changes the exit code
defaultTargetstringnoneThe target pull, plan and snapshot use without --target. It must name a declared target (E_DEFAULT_TARGET). It never enters the IR
mode'addon' or 'takeover''addon'The mode of every object, unless an object or a target states its own. See mode
objectsmap of object name to scope{}The pull scope, below
targetsmap of target name to target{}The portals, below

objects: the pull scope

Each key is an object name. A standard object uses its HubSpot name (companies, contacts, deals, tickets, line_items and the others HubSpot lists properties for by name). Any other key is read as a custom object's name, such as subscription.

FieldTypeDefaultWhat it does
custombooleantruePull every property HubSpot did not define
includestring[][]Properties to pull as well, by internal name: HubSpot-defined ones, or custom ones while custom is off. The files' own properties need no entry
excludestring[][]Internal names of properties and groups to leave out: never pulled into the files, never archived by takeover. * matches any run of characters, as in --only. A name both lists hold is E_SETTING_VALUE; include wins over a pattern
mode'addon' or 'takeover'the top-level modeThis object's mode. A target's statement wins over it
asstringPascalCase singular of the keyThe export name pull gives the object when it writes the object for the first time. companies becomes Company, line_items becomes LineItem by default. An export that already exists keeps its name: rename it in the file

Every property an object file defines is in scope, whatever custom, include and exclude say. Any other portal property is in scope when include names it, or when it is custom, custom is on and exclude does not name it. So custom: false means: pull no custom property the files do not define already. A file property the portal lacks is reported missing in the portal by pull, and plan creates it. custom, include and exclude are the only way to select properties, and never differ per target: under takeover the scope is also what takeover may archive. Without a scope, contacts alone hold hundreds of properties, so pull warns when it writes more than 200 properties into a new object file. A HubSpot property outside the scope and the files raises no warning, even one Kalup does not write.

To see what the portal has outside the scope without writing anything, run kalup pull --discover. See kalup pull.

targets

A target is a named portal pinned to a portal ID. Every command that reads a target checks the key's portal against the pin first, and stops before reading anything when they differ.

FieldTypeDefaultWhat it does
portalIdpositive integernone: a pending targetThe Hub ID of the portal. The wrong-portal guard. Not a positive integer is E_PORTAL_ID. Without it the target is pending (W_PENDING_TARGET): offline commands work, and every command that would read or write its portal refuses it (E_PENDING_TARGET)
credentials.read.envstringHUBSPOT_SERVICE_KEYThe environment variable that holds the read key. credentials needs read when present
credentials.write.envstringthe read keyThe environment variable that holds the key apply, state rebuild --write and target rebind use for every request of a run, so it needs the read scopes too. --approve requires it, read from the process environment only
protectedbooleanfalse for a DEVELOPER_TEST, SANDBOX or APP_DEVELOPER account, else true, an unknown account type includedMarks a portal where an apply needs a person at a terminal, with a saved plan or one apply makes in the same run, or a reviewed CI job with --approve and a write key only that job holds. --yes never covers it. init does not set it. status reports it, and plan records it in the plan and its approval digest
drift'hold' or 'overwrite''hold'How a plan treats edits made in the HubSpot UI. 'hold' holds drift and conflicts for a person to settle; 'overwrite' writes config over them. It needs a base in state: a unit with no base follows adopt. plan records it in the plan and its approval digest
adopt'hold' or 'overwrite''hold'What a plan does with a unit that has no base in state and differs from the portal, as on a first adoption. 'hold' holds it as diverged; 'overwrite' writes config over it, a risky step labelled overwrites-portal that --yes never covers. For single units, use plan --take config
allowDestroybooleanfalseWhether this portal may lose things: a destroy tombstone, and every takeover removal. Never inherited from the project or an object
yesLimitinteger, 0 to 100025The most writes, adoptions and releases one --yes covers. 0 turns --yes off
mode'addon' or 'takeover'the object's modeThis target's mode for every object. It wins over the top level and objects.<object>.mode
objectsmap of object name to { mode }{}This target's mode for one object, the most specific statement. The key must be under objects (E_SETTING_VALUE)
overridesmap of address to override{}Per-target differences, below

A target may not be named config (E_TARGET_NAME): compare uses that word for the config side. Any other name works, and a name carries no meaning.

pull, plan and snapshot run against one target: the one --target names, else defaultTarget, else the only target. With several targets and neither, a person at a terminal is asked which one; without a terminal the command stops with E_TARGET_REQUIRED and lists the names. Targets and credentials has the full rule.

The key is read from the environment or from .env in the project directory, and it is never printed. Targets and credentials covers keys, scopes and the portal check.

state

Kalup keeps a state file per portal: which resources it created or adopted there, and the values config and the portal last agreed on. With 'local', the default, the file is .kalup/state/portal-<id>.json, gitignored, on this machine only; a teammate's clone or a CI job starts without it and adopts again, or rebuilds it with kalup state rebuild. With 'repo' it is hubspot/state/portal-<id>.json, next to the object files, and you commit it: everyone reads the same ownership and bases. The trade-off is git conflicts. Every apply, and every pull that records a base, changes the file, so two branches that apply to the same portal both change it; keep the higher serial when you merge, and Kalup refuses a stale one (E_STATE_CONFLICT). The file holds no key and no record data. The portal lock and the journal stay in .kalup/ either way, and KALUP_STATE_DIR still wins over both settings.

mode: addon and takeover

mode says what Kalup does with a custom property or group the portal holds and config lacks. 'addon', the default, leaves it alone: Kalup manages only what config names. 'takeover' makes config the whole truth for the object's pull scope on that target.

The most specific statement wins: targets.<target>.objects.<object>.mode, then targets.<target>.mode, then objects.<object>.mode, then the top-level mode, then 'addon'. A target's mode that differs from an object's mode, where the target says nothing more about that object, gets W_MODE_SHADOWED. plan records the objects in takeover in target.takeover.

export default defineConfig({
  mode: 'takeover',
  objects: {
    companies: { include: ['name'], exclude: ['zi_*'] },
    contacts: { mode: 'addon' },
  },
  targets: {
    sandbox: { portalId: 1111111, allowDestroy: true, objects: { contacts: { mode: 'takeover' } } },
    production: { portalId: 2222222, mode: 'addon', protected: true },
  },
})

Here companies are taken over on the sandbox, except zi_* properties an integration creates; contacts stay addon, where marketing tools create properties, except on the sandbox; production stays addon until the client signs off. Under takeover, on a target:

  • plan archives every custom property and group in the object's pull scope that config lacks and hubspot/removed.ts does not name. A group goes only when every property HubSpot holds in it goes too, and after them.
  • It never archives a HubSpot-defined property or one HubSpot calculates (a rollup), a kind Kalup does not write (an externalOptions property that is not an owner select or radio), anything an object file lists (references and .managed(false) included), a name exclude covers, a resource a skip override covers or a property in a skipped group, a property a custom object schema names, or an empty group. Apply checks these again against its own read.
  • lifecycle.options defaults to 'exact', so options only the portal holds are removed. A lifecycle.options stated on the property, or in the target's override, wins: 'additive' keeps admin-added options.
  • Every takeover removal, an option included, is destructive: it needs allowDestroy: true on the target and a person at a terminal who types the number of destructive steps. --yes and --approve never cover one. Without allowDestroy the step is blocked, and its fix leads with the kalup pull --target <t> --only <address> that keeps the resource in config, then the exclude entry (or lifecycle: { options: 'additive' }) that leaves it unmanaged, then allowDestroy.
  • A read that was not complete (a 403, a missing scope) blocks every takeover removal on the target.
  • HubSpot refuses to archive a property something still uses, with a 400; apply reports HubSpot's answer.

overrides

An override lets one target differ from the files for one resource. The key is an address that exists in config; anything else is E_UNKNOWN_OVERRIDE.

FieldTypeUsed in this versionWhat it does
namestringyes, by every command that reads a portalThe internal name this resource has on this target. 'property:subscription/status': { name: 'subscriptionstatus' } makes pull read subscriptionstatus on that target and write it under status
skiptrueyes, by every command that reads a portalLeave the resource out on this target. A skipped group takes its config properties with it
definitionpart of a property or group definitionyes, by validate, plan, apply, compare, pull and docsDefinition fields that differ on this target, below
lookupmap of string to stringparsed and validated. plan blocks the resource and compare reports it unknown, since this version manages no lookup resourcesValues that differ on this target

Any other override field is E_NOT_DATA.

definition

A definition override states only what differs on this target. Each field it states replaces the shared field whole; the fields it leaves out stay shared.

overrides: {
  'property:subscription/status': {
    definition: {
      label: 'Contract status',
      description: '',
      options: [
        { value: 'active', label: 'Active' },
        { value: 'paused', label: 'On hold' },
      ],
    },
  },
},
  • A property may override label, description, group, fieldType, formField, options, hidden, displayOrder, numberDisplayHint, showCurrencySymbol, currencyPropertyName, textDisplayHint and calculationFormula, and in lifecycle each of options, removedOptions and ignoreChanges. A group may override label. Anything else, hasUniqueValue, dataSensitivity and type included, is E_OVERRIDE_DEFINITION, as is an override on a reference, a .managed(false) property or a custom object.
  • options is a whole list: the override's options, in its order, are the target's options. There is no merge by value.
  • A field the override states is owned on that target, even an empty value: description: '' owns an empty description there, and options: [] an empty option list.
  • An option carries value, label, hidden and description. Aliases (as) belong to the app and stay in the shared file. An option value the shared options lack is allowed. On a .strict() enum it gets W_OVERRIDE_OPTION, because the app's codec throws on it; a lenient enum reads it as Unlisted.
  • The result must pass the rules a shared definition passes: a fieldType the builder takes, a group of the same object, unique option values, removedOptions naming no kept option.
  • plan, apply, compare with that target and docs use the result. kalup ir prints the shared definitions and the overrides as written.
  • pull --target <name> writes the portal value of an overridden field into the override here, never into the object file, and leaves other targets' overrides alone. A field only the override's lifecycle.ignoreChanges names is left to that portal, so pull keeps the object file's value; see pull.

What enters the IR

Credentials never enter the IR, and neither does defaultTarget. kalup ir shows each target's portalId, protected, drift, adopt, allowDestroy, yesLimit and overrides; mode resolves with the pull scope, and plan records the result in target.takeover.

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.

On this page