# License pre-checks Two license gates guard steps that fail with cryptic errors when the org lacks the right UserLicense. Both are derived from the **self-reg profile** — there is no separate license config, so the requirement can't drift from the profile the flow actually assigns. Both only apply **when self-registration is configured**. Source of truth: reference `org-setup.mjs` `checkDeployLicense` (956-992), `readProfileUserLicense` (938-944), `checkSelfRegLicense` (895-926), and `evaluateLicenseRows` (`org-setup-utils.mjs` ~224+). ## Gate 1 — deploy license (pre-deploy, HARD block) Runs **before** `sf project deploy start` (lines 1478-1483). The profile isn't in the org yet pre-deploy, so querying `Profile` would only report "profile missing" — instead, read the required license NAME straight from the local profile source: `/main/default/profiles/.profile-meta.xml` → `...`. - If the profile file or `` element is absent → nothing to gate on; let deploy proceed (line 960). - Otherwise query the org: ```sql SELECT LicenseDefinitionKey, Name, Status, TotalLicenses, UsedLicenses FROM UserLicense WHERE Name = '' ``` - Zero rows → **block the deploy** with a message NAMING the missing license (`required license "" is not present in the org — add it before deploying`). Query/parse failure → block. Otherwise run the seat/status check. This is a hard block (a `StepError`) because deploying without the license fails anyway, but with an unhelpful error. ## Gate 2 — self-reg license (pre-selfReg, SOFT skip) Runs **before** the self-reg step (lines 1598-1601). Query the license via the profile: ```sql SELECT UserLicense.LicenseDefinitionKey, UserLicense.Name, UserLicense.Status, UserLicense.TotalLicenses, UserLicense.UsedLicenses FROM Profile WHERE Name = '' ``` A query failure, zero rows, inactive license, or no free seats is a **soft skip**: warn with the specific reason and record the step skipped. Self-registration being unavailable is not a setup failure. ## Seat / status math (`evaluateLicenseRows`) Done client-side because SOQL can't compare two fields. The license is **satisfied** only when it is **Active** and either `UsedLicenses < TotalLicenses` or `TotalLicenses === -1` (the documented "unlimited" sentinel — treat it as always having free seats). Report the specific reason (missing / inactive / no seats) so the user knows exactly what to fix. ## SOQL-safety of the profile name - In Gate 2 the profile name is **validate-and-fail**: a `'`, `\`, or control char throws a config error rather than being escaped (developer config, not user input). - In Gate 1 the license NAME read from the profile XML is **escaped** for the SOQL literal (`escapeSoqlString`).