Semver policy
This page is the developer-friendly summary of the in-tree
docs/VERSIONING.md.
That document is the authority — every commitment below is bound by what
it says.
Four artefacts, four rules
Section titled “Four artefacts, four rules”| Artefact | Versioning | Source of truth |
|---|---|---|
SDK packages (@syntarie/tracking, @syntarie/tracking-node, @syntarie/shared) | Strict semver | npm + the API contract §1 |
| Collector + Query API HTTP | URL-prefix versioning (/v1/…) | The API contract §2 + §3 |
| Postgres schema | Append-only with respect to in-support SDKs | The migrations directory |
| Tracking plan | Customer-owned | The plan-diff tool |
SDK packages — strict semver
Section titled “SDK packages — strict semver”Major bump
Section titled “Major bump”Required for any of:
- A symbol listed in the API contract is removed or renamed.
- A required parameter is added.
- A required parameter changes type to a non-supertype.
- A return type narrows in a way that breaks consumers.
- A previously-narrow union widens (
'cookied' | 'cookieless'→string). - A default behaviour reverses (e.g.
respectDntflipping fromtruetofalsewould be a privacy regression and is breaking). - A runtime / browser target is dropped (e.g. raising the Safari floor above 15, or the Node floor above 20).
Minor bump
Section titled “Minor bump”- A new export (function, type, interface, subpath).
- A new optional parameter with a back-compatible default.
- A new optional field on an interface.
- A new tracking-plan event surfaced via
@syntarie/tracking/generated/events.
A minor MAY introduce a deprecation notice on an existing export — but the export itself MUST keep working through the rest of the major. Removal is a major bump.
Patch bump
Section titled “Patch bump”- Bug fix that does not change documented behaviour.
- Performance improvement with no observable surface change.
- Internal refactor that keeps every documented contract stable.
- Doc / typing tightening that narrows an
unknownto its already-promised type.
Subpaths follow the parent
Section titled “Subpaths follow the parent”Every subpath under @syntarie/tracking/<name> shares the version of
the parent package. There is no per-subpath version — a breaking change
in @syntarie/tracking/vitals is a major bump of @syntarie/tracking
as a whole.
Bundle size is a CI gate, not a semver gate
Section titled “Bundle size is a CI gate, not a semver gate”Bundle budgets (< 5 kB gzip core, < 1 kB / ≤ 1.5 kB per opt-in) are enforced in CI as deployment gates. A bug fix that bumps gzip size triggers a code review (the reviewer may require a refactor) — it does NOT trigger a major bump.
Collector + Query API — URL-prefix versioning
Section titled “Collector + Query API — URL-prefix versioning”/v1/ is canonical
Section titled “/v1/ is canonical”Every endpoint listed in the API contract is reachable at BOTH the
existing unprefixed path AND the new /v1/... path. The unprefixed
paths are aliases — adding the alias at v1.0 prevents breaking every
customer who pinned the v0.9 paths.
| Canonical (v1.0+) | Alias (kept through v1.x) |
|---|---|
POST /v1/events | POST /events |
POST /v1/s2s/events | POST /s2s/events |
GET /v1/healthz | GET /healthz |
GET /v1/metrics | GET /metrics |
GET /v1/workspaces, … | GET /workspaces, … |
GET /v1/sites/:siteId/events, … | GET /sites/:siteId/events, … |
POST /v1/webhooks/:adapter/:siteId | POST /webhooks/:adapter/:siteId |
New endpoints land only under /v1/. The unprefixed alias is a
back-compat affordance for the v0.9-era paths; new surface area does
not get a parallel alias.
Non-breaking
Section titled “Non-breaking”- Adding a new endpoint under
/v1/. - Adding an optional query param, header, or request-body field.
- Adding a new field to a response body (callers tolerate unknown JSON keys).
- Adding a new accepted enum value if existing values keep their semantics.
- Adding a new error code for a previously-unrepresented failure mode.
Breaking
Section titled “Breaking”Requires a /v2/ namespace and a deprecation cycle on /v1/:
- Removing a response field.
- Renaming a response field.
- Changing the type of a response field.
- Changing a status code for an existing condition.
- Changing an auth model.
- Removing an endpoint OR removing an unprefixed alias.
- Tightening request validation in a way that rejects a previously-accepted payload.
Deprecation headers
Section titled “Deprecation headers”Per RFC 8594, every deprecated /v1/ endpoint emits:
Deprecation: <http-date>Sunset: <http-date>Link: <https://syntarie.dev/migrate/v1-v2>; rel="deprecation"Deprecation period is at least 12 months between announcement and sunset. The sunset MUST land in a major release — never inside v1.x.
Error envelope stability
Section titled “Error envelope stability”{ "error": { "code": "<stable_string>", "message": "…", "details": [/* optional */] } }code is part of the contract. message is human-readable and may
evolve. details may gain new structured entries.
Postgres schema — append-only
Section titled “Postgres schema — append-only”Append-only with respect to columns written by any SDK still in support.
The platform supports the N-2 SDK majors at the storage layer — at
v1.x that is @syntarie/tracking@0.x and @syntarie/tracking@1.x.
Non-breaking
Section titled “Non-breaking”- Add a column. New SDK populates; old leaves NULL.
- Add an index. Operators create concurrently in production.
- Add a new table.
- Widen a column type so all existing values remain valid.
- Add a constraint that current data already satisfies.
Breaking
Section titled “Breaking”- Rename a column an in-support SDK writes.
- Drop a column an in-support SDK writes.
- Tighten a constraint that current data violates (any
NOT NULLon a previously-nullable column with extantNULLs is breaking). - Change semantic meaning of an existing column without a name change.
The events.raw JSONB safety net
Section titled “The events.raw JSONB safety net”Every event lands in events.raw as the verbatim wire JSON. This is
intentional: it lets columnar fields be backfilled without re-ingesting
from the SDK. Adding a new top-level event field at the wire layer
means the collector can persist the value into events.raw immediately,
and a follow-up migration that promotes it into a typed column can
backfill from raw without losing any historical events.
This safety net is a hard requirement — every collector release must
continue to write the full payload to raw, even fields it doesn’t yet
understand.
Tracking plan — customer-owned
Section titled “Tracking plan — customer-owned”The tracking-plan YAML format and parser follow the same semver as the
rest of @syntarie/shared. Customer plans evolve under their own
breaking-change policy — the plan-diff tool
enforces it in CI.
The platform itself does not constrain how customers evolve their
plans. A customer adding purchase_v2 while keeping purchase is
non-breaking from the platform’s perspective; whether it is breaking
for their downstream analytics consumers is the customer’s decision,
surfaced by the diff tool.
Changelog conventions
Section titled “Changelog conventions”Every release ships a CHANGELOG.md entry following Keep a Changelog.
- Breaking changes — only present in major releases.
- Added — new public surface.
- Changed — observable behaviour change that is not breaking.
- Deprecated — newly-deprecated surface, with the planned removal version.
- Removed — only present in major releases.
- Fixed — bug fixes.
- Security — security fixes, always backported to the most recent minor of every supported major.
- Performance — bundle size deltas, p99 deltas. Reviewer-required when a budget moves.