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' },
},
},
},
})nameis the project name used in the IR and the data dictionary. It defaults to the name in the nearestpackage.jsonup to the repository root, else the directory name.prefixis optional.validatewarns when a managed property's internal name lacks it.defaultTargetis the targetpull,planandsnapshotuse when you pass no--target.objectsis the pull scope: which objects, and which of their properties, the files hold.excludekeeps an integration'szi_properties out.targetslists 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
| Field | Type | Default | What it does |
|---|---|---|---|
name | string | the nearest package.json name up to the repository root, else the project directory's name | The project name. It appears as project in the IR and heads the data dictionary |
dir | string | '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 |
prefix | string | none | When set, validate warns (W_PREFIX) for every managed property whose internal name does not start with it. A warning never changes the exit code |
defaultTarget | string | none | The 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 |
objects | map of object name to scope | {} | The pull scope, below |
targets | map 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.
| Field | Type | Default | What it does |
|---|---|---|---|
custom | boolean | true | Pull every property HubSpot did not define |
include | string[] | [] | 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 |
exclude | string[] | [] | 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 mode | This object's mode. A target's statement wins over it |
as | string | PascalCase singular of the key | The 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.
| Field | Type | Default | What it does |
|---|---|---|---|
portalId | positive integer | none: a pending target | The 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.env | string | HUBSPOT_SERVICE_KEY | The environment variable that holds the read key. credentials needs read when present |
credentials.write.env | string | the read key | The 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 |
protected | boolean | false for a DEVELOPER_TEST, SANDBOX or APP_DEVELOPER account, else true, an unknown account type included | Marks 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 |
allowDestroy | boolean | false | Whether this portal may lose things: a destroy tombstone, and every takeover removal. Never inherited from the project or an object |
yesLimit | integer, 0 to 1000 | 25 | The most writes, adoptions and releases one --yes covers. 0 turns --yes off |
mode | 'addon' or 'takeover' | the object's mode | This target's mode for every object. It wins over the top level and objects.<object>.mode |
objects | map 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) |
overrides | map 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:
planarchives every custom property and group in the object's pull scope that config lacks andhubspot/removed.tsdoes 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
externalOptionsproperty that is not an owner select or radio), anything an object file lists (references and.managed(false)included), a nameexcludecovers, a resource askipoverride 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.optionsdefaults to'exact', so options only the portal holds are removed. Alifecycle.optionsstated 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: trueon the target and a person at a terminal who types the number of destructive steps.--yesand--approvenever cover one. WithoutallowDestroythe step is blocked, and its fix leads with thekalup pull --target <t> --only <address>that keeps the resource in config, then theexcludeentry (orlifecycle: { options: 'additive' }) that leaves it unmanaged, thenallowDestroy. - 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;
applyreports 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.
| Field | Type | Used in this version | What it does |
|---|---|---|---|
name | string | yes, by every command that reads a portal | The 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 |
skip | true | yes, by every command that reads a portal | Leave the resource out on this target. A skipped group takes its config properties with it |
definition | part of a property or group definition | yes, by validate, plan, apply, compare, pull and docs | Definition fields that differ on this target, below |
lookup | map of string to string | parsed and validated. plan blocks the resource and compare reports it unknown, since this version manages no lookup resources | Values 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,textDisplayHintandcalculationFormula, and inlifecycleeach ofoptions,removedOptionsandignoreChanges. A group may overridelabel. Anything else,hasUniqueValue,dataSensitivityandtypeincluded, isE_OVERRIDE_DEFINITION, as is an override on a reference, a.managed(false)property or a custom object. optionsis 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, andoptions: []an empty option list. - An option carries
value,label,hiddenanddescription. 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 getsW_OVERRIDE_OPTION, because the app's codec throws on it; a lenient enum reads it asUnlisted. - The result must pass the rules a shared definition passes: a
fieldTypethe builder takes, agroupof the same object, unique option values,removedOptionsnaming no kept option. plan,apply,comparewith that target anddocsuse the result.kalup irprints 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'slifecycle.ignoreChangesnames is left to that portal, sopullkeeps the object file's value; seepull.
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.