Configuration Versioning & Lifecycle
Every namespace’s dial plan lives in one YAML document, and that document is versioned like a source repository, not edited like a switch database. If you are coming from a legacy platform where translations tables are mutated live, row by row, this page is the mental-model shift; the day-to-day commands build on it.
The model: immutable versions, two pointers
Section titled “The model: immutable versions, two pointers”Nothing ever edits a configuration in place. Every write — a full upload, a patch, an edited sub-tree — produces a new, immutable version, identified by a ULID (sortable by creation time). Old versions remain readable forever.
Two named pointers move across that history:
latest— the newest stored version: your drafting head.running— the version calls are actually processed against. It advances only when someone applies a version.
The gap between them is the feature: you can stage a change, validate it,
diff it, have a colleague review it, tag it — and only then promote it. A
namespace whose latest is ahead of running is in the normal
“change staged, not yet live” state.
In git terms: a version is a commit, latest is your branch head,
apply is the deploy, and tags are tags. Two deliberate differences from
git: history is linear (no branches — see concurrency below), and the
deploy step is first-class in the API rather than an external CD pipeline,
with its own permission (config:apply) so drafting and promoting can be
held by different people (the editor / config-publisher split on the
RBAC page).
What a version records
Section titled “What a version records”| Field | Meaning |
|---|---|
version |
The ULID — also the ETag everywhere this version appears |
parent_version |
The version this one was derived from (provenance) |
tag |
Optional human label, unique per namespace, reassignable (golden, pre-migration) |
description |
Free-form operator notes |
config_schema_version |
The configuration-schema revision it was written against |
Two documents are stored per version: the YAML you authored, and the
compiled effective configuration — your document after
parent inheritance is merged in
at store time. Apply renders the effective document, which is why a parent’s
change reaches you only when you store (and apply) a new version. Both
documents are retrievable for any version: the authored view is the default,
and ?view=effective (or vsscli config get --effective) returns the
merged one — the view to read when the switch behaves per a rule you never
wrote.
Tags matter operationally because any API field or CLI argument that
accepts a version accepts a tag (and the latest / running aliases).
Keeping a golden tag on your last known-good version turns “roll back”
into a one-liner.
Concurrency: declared bases and linear history
Section titled “Concurrency: declared bases and linear history”Two people editing one document is the classic way to silently lose a change: A downloads, B downloads, A uploads, B uploads — A’s work vanishes. The API makes that impossible by requiring every write to declare the version it is based on:
- A full upload (
PUT) must carry the base in anIf-Matchheader (the ETag from the version you fetched) or a?base=parameter. - A patch (
PATCH) names its source version in the URL.
If your declared base is no longer the current latest, the write is
refused with 412 Precondition Failed — and the response tells you what
latest has become (in the body’s current_latest field and the ETag
header), so you can re-fetch, reconcile, and retry. Nothing is guessed and
nothing is merged behind your back.
A consequence worth internalizing: because every accepted write is based on
the latest it replaced, history is a single straight line. There are
no branches to reconcile later; the 412 is where a would-have-been branch
is stopped and turned into an explicit rebase by the losing writer.
Rebase, explicitly
Section titled “Rebase, explicitly”For patch-shaped edits there is an opt-in shortcut: ?rebase=true on a
PATCH replays your operations against the current latest instead of
returning 412, and reports which version it rebased onto. This is a
replay, not a merge — each operation is re-applied to the newer
document, and an operation that no longer makes sense there (a path that
vanished, a test op that no longer matches) still fails the patch. Use
JSON Patch test operations when you need “apply only if the value is
still what I saw” semantics; use plain 412-handling when a human should
look before retrying. vsscli config push --rebase does the human version:
on a 412 it shows you a diff against the new latest and asks.
Validation: three gates before traffic
Section titled “Validation: three gates before traffic”- Dry-run validate — submit a document to the validate endpoint and get the full error list without storing anything. Two variants: one validates in your namespace’s context (inheritance merged in, so references to inherited entries resolve), one validates a standalone document with no context.
- Store-time validation — every write runs the same compiler; an
invalid document is refused outright, so an invalid version cannot
exist. Whatever
latestpoints at is always applyable in the validation sense. - Apply-time guards and render — apply re-checks two cheap guards (below), then converges the switch onto the version; a render failure rolls back (below).
Validation failures return a structured problem document whose
validation_errors field lists every finding, not just the first — fix
them in one pass.
Apply: promotion, guards, and rollback
Section titled “Apply: promotion, guards, and rollback”POST …/config/{version}/apply promotes a version to running. Unless
you pass force=true, two guards run first:
- Descendancy — the version must be
runningitself or a descendant of it (reachable by walkingparent_versionback torunning). This stops you from accidentally promoting a stale line of history — e.g. an old version fetched by a script racing a colleague’s newer apply. Applying an older version (a revert) trips this guard by design; that’s whatforce— or better, roll-forward (below) — is for. - Schema version — the version must have been stored under the same configuration-schema revision the server currently runs. After a platform upgrade that bumps the schema, old versions refuse to apply until re-stored (or forced) so they are never reinterpreted under changed rules.
What apply actually does
Section titled “What apply actually does”Apply is a convergence, not a restart. The switch diffs the new
effective configuration against what is currently rendered and touches only
the deltas: unchanged trunks, devices, and registrations are left alone (a
re-apply does not drop active calls or force re-registration), new
resources are provisioned, and removed resources are torn down (including
the voicemail-cleanup behavior governed by
cleanup-mailboxes). Re-applying
the currently running version is a supported repair operation — a no-op
promote that re-renders the side state.
If apply fails
Section titled “If apply fails”Apply is transactional at the namespace level. If rendering the new version fails partway (for example, a trunk IP turns out to be claimed by another namespace at the last moment):
- the switch automatically re-renders the previous running version and reports the failure with the running version unchanged — you are not left half-migrated;
- only if that rollback also fails do you get an explicit “namespace may be non-operational” error, which is your cue to re-apply a known-good version (or call support).
A version that failed to apply remains stored; nothing is deleted by a failed promotion.
Reverting
Section titled “Reverting”Two ways to get back to a previous version, with a clear preference:
Roll forward (recommended). Fetch the old version’s YAML, push it as a
new version (optionally tagging it revert-of-…), and apply that.
History stays linear, the descendancy guard stays happy, provenance shows
exactly what happened, and the next engineer’s latest is also the
configuration that is actually running.
Force-apply the old version. apply with force=true skips the
descendancy guard and promotes the old version directly. Faster in an
incident, but it leaves running pointing behind latest — anyone who
now edits latest is editing a document that does not match what’s live,
and their eventual apply will silently re-deploy whatever was staged above
your revert point. If you force-revert, follow up with a roll-forward push
so latest and running agree again.
Interaction with the namespace tree
Section titled “Interaction with the namespace tree”Two lifecycle rules from the inheritance model are worth restating here because they shape rollout ordering:
- A child’s version compiles against its parent’s running config at the moment the child stores — so tree-wide changes deploy top-down: parent applies first, then each child re-stores and applies.
- Your own store can be rejected by constraints you don’t author: the parent’s grant (permitted realms/edge IPs, linkage bindings) and the partition’s allow-lists are enforced at validate/store time.
Limits and defaults
Section titled “Limits and defaults”| Item | Value |
|---|---|
| Version identifier | ULID (time-sortable); doubles as the ETag |
| Version list page size | 100 (cursor-paginated) |
| Document size limit | 10 MB per configuration document |
PUT precondition |
required — If-Match or ?base= (412 on staleness) |
| Patch format | RFC 6902 JSON Patch, JSON or YAML encoded |
| Force / rebase toggles | ?force=true / ?rebase=true (or X-Force / X-Rebase headers) |
| Tag | unique per namespace, reassignable; valid anywhere a version is accepted |