Targets and credentials
Named portals, the portal check, protected targets, where keys come from, and per-target overrides
A target is a named portal. Every command that talks to HubSpot runs against one target, and every target is pinned to one portal ID, so a key for the wrong portal is caught before anything is read.
Declaring targets
Targets live in kalup.config.ts:
import { defineConfig } from 'kalup'
export default defineConfig({
objects: {
companies: { include: ['name', 'domain'] },
subscription: {},
},
targets: {
sandbox: {
portalId: 1111111,
credentials: { read: { env: 'HUBSPOT_SANDBOX_KEY' } },
},
production: {
portalId: 2222222,
protected: true,
credentials: { read: { env: 'HUBSPOT_PROD_READ_KEY' } },
},
},
})Run a command against one of them with --target:
npx kalup pull --target sandboxName targets after what they are to you: sandbox, production, staging, or a client's name. kalup init names the first one sandbox for a sandbox or developer test account and production for a standard account. The one reserved name is config, which validate rejects with E_TARGET_NAME.
The portal check
portalId is the portal's Hub ID, the number in the HubSpot account menu. It must be a positive integer, and validate reports a missing or malformed one as E_PORTAL_ID.
Before a command reads anything else, it asks HubSpot which portal the key belongs to. When the answer is not the pinned portalId, the command stops with E_TARGET_PORTAL_MISMATCH and exit 4, and writes nothing:
E_TARGET_PORTAL_MISMATCH: The key in HUBSPOT_SANDBOX_KEY belongs to portal 2222222, not portal 1111111 pinned for target sandbox. (fix: The key in HUBSPOT_SANDBOX_KEY belongs to portal 2222222. Ask the user to check the key and the pinned portalId for target sandbox.)Exit 4 means a person has to act. Either the key is the wrong one, or the pin is. Kalup never changes the pin by itself, and an agent should not either. See Working with AI agents.
kalup init runs the same check against --portal before it writes any file.
Protected targets
protected: true marks a target that needs extra care, usually production. In this version nothing writes to a portal, so the flag changes no behaviour yet. kalup status shows it for every target.
When the config does not say, status treats a standard (production) account as protected and a sandbox or developer test account as not protected, and marks that value as the default. kalup init writes protected: true for a standard account.
Coming with apply: a protected target will accept only saved plans, and a person at a real terminal will have to type the target name before a risky step runs.
Where the key comes from
Each target names the environment variable that holds its read key:
credentials: { read: { env: 'HUBSPOT_SANDBOX_KEY' } }A target without credentials reads HUBSPOT_SERVICE_KEY.
Kalup looks for the variable in two places, in this order:
- The process environment.
- A
.envfile in the project directory, the directory that holdskalup.config.ts.
.env takes KEY=value lines. Blank lines, # comments, an export prefix and single or double quotes are all fine. Kalup reads only the variable a target names. It does not load the rest of the file into the environment.
# .env, never committed
HUBSPOT_SANDBOX_KEY=...
HUBSPOT_PROD_READ_KEY=...When the variable is set in neither place, the command stops with E_MISSING_KEY. The message names the variable and never contains a value. Kalup never prints, logs or writes a key, not even in an error.
Keep .env out of git. Keys never enter the IR, the object files or any output.
Which key to use
Use a HubSpot service key with the read scopes for the objects in scope. kalup init prints the exact scopes the key needs, one per object, and kalup status checks each one. A key can only hold scopes its creator has, so a Super Admin should create it.
HubSpot is switching off the creation of new legacy private apps in autumn 2026, and service keys replace them, so Kalup is built around service keys.
credentials.write is accepted in the config and ignored in this version, which never writes. A write key for apply comes later.
Per-target overrides
A resource can have a different name in one portal than in your files. overrides maps an address to what differs in that target:
production: {
portalId: 2222222,
overrides: {
'property:companies/billing_status': { name: 'billing_state' },
},
},In this version:
nameworks inpull. Kalup readsbilling_statefrom the production portal and treats it asproperty:companies/billing_status. When the portal holds both names, the pull stops withE_OVERRIDE_AMBIGUOUS, because the override would hide a real property.skip,definitionandlookupare validated but not applied yet.- Every override key must be an address in your files. An unknown one is
E_UNKNOWN_OVERRIDE.npx kalup irlists the valid addresses.
drift
drift: 'hold' | 'overwrite' is accepted and carried into the IR. It changes nothing in this version. When plan arrives, hold will be the default everywhere, and overwrite will be for a personal sandbox where the files should always win.
Check a target
npx kalup status --target sandboxstatus reports, per target: whether the key is set, whether the portal matches the pin, the account type, the time zone, whether it is protected, and whether each read scope is present. It exits 0 when everything passes, 1 when a target has a problem, and 4 on a portal mismatch. See kalup status.