Deprecation & sunset signalling
Solvano gives integrators a machine-readable heads-up when a route is on a
deprecation path, ahead of any future /v2. The signalling is driven by one
in-code registry in
the deprecation module. Sunset follows
RFC 8594; the companion Deprecation
header predates RFC 9745 and still
carries that draft's boolean value (see below).
This process is the exception path for the frozen API v1 stability contract: because a
/v1-breaking change has no override in the CI compatibility gate, a genuine break is deprecated here and shipped under a new version rather than edited into/v1.
Contractual notice periods
Under the versioning & support policy, the notice period before a surface is removed is a contract, not a courtesy. The minimum notice depends on the surface class:
| Surface class | Minimum notice before removal | Where it lands |
|---|---|---|
/v1 API — a path, operation, field, enum value, or parameter | Never removed within /v1. The old shape is kept and a replacement ships under /v2, with a ≥ 12-month overlap during which both are served before the /v1 shape is sunset. | Deprecation/Sunset/Link headers, /v1/meta/deprecations, and the changelog |
Webhook v1 event shapes | Per each event's published Sunset date — once published, a sunset date is never shortened. | headers + the X-Solvano-Event-Version version stamp + the changelog |
| Config / environment variables | ≥ 2 releases: announced (deprecated) in release N, removed no earlier than release N+2, counted in the published platform version (doc 57 §1). | the release notes Operator actions section + docs/releases/ledger.yaml |
Three rules bind these guarantees:
- Headers are the notification channel. A deprecated route/field carries the headers below on every response for its entire notice window — a caller can detect a pending removal programmatically, never only at removal time.
- The changelog is the record. Every deprecation is recorded in the generated changelog at announcement and again at removal, so there is a durable, dated audit trail of the notice.
- A config/env removal is a schema-style contraction. It therefore also obeys
the release-sequencing contract: the deprecation is an
announcement entry in
docs/releases/ledger.yamlin release N, and an automated sequencing check fails if the removal lands earlier than N+2. The one-release-plus overlap is what keeps anN+1 → Nbinary rollback safe without a config rollback. N is the platform version you deploy, so "two releases" is something you can count off your own upgrade history — no separate scheme to map onto.
The notice periods are minimums — a widely-used surface may be given longer, never shorter. Shortening a published notice period is itself a breaking change to this contract and is not permitted.
What a caller sees
When a route is flagged deprecated, every response to it carries:
Deprecation: @<seconds>— the date the deprecation took effect, as the RFC 9745 Structured Fields date: an@followed by seconds since the Unix epoch (e.g.Deprecation: @1784678400for 2026-07-22, resolved at midnight UTC). This is the announcement date;Sunsetis the one to plan against. The same date is available in ISO-8601 form as thesincefield ofGET /v1/meta/deprecations.Sunset: <HTTP-date>— the RFC 7231 IMF-fixdate after which the route may be removed (e.g.Tue, 31 Dec 2030 23:59:59 GMT).Link: <doc-url>; rel="deprecation"— a link to the documentation for the change, and, when a successor exists,Link: </v1/new-route>; rel="successor-version".
The full set of deprecated routes is also available as JSON:
GET /v1/meta/deprecations
[
{
"route": "/v1/old-thing/{id}",
"since": "2026-01-01",
"sunset": "Tue, 31 Dec 2030 23:59:59 GMT",
"replacement": "/v1/new-thing/{id}",
"documentation": "https://docs.solvano.ai/deprecations#old-thing"
}
]
See Active deprecations below for the routes
currently in the registry. Actual /v2 routing is out of scope of this
signalling layer.
Active deprecations
The deprecations currently open under this policy. Each is an
announcement: the surface is still served for its entire notice window, the
signalling headers are stamped now, and removal lands in a later release under
the expand/contract / release-sequencing contract (never sooner than the
published Sunset).
| Surface | Class | Announced | Sunset (published) | Replacement | Migration steps |
|---|---|---|---|---|---|
GET /v1/admin/orgs/{org_id}/webhook-secret | Webhook v1 signing (org-level shared secret) | 2026-07-22 | Fri, 31 Dec 2027 23:59:59 GMT | GET /v1/orgs/{id}/webhooks/{sub_id}/signing (per-subscription v2) | Create a per-subscription signing secret via POST /v1/orgs/{id}/webhooks/{sub_id}/signing/rotate, switch consumers to verify the combined X-Solvano-Signature: t=…,v1=… header, then stop relying on the org-level secret. |
POST /v1/admin/orgs/{org_id}/webhook-secret/rotate | Webhook v1 signing (org-level shared secret) | 2026-07-22 | Fri, 31 Dec 2027 23:59:59 GMT | POST /v1/orgs/{id}/webhooks/{sub_id}/signing/rotate (per-subscription v2) | Rotate the per-subscription secret instead; the org-level rotate becomes a no-op once every subscription has its own secret. |
Outbound webhook v1 delivery (org-level signing, X-Solvano-Signature: v1=…) | Webhook v1 event shapes | 2026-07-22 | Fri, 31 Dec 2027 23:59:59 GMT | Per-subscription v2 signing (X-Solvano-Signature: t=…,v1=…) | Deliveries to a subscription without a per-subscription secret carry Deprecation + Sunset + a rel="deprecation" Link; configure per-subscription signing to move a subscription off the v1 wire format. |
SOLVANO_READ_LIMIT_PER_MINUTE env var (global read-quota knob) | Config / environment variable | 2026-07-22 | Removal no earlier than two releases after the announcement | Per-org org_rate_limits.read_rpm rows (admin rate-limit endpoint) + rate_limit_policies | Set per-org read quotas via the admin rate-limit surface instead of the global env knob; the env var stays as the global fallback for orgs with no row until removal. |
GET /v1/forecast (dry-run) | /v1 API | 2026-07-22 | Wed, 31 Dec 2036 23:59:59 GMT (decade-out) | /v1/capacity/forecasts (scenario-based capacity planning) | Move deployment-scaling ROI questions to the richer scenario-based capacity-planning surface. This is a deprecation-path dry-run — the machinery is proven end-to-end (headers, meta listing, counter, OpenAPI flag) on one low-value legacy read (the crude linear projection) with a decade-out sunset and no removal planned; the route stays fully served. |
The two webhook-secret routes carry these headers and appear in
GET /v1/meta/deprecations; the outbound delivery nudge stamps the same
Deprecation/Sunset/Link headers on each v1 delivery. The
GET /v1/forecast dry-run row is deliberately far-future (a decade out)
and slated for no removal — it exists to exercise the full signalling path
(response headers, the /v1/meta/deprecations listing, the
solvano_deprecated_route_calls_total{route="/v1/forecast"} counter, and the
OpenAPI deprecated: true flag) on a genuinely low-value legacy read without
committing to a /v2, and the regression suite covers it end to end. The
env-var deprecation is a config-class change (no HTTP surface), announced here and in
docs/releases/ledger.yaml; per the config contract it is removed no earlier
than two releases after this announcement.
Webhooks v1 signing
The documentation link on the three webhook rows above points here. They
are one deprecation: the org-level shared signing secret, and the v1=
signature it produces on outbound deliveries. Each subscription can carry its
own secret instead; once it does, its deliveries are signed per subscription
and stop carrying the deprecation headers.
Forecast v1 (linear)
The documentation link on GET /v1/forecast points here. The route
projects a deployment's measured return linearly by team count, with no
diminishing-returns curve. The replacement is scenario-based capacity
planning (/v1/capacity/forecasts). The route stays served, and no removal is
planned.
Not deprecated in this sweep: the server-rendered console
The deprecation sweep considered the server-rendered console
in favour of the single-page
console, but did not. ADR 0006 is
explicit that the server-rendered console "stays as the fallback under the
doc 37 lifecycle" while the SPA matures — it is a deliberate fallback, not a
sunsetting surface. Deprecating it would require a proven-parity + fallback-
retirement decision that ADR 0006 has not taken, so no Deprecation header or
sunset date is attached to those routes here. Revisit when a future ADR
supersedes ADR 0006's fallback designation.
Observability
Calls to deprecated routes are counted and exported on /metrics:
# TYPE solvano_deprecated_route_calls_total counter
solvano_deprecated_route_calls_total{route="/v1/old-thing/{id}"} 12
The label is the route template (via MatchedPath), so cardinality stays
bounded no matter how many distinct ids are called. Operators can alert on this
to find integrators still using a sunsetting route.
What a deprecation guarantees
The Deprecation/Sunset/Link headers, the /v1/meta/deprecations entry,
and the counter metric are not opt-in per route: the registry is read on every
response, so once a route is deprecated every response to it carries the
headers, the listing includes it, and its calls are counted.
Every sunset value is a valid RFC 7231 HTTP-date, so you can parse it rather
than pattern-match it.
How a route is flagged is documented for contributors in Adopting the endpoint conventions.