Location templates
A location template is a provider-scoped, reusable bundle of connectors. It captures the set of connectors you would normally stand up at a new location so a caller can replay
it later instead of re-typing the same POST /connectors payloads each time.
It names those connectors with connectorTemplateIds — a list of references to reusable connector templates, each of which materializes one
connector when the location template is applied. The same connector template may be referenced more than once, which is how you express an HA pair from a single blueprint.
:::warning Inline connectors is deprecated
A location template may also carry connectors, an inline list of CreateConnector blueprints stored on the template itself. It predates connector templates, remains fully
supported, and applies after the referenced connector templates — but new templates should use connectorTemplateIds. A blueprint written inline can only ever be used by the one
location template holding it; the same blueprint as a connector template is referenceable, editable in one place, and reportable through the usage endpoints.
:::
What a location template is (and isn't)
- Its references are checked, its inline blueprints are not. Each id in
connectorTemplateIdsmust resolve to a live connector template belonging to the same provider, and is rejected at save time if it doesn't. The deprecated inlineconnectorslist, by contrast, is held verbatim as a list ofCreateConnectorpayloads and is not validated against the live data model until the caller actually creates a connector from it — referenced models, types, and addresses are all checked at apply. - It is not an active resource. Saving a template doesn't create connectors. Materialization happens on demand via
POST /location-templates/{templateId}/apply, which creates a location and its connectors atomically (the server injects each connector'slocationIdfrom the new location). The pairedGET /…/applyreturns a preview payload with a_placeholderslist describing the fields the caller is expected to fill in. See the API reference for details. - It is scoped to a provider. The
providerIdis set on create and immutable. Authorization gates on the parent provider. Anyone withupdateon the provider can manage its templates. When applying, callers can omitcustomerIdto land the new location directly under the template's provider; otherwise the provided customer must belong to the same provider scope the caller has access to.
Fields
| Field | Description | Example |
|---|---|---|
id | Unique ID assigned at create time | 7e1d2c3b-4a5f-4678-9012-3456789abcde |
providerId | Parent provider — set on create, immutable | 8b14d4f9-3a52-4b91-8e7c-d3e44b5c1f2a |
name | Human-readable name, unique within the provider (1–255 chars). Identifies the template itself; it isn't the created location's name | Standard branch site |
locationName | Default for the created location's name — seeds the apply body's name, which the caller can override. Required (1–255 chars). Supports {{variable}} substitution | {{customer.name}} — branch |
locationDescription | Default for the created location's description — seeds the apply body's description. Optional (≤1000 chars). Supports {{variable}} substitution | Managed by {{provider.name}} |
connectorTemplateIds | References to connector templates. One connector is materialized per entry, in order; the same id may appear more than once (an HA pair) | ["3f2a1b0c-…", "3f2a1b0c-…"] |
connectors (deprecated) | Inline list of CreateConnector blueprints. Stored verbatim; the nested locationId is set at materialization time, and templated fields support {{variable}} substitution | |
ownerIdentityId | Identity that owns the template | c0ffee00-1111-2222-3333-444455556666 |
createdBy, createdAt, updatedAt, deletedAt, deletedBy | Standard lifecycle fields |
Applying a template
Materializing a template into real resources is a two-step interaction against a single endpoint pair.
The split pays off when the caller is a UI: the _placeholders payload is enough to generate a form (labels, input
types, required-flags, indexed connector rows) without the UI needing to know anything about the underlying schema.
CLIs benefit too. connect-cli apply <templateId> can render the preview, prompt the operator for each placeholder, and
submit the filled body, all without hard-coding which fields exist.
See the API reference for request/response examples and the full path table.
The preview (GET)
GET /location-templates/{templateId}/apply returns a payload shaped exactly like the POST body, pre-filled from the
template: name/description are seeded from the template's locationName/locationDescription, and the blueprint
connectors are echoed back (all still containing any {{variable}} placeholders; see Variable
substitution). Alongside the payload, a server-generated _placeholders list tells the
caller exactly which fields it is expected to fill in. That's enough to render a form or drive a CLI prompt without
the caller knowing the underlying schema. The list is READ_ONLY for Jackson, so a caller can echo the entire
response straight back into the POST without stripping it.
Alongside them, connectorTemplates carries one nested entry per referenced connector template, in connectorTemplateIds order, each pre-filled from
that template's connectorName/connectorDescription and each with its own _placeholders ($.name, $.description — no $.locationId, which the server injects from the
location being created).
The caller may keep the seeded name/description or override them; any {{variable}} tokens they contain are
rendered at apply time. The connectors' own fields are provider-authored and rendered at apply time, so they are
echoed but not enumerated as placeholders. The preview also strips fields that don't belong in the caller's
submission: id, providerId, ownerIdentityId, and the createdBy/createdAt/updatedAt/deletedAt/deletedBy
lifecycle fields. The provider in particular is always taken from the template, never from the caller.
The _placeholders list
Each entry is a small descriptor with four fields:
| Field | Meaning | Type | Required |
|---|---|---|---|
$.customerId | Owning customer; omit to land the location directly under the provider | UUID | no |
$.name | The new location's name (seeded from locationName) | string | yes |
$.description | The new location's description (seeded from locationDescription) | string | no |
$.address | Street address | string | no |
$.longitude | Longitude — must be paired with $.latitude | number | no |
$.latitude | Latitude — must be paired with $.longitude | number | no |
$.virtual | Virtual location — see Cloud placement | boolean | no |
$.cloudProvider | Cloud provider; only allowed when $.virtual is true | string | no |
$.cloudRegion | Cloud region; only allowed when $.virtual is true | string | no |
The connector fields carry no placeholders: they are provider-authored and rendered from the variable catalog at
apply time, not filled in by the caller. When this preview is embedded inside a customer template
apply, the $.customerId placeholder is also omitted. The server
injects the new customer's id, so the caller must not set it.
The apply (POST)
The POST body is a CreateLocationFromTemplate — the caller-supplied location fields (name, description,
customerId, address, longitude, latitude, virtual, cloudProvider, cloudRegion) plus a
connectors: List<CreateConnector>. It carries no providerId:
nameanddescriptionare seeded from the template, then rendered. The preview pre-fills them from the template'slocationName/locationDescription. The caller may keep or override them; any{{variable}}tokens they contain are rendered at apply time to become the new location's name/description. The rendered name is validated for length and per-customer uniqueness. A duplicate, or a rendered name longer than 255 chars, fails the apply.customerIdis optional. When provided, the new location lands under that customer (which must belong to a provider the caller has scope on). When omitted, the server resolves the template's provider's internal customer and uses that, so the location is created directly under the template's provider.virtual,cloudProvider, andcloudRegionset the location's cloud placement, decided fresh on every apply. See Cloud placement below.connectorTemplatesmust line up with the template'sconnectorTemplateIds. One entry per reference, in the same order; a mismatched count is rejected before anything is created. Positional matching is deliberate — the same connector template may be referenced twice to express an HA pair, which a keyed map could not represent. Each entry supplies onlyname/description; the type, model, overrides and applications come from the connector template.connectors[*].locationIdandconnectorTemplates[*].locationIdare server-set. Anything the caller sends for either is overwritten with the new Location's id before validation.- The whole call is one transaction. The location is inserted, then each connector is bound, validated, and
created in order. Any failure rolls the whole thing back: an unresolved
{{variable}}, bean validation on a connector, a missing connector model, an authorization mismatch, or a Ziti-side error. Ziti compensations registered against the connector creates fire on rollback so the network state stays consistent with the database.
Cloud placement
virtual, cloudProvider, and cloudRegion are chosen per apply call, exactly like address/longitude/latitude —
the location template itself stores none of them. Applying the same template twice can produce one physical location
and one virtual location in a different cloud region each time, without changing the template.
virtual defaults to false. The same rule that governs a location's fields directly applies here:
cloudProvider and cloudRegion are only allowed when virtual is true (and if either is set, both must be);
address is only allowed when virtual is false. An inconsistent combination fails the apply.
Authorization
The caller needs three things in combination:
- Read on the location template: Gated through its parent provider.
- Create on locations under the resolved customer (or, when
customerIdis omitted, under the template's provider's internal customer). This is verified up front, before any resource is touched. - Create on connectors at the new location's path. This is verified per connector during validation, after the
server has bound
locationIdto the new location. A caller who has location-create but not connector-create will get past the location step and then see the whole apply roll back when the first connector fails the check.
Variable substitution
These apply payload fields support {{variable}} placeholders and generator tokens. Most resolve at apply time; the connector.* namespace on the applicationOverrides
fields is a deferred projection, left intact at apply and resolved only when each blueprint connector materializes its model — the connector does not exist yet while the
blueprint renders. The location.* namespace is in scope only for the connector fields, because the location doesn't exist yet while its own name/description render (the
two phases are explained below):
| Property | In-scope variable namespaces |
|---|---|
$.name (the location's name, seeded from locationName) | provider.*, customer.* |
$.description (the location's description, seeded from locationDescription) | provider.*, customer.* |
$.connectorTemplates[].name | provider.*, customer.*, location.* |
$.connectorTemplates[].description | provider.*, customer.*, location.* |
$.connectors[].name | provider.*, customer.*, location.* |
$.connectors[].description | provider.*, customer.*, location.* |
$.connectors[].applicationOverrides[].appName | provider.*, customer.*, location.*, connector.* |
$.connectors[].applicationOverrides[].addresses[].listenAddress | provider.*, customer.*, location.*, connector.* |
$.connectors[].applicationOverrides[].addresses[].listenPort | provider.*, customer.*, location.*, connector.* |
$.connectors[].applicationOverrides[].addresses[].targetAddress | provider.*, customer.*, location.*, connector.* |
$.connectors[].applicationOverrides[].addresses[].targetPort | provider.*, customer.*, location.*, connector.* |
$.connectors[].applicationOverrides[].addresses[].allowedAddresses[] | provider.*, customer.*, location.*, connector.* |
$.connectors[].applicationOverrides[].addresses[].allowedPorts[] | provider.*, customer.*, location.*, connector.* |
A blueprint connector's own name/description may not reference connector.* — they are never re-rendered after apply, so an unresolved {{connector.*}} there is
rejected as unknown — but its applicationOverrides may, because those are re-rendered when the connector materializes the model. Generator tokens ({{alpha:8}} etc.) are
allowed in any of these fields and produce a value at apply time, seeded per apply. See
Template variables in model applications.
Substitution runs against a catalog drawn from the provider, the (optional) customer, and the location being created.
Each namespace contributes its fixed "standard" keys plus that resource's user-defined
custom variables under a .custom. sub-space (e.g. provider.custom.edgeDomain):
| Namespace | Variables | Available to |
|---|---|---|
provider.* | provider.id, provider.name, provider.networkId, provider.networkGroupId, provider.organizationId, provider.custom.* | everything |
customer.* | customer.id, customer.name, customer.custom.* | only when customerId is supplied on apply |
location.* | location.id, location.name, location.description, location.address, location.custom.* | connector fields only — the location is created before its connectors render |
Rendering happens in two phases. First, the body's name/description are rendered against provider.* +
customer.* to produce the location's name/description. location.* isn't available here (the location doesn't
exist). Then the connectors are rendered against provider.* + customer.* + location.*, so a connector can be
named after the location it attaches to, e.g. {{location.name}}-gw. Within each connector the applicationOverrides
subtree additionally defers connector.* — those placeholders are carried through unresolved and substituted when the
connector materializes its model, exactly as a directly created connector's overrides are.
Substitution is strict. A placeholder that references a variable not in the catalog fails the whole apply with a
4xx listing the offending variables, rather than emitting a blank or a literal {{…}}. That covers a typo, a
custom variable not defined on the resolving resource, and customer.* when applying directly under a provider with
no customer. The one exception is a deferred connector.* reference inside applicationOverrides, which is
intentionally left intact for materialization rather than treated as unknown. A resolved value is never re-scanned, so
a value that happens to contain braces can't trigger further substitution.
Beyond these standard variables, each namespace also carries a user-defined <namespace>.custom.* sub-space — {{provider.custom.region}}, {{customer.custom.tier}},
{{location.custom.rack}} — available to a field exactly where the standard namespace of the same level is. See Custom variables.
Lifecycle hooks
Customer Connect listens for resource-change events from upstream resources and keeps templates in sync:
- Provider deleted: Every location template under that provider is deleted in turn. The cascade is driven by the
connect-providerDELETEDevent. - Connector model deleted: Every connector entry that referenced the deleted model is stripped from each
template's inline
connectorslist. The template itself is preserved, even if itsconnectorslist ends up empty. The cascade is driven by theconnect-connector-modelDELETEDevent. - Connector template deleted: Its id is stripped from each template's
connectorTemplateIdslist — every occurrence of it, since one template may reference it more than once. The location template itself is preserved, even if the list ends up empty. The cascade is driven by theconnect-connector-templateDELETEDevent. Until the event has been consumed the reference survives; applying in that window fails with a404rather than materializing a connector from a retired blueprint.
These hooks are best-effort housekeeping; they keep stored blueprints from pointing at resources that no longer exist. They don't affect any connectors that were already materialized from the template.
Filtering
The list endpoint accepts the usual id / name / providerId / ownerIdentityId / createdAt / updatedAt / deletedAt /
deleted filters, plus a connectorTemplateId filter matching templates that reference the given connector template, and a connectorModelId filter that matches templates whose
stored inline connectors array contains at least one entry referencing the given model. That filter is implemented as a JSON-aware predicate against the
connectors column, so it scales without materializing the array in the application tier.