Skip to main content

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 connectorTemplateIds must resolve to a live connector template belonging to the same provider, and is rejected at save time if it doesn't. The deprecated inline connectors list, by contrast, is held verbatim as a list of CreateConnector payloads 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's locationId from the new location). The paired GET /…/apply returns a preview payload with a _placeholders list describing the fields the caller is expected to fill in. See the API reference for details.
  • It is scoped to a provider. The providerId is set on create and immutable. Authorization gates on the parent provider. Anyone with update on the provider can manage its templates. When applying, callers can omit customerId to 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

FieldDescriptionExample
idUnique ID assigned at create time7e1d2c3b-4a5f-4678-9012-3456789abcde
providerIdParent provider — set on create, immutable8b14d4f9-3a52-4b91-8e7c-d3e44b5c1f2a
nameHuman-readable name, unique within the provider (1–255 chars). Identifies the template itself; it isn't the created location's nameStandard branch site
locationNameDefault 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
locationDescriptionDefault for the created location's description — seeds the apply body's description. Optional (≤1000 chars). Supports {{variable}} substitutionManaged by {{provider.name}}
connectorTemplateIdsReferences 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
[
  {
    "name": "edge-gateway",
    "type": "GATEWAY"
  }
]
ownerIdentityIdIdentity that owns the templatec0ffee00-1111-2222-3333-444455556666
createdBy, createdAt, updatedAt, deletedAt, deletedByStandard 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:

FieldMeaningTypeRequired
$.customerIdOwning customer; omit to land the location directly under the providerUUIDno
$.nameThe new location's name (seeded from locationName)stringyes
$.descriptionThe new location's description (seeded from locationDescription)stringno
$.addressStreet addressstringno
$.longitudeLongitude — must be paired with $.latitudenumberno
$.latitudeLatitude — must be paired with $.longitudenumberno
$.virtualVirtual location — see Cloud placementbooleanno
$.cloudProviderCloud provider; only allowed when $.virtual is truestringno
$.cloudRegionCloud region; only allowed when $.virtual is truestringno

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:

  • name and description are seeded from the template, then rendered. The preview pre-fills them from the template's locationName/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.
  • customerId is 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, and cloudRegion set the location's cloud placement, decided fresh on every apply. See Cloud placement below.
  • connectorTemplates must line up with the template's connectorTemplateIds. 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 only name/description; the type, model, overrides and applications come from the connector template.
  • connectors[*].locationId and connectorTemplates[*].locationId are 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:

  1. Read on the location template: Gated through its parent provider.
  2. Create on locations under the resolved customer (or, when customerId is omitted, under the template's provider's internal customer). This is verified up front, before any resource is touched.
  3. Create on connectors at the new location's path. This is verified per connector during validation, after the server has bound locationId to 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):

PropertyIn-scope variable namespaces
$.name (the location's name, seeded from locationName)provider.*, customer.*
$.description (the location's description, seeded from locationDescription)provider.*, customer.*
$.connectorTemplates[].nameprovider.*, customer.*, location.*
$.connectorTemplates[].descriptionprovider.*, customer.*, location.*
$.connectors[].nameprovider.*, customer.*, location.*
$.connectors[].descriptionprovider.*, customer.*, location.*
$.connectors[].applicationOverrides[].appNameprovider.*, customer.*, location.*, connector.*
$.connectors[].applicationOverrides[].addresses[].listenAddressprovider.*, customer.*, location.*, connector.*
$.connectors[].applicationOverrides[].addresses[].listenPortprovider.*, customer.*, location.*, connector.*
$.connectors[].applicationOverrides[].addresses[].targetAddressprovider.*, customer.*, location.*, connector.*
$.connectors[].applicationOverrides[].addresses[].targetPortprovider.*, 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):

NamespaceVariablesAvailable 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-provider DELETED event.
  • Connector model deleted: Every connector entry that referenced the deleted model is stripped from each template's inline connectors list. The template itself is preserved, even if its connectors list ends up empty. The cascade is driven by the connect-connector-model DELETED event.
  • Connector template deleted: Its id is stripped from each template's connectorTemplateIds list — 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 the connect-connector-template DELETED event. Until the event has been consumed the reference survives; applying in that window fails with a 404 rather 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.

More info