A Practical Guide to Backward-Compatible API Evolution

All backend tests passed. The deployment succeeded. Yesterday’s mobile app stopped working.

This is a fictional scenario, not a report about a customer or Tagbogen. Imagine a small appointment-booking app. Its backend now returns startTime instead of startsAt. The current provider tests expect startTime, so they pass. An installed app still reads startsAt, so opening an appointment fails.

The missing test was a question about the system: Can the new server still serve the client people already have? Backend engineers, mobile developers and architects need to answer that question together. A server can be deployed in minutes; installed clients follow a different release clock.

This guide explains that boundary and includes a runnable, dependency-free HTTP lab. The principles apply across technology stacks. If HTTP controllers and data transfer objects are new to you, start with our Spring Boot REST API guide. Here, the architecture question is how to evolve that API when clients cannot all update together.

1. Define compatibility against named clients

Backward compatibility means an updated provider continues to satisfy the relevant expectations of the existing clients we have committed to support. It needs a scope: which clients, which operations, which data and which observable behavior?

For our example, the support set contains simulated mobile client 1.0 and, once released, simulated mobile client 2.0. Client 1.0 calls GET /appointments/a1, expects HTTP 200 with JSON, requires a non-null UTC timestamp string named startsAt, and recognizes CONFIRMED and CANCELLED. It ignores unknown response fields. Client 2.0 has the same assumptions except that its required time field is startTime. Both interpret the value as the appointment’s start instant. These are deliberately narrow teaching clients, not real mobile binaries.

API revision A returns:

{"id":"a1","startsAt":"2026-12-01T09:00:00Z","status":"CONFIRMED"}

The proposed API revision B returns:

{"id":"a1","startTime":"2026-12-01T09:00:00Z","status":"CONFIRMED"}

The revised provider works for client 2.0, but client 1.0 cannot find its required field. A compatible transition for these two specific readers is:

{"id":"a1","startsAt":"2026-12-01T09:00:00Z","startTime":"2026-12-01T09:00:00Z","status":"CONFIRMED"}

Both aliases must represent the same instant. We keep the endpoint unchanged here; A and B are consecutive contract revisions, not /v1 and /v2 routes. Adding a second field would fail a different client that rejects unknown properties. Compatibility is always relative to actual reader behavior.

2. Three ways to change a contract

A structural change alters the message shape: property names, types, presence, allowed values or nesting. Removing startsAt, changing a timestamp string to an integer, or requiring a new request field can affect an existing reader or writer.

A semantic change alters meaning while leaving the shape valid. A string could still parse while changing from an appointment start to a check-in deadline. A numeric duration could change from minutes to seconds. Tests must assert business meaning, not only successful parsing.

A behavioral change alters interactions: a default page size, error status, ordering, validation rule, retry expectation or pagination flow. A list that changes from ascending appointment time to descending creation time may still match its schema while an old app displays the wrong “next appointment.” The categories can overlap; their purpose is to help reviewers find assumptions.

On small screens, scroll the table horizontally.

Change Possible client problem Useful check Safer migration
Remove or rename a response field Reader still requires the old name Old decoder over HTTP; schema diff Retain alias or keep old representation
Change type or permit null String parser or non-null model rejects value Boundary and null fixtures with client parser Preserve old type; introduce separately defined field
Add a required request field Existing writers omit it Replay supported request shapes Derive a safe default if meaning permits, or version the operation
Add a response field Strict decoder rejects unknown properties Actual serializer configuration Preserve a separate old representation if necessary
Add an enum value Closed enum decoder fails or UI lacks a branch New value through old reader and UI flow Negotiate capability or map only where meaning remains valid
Change default or business meaning Omitted input produces a different appointment Business outcome fixtures Retain default; make new behavior explicit
Change error status or body Client retries a permanent error or loses recovery UI Error paths with client handling Preserve established error contract; stage adoption
Change pagination or sorting Client misses, repeats or misorders appointments Multi-page traversal with stable fixtures Preserve old defaults/cursor semantics; opt in to new scheme

The direction matters. Allowing a request field to be omitted may widen accepted inputs; allowing a response field to be absent can violate an existing reader’s requirement. Optional and nullable are separate: “the property can be missing” and “the property can contain null” are different promises.

An additive change deserves review too. Ignoring unknown JSON keys does not imply tolerating unknown enum values. Nor does accepting a new enum value mean the interface can display it meaningfully. An UNKNOWN fallback may be appropriate for a label; it may be unsafe if the status determines whether a user may cancel a booking. Design and test that behavior explicitly.

3. Why the green build missed the old app

Unit tests can verify that the appointment mapper returns the newly requested field. Provider integration tests can verify that routing, serialization and persistence work together. Both can pass after their expectations are updated to the new contract. Neither automatically exercises the decoder inside an installed client.

Provider tests inspect the new server; a missing path to the old client explains the test gap.Select the diagram for full-size reading.

An OpenAPI description makes paths, responses and schemas reviewable. A compatibility diff can flag a removed property or a newly required request field. OpenAPI does not contain every assumption a client makes about business meaning, sort order or error recovery. Its schema is also not evidence that a particular generated SDK tolerates a change. Use the specification version supported by your actual validators and generators. This article references OpenAPI 3.2.0; the official version index also lists newer patch revisions. The lab does not run an OpenAPI validator or claim generator support for 3.2.

Three checks serve different purposes:

  • Schema compatibility checks compare declared interfaces and catch classes of structural change. Their coverage depends on the descriptions, rule configuration and supported dialect.
  • Contract tests verify captured interactions and expectations. They can catch a missing old field before deployment, provided the supported consumer’s expectation remains in the test set.
  • Tests against real supported client versions exercise their shipped parsing and application behavior. Include representative device/runtime combinations and relevant success, error and upgrade flows; these tests cost more and still cover selected cases.

Use these together. A schema diff is fast review evidence; a retained consumer contract is a focused release gate; an old binary can expose assumptions neither captured.

What consumer-driven contracts add

In a consumer-driven workflow, the consumer team tests its API-using code against a controlled provider substitute and records the interactions it needs. The provider verifies those contracts against its implementation with appropriate test data. Pact describes this consumer-test and provider-verification cycle. For the appointment reader, the interaction would capture the request and a response containing the required old timestamp field.

The practical requirement is to retain the contracts of supported installed releases. Verifying only the latest consumer branch recreates the original gap. Record consumer release identity, provider revision and verification results; gate deployment against the relevant support set, rather than merely collecting contract files.

Contracts protect the assumptions they actually express. A timestamp matcher that accepts any string does not prove UTC meaning. A recorded successful read says nothing about offline reconciliation. Pact’s FAQ distinguishes contract verification from provider functional and load testing. Business behavior and device tests still have work to do. Pact is discussed as an established workflow; it is not installed or executed in this lab, and no Pact runtime version is claimed as tested.

4. Run the failure, then close the gap

Download the API evolution lab. Extract it and open a terminal in lab. It uses CPython’s built-in unittest, JSON encoding/decoding and a loopback HTTP server. There are no third-party dependencies. The tested runtime is CPython 3.13.14 on Windows.

The provider sends actual JSON bytes over HTTP. The simulated clients fetch them, check status and content type, decode JSON and validate their required timestamp and status. No iOS or Android application was run.

First, run the existing provider suite:

python -m unittest -v test_lab.ProviderTests

Both tests pass against the broken revision: they check the new response and the new simulated reader. Now run the compatibility gate in PowerShell:

$env:API_MODE = 'broken'
python -m unittest -v test_lab.RegressionGate

The gate exits with an error: KeyError: 'startsAt'. It calls the old client’s decoder over real HTTP. This is the previously missing expectation, not a test that merely compares two server dictionaries.

The fix keeps both aliases in the response. Internally they derive from one START value; production code should likewise have one authoritative instant and separate contract mappings. Test the transition and then the full suite:

$env:API_MODE = 'transition'
python -m unittest -v test_lab.RegressionGate
Remove-Item Env:API_MODE
python -m unittest -v

The transition gate passes. The full suite has 11 passing tests, including both supported simulated clients, alias equality, the original old reader, and explicit reproductions of the rename failure, strict-reader addition failure, unknown-enum failure, null failure and rollback failure. Expected failures are asserted as such in that full suite. They remain defects of the deliberately incompatible variants; a green demonstration suite does not make those variants deployable.

The client requirement is visible in a small piece of code:

def old_client(base):
    # Simulated mobile 1.0: requires startsAt; ignores unknown fields.
    return decode(fetch(base), 'startsAt')

def new_client(base):
    # Simulated mobile 2.0: requires startTime; ignores unknown fields.
    return decode(fetch(base), 'startTime')

This is a handwritten consumer compatibility test using unittest, not a Pact-generated consumer-driven contract. It makes the failure reproducible with minimal infrastructure. In a larger system, generate and verify shared contracts from consumer tests, then supplement them with real supported app releases. The README gives manual server commands and POSIX equivalents. The teaching server uses http.server, which Python explicitly does not recommend for production.

5. Evolve one contract or introduce a version?

Compatible evolution fits when the existing meaning can be preserved at reasonable cost. Keep the old field, add an optional capability, retain established defaults and test the support matrix. One public contract avoids duplicate documentation and routing, but accumulates aliases, fallback behavior and review obligations.

Explicit versioning fits when the old and new contracts need incompatible representations or semantics, or strict old readers cannot accept an extension. For example, keep the exact A representation on /v1/appointments/a1 and offer the new representation on /v2/appointments/a1. Header or media-type negotiation is another design; document selection rules and configure caches to distinguish negotiated representations correctly.

Versioning provides a place to preserve the old promise. It does not preserve that promise by itself. Both routes may still share a service whose default sort order was changed. Each supported version needs tests, documentation, monitoring, security fixes and operational ownership. Separate deployments add more release and rollback coordination. A shared implementation with separate adapters reduces duplication only while the shared business behavior satisfies both contracts.

Avoid creating a new version for every harmless change. Also avoid calling a change compatible solely because a new URL is available: existing clients must still receive their supported behavior at the old URL. Choose based on the cost of maintaining both promises and the risk of mixing their meanings.

Make the version boundary concrete

For a versioned alternative to our example, client 1.0 could read /v1/appointments/a1 and receive only id, startsAt and status. Client 2.0 could read /v2/appointments/a1 and receive id, startTime and status. Separate response adapters map the same internal appointment start instant to each representation. A strict v1 reader then never sees the extra v2 field. The request path selects the contract; changing the path alone does not implement the mapping.

An important migration detail: our already installed fictional client 1.0 actually calls the unversioned /appointments/a1. Introducing /v1 cannot teach that binary a new URL. Preserve its existing route and representation as a legacy entry point, while newer clients explicitly select their version. A redirect is not a universal repair: the reader still needs a compatible response, and redirect handling depends on the client and operation. Version selection must be planned before relying on it as an escape route.

UML component diagram: legacy, v1 and v2 HTTP adapters require ReadAppointment; the appointment application provides that interface. Route and response-field notes distinguish the external contracts.Select the diagram for full-size reading.

Reading the UML: boxes marked as components represent implementation responsibilities. The interface rectangle names the application contract. A dashed open arrow marked «use» points from a component to the interface it requires; a dashed line ending in a hollow triangle points from an implementation to the interface it realizes, or provides. These are static relationships, not the order of an HTTP request. The diagrams use component and sequence notation from UML, with editable PlantUML source retained in the article package.

In this design, a port is an application-facing interface such as “read an appointment.” The HTTP adapters translate paths, headers and wire models into that interface, then translate the result back into the selected response model. This applies the boundary separation described in Alistair Cockburn’s Ports and Adapters architecture. The domain does not need to know whether its result will become startsAt, startTime or a screen-specific response.

The component diagram makes the dependency direction explicit: all three HTTP adapters require the same application-owned interface, and the application implements it. The core should not import a v1 HTTP response class. Keep authorization and appointment invariants at their appropriate shared enforcement points. A legacy adapter must not provide a way around a rule enforced for v2.

For a read, the shared use case can return one appointment projection, and each adapter maps it to its response DTO. For a write, each adapter validates its input shape and constructs an application command. The use case still decides whether the requested booking or cancellation is allowed. If an old operation means “request cancellation” and a new operation means “cancel immediately,” they need explicit application behavior; a DTO mapper cannot make those operations equivalent.

Test the boundary in both directions: supported HTTP request → expected application command, and application result → supported HTTP response. Include omitted fields, unknown values and error mappings. Test the shared use case’s business rules separately. This keeps version-specific wire concerns visible without duplicating the whole booking domain for every API version.

API adapters and an Anti-Corruption Layer

Keep public request/response models at the boundary instead of spreading startsAt, startTime and version checks through the domain. The conceptual path is: client contract → version-specific adapter → application use case → domain model. Responses travel back through the appropriate adapter. A simple field-name mapping is an API adapter; it does not by itself require the full Anti-Corruption Layer pattern.

An Anti-Corruption Layer (ACL) becomes useful when a legacy or external model carries different meanings that would otherwise distort the internal model. It translates between models while protecting the model on its own side of the boundary, as described in the Microsoft architecture pattern guidance. For example, an old booking system might encode an appointment as a local wall-clock time plus a location code, while the new domain requires an unambiguous instant. The translation boundary needs explicit time-zone and daylight-saving rules; renaming a property cannot resolve that semantic difference.

Such a boundary can be an in-process module. A separately deployed translation service adds network latency and operational work. Either form needs maintained mappings and tests for both contracts and their business meaning. If the old model cannot represent a new state faithfully, define a deliberate old-client policy or withhold the new capability; an ACL cannot invent a lossless translation. Keep business decisions in their owning domain and isolate the conversion logic. The downloadable lab demonstrates the simpler alias transition on one unversioned endpoint; these versioned routes and the ACL are design alternatives, not additional implemented lab features.

UML component diagram: the appointment application requires its own SchedulingPort, provided by the ACL. The ACL requires LegacySchedulerAPI, provided by the legacy scheduler; interface ownership isolates the legacy model.Select the diagram for full-size reading.

The ACL in this second component diagram sits at an outgoing integration boundary: our application requires SchedulingPort, expressed in our vocabulary, and the ACL provides it. The ACL in turn requires the legacy system’s LegacySchedulerAPI. The application therefore does not need to depend on LegacySlot. This placement differs from the incoming v1/v2 HTTP adapters above. A system may need both, but each exists for a specific boundary.

Consider a legacy value of “02:30 at location X” on a daylight-saving transition. Before producing an instant, the integration needs a reliable location-to-time-zone mapping and enough information to resolve whether that wall time occurred once, twice or not at all. If the legacy data is insufficient, reject the mapping or use an explicitly approved clarification policy; silently guessing would create a syntactically valid but potentially wrong appointment. This is an illustrative edge case, not a conversion implemented by the lab.

Give translation failures their own diagnostic category. A legacy timeout, an unmapped location and an ambiguous local time call for different recovery. Record the mapping revision and useful correlation data without indiscriminately logging appointment payloads. Verify round trips only where the models can represent the same information; otherwise test the intended information loss or refusal explicitly. These are practical recommendations for operating the boundary, rather than extra guarantees provided by the pattern name.

Follow one read through the selected contract

UML sequence diagram: an old or new client selects the legacy or v2 HTTP adapter. Both use the appointment application and scheduling ACL. An unambiguous legacy value produces the selected response field; ambiguous data produces the established error response.Select the diagram for full-size reading.

Reading the UML: time proceeds downward along the dashed lifelines. Solid message arrows show calls; dashed arrows show replies. An alt frame contains mutually exclusive alternatives, selected by its condition. The client and HTTP-adapter lifelines stand for the instances selected for one request, rather than both app releases running as one client.

The first alternative preserves the old client’s actual route, /appointments/a1; the second explicitly selects /v2/appointments/a1. Both adapters call the same read operation. The shown path assumes authorization succeeds, then obtains the appointment start through the ACL. Other network, authorization and business failures are omitted to keep this interaction readable.

If the legacy data determines an unambiguous instant, the application returns an appointment view. The selected HTTP adapter maps that result to startsAt or startTime. If the ACL cannot determine the instant, it returns a mapping failure instead of inventing an appointment time. The application and adapter then apply the established error policy for that client contract; the diagram deliberately does not invent a universal HTTP status for every translation failure.

Together, the two component diagrams answer “who depends on which contract?” and the sequence answers “what happens during a request?” They describe an illustrative versioned design with a legacy integration. The executable lab remains the smaller unversioned alias example and does not implement this ACL or these versioned routes.

Choose patterns by the boundary they protect

On small screens, scroll the table horizontally.

Pattern or mechanism Main question Appointment example Cost or limit
Versioned API adapters How do several wire contracts reach application behavior? Legacy/v1 emits startsAt; v2 emits startTime Each adapter needs request, response and error tests
Anti-Corruption Layer How do we keep another model’s meanings outside our domain? Translate legacy scheduling values into an unambiguous instant Incomplete source information may make translation impossible
Parallel Change / Expand–Migrate–Contract How do consumers move without an atomic cutover? Serve both contracts before retiring the old one The overlap lasts as long as the support decision requires
Strangler Fig How do we replace an implementation gradually? Route migrated appointment reads to a new service Routing and data ownership must stay coherent
Backend for Frontend How do different frontend needs get suitable APIs? A mobile agenda response versus an admin scheduling view The mobile BFF still supports multiple installed app versions

The first two organize translation responsibilities, Parallel Change organizes time, and Strangler Fig organizes replacement. They can be combined, but adopting all of them would add work without necessarily adding protection. For this lab’s rename, retaining an alias and testing both readers is enough. Strict readers motivate separate representations; genuinely different models motivate an ACL; replacement of a substantial legacy implementation may motivate a Strangler Fig rollout.

Strangler Fig: replace the backend behind a stable contract

Suppose the appointment API is backed by an old scheduling service and we want to replace that service incrementally. A routing facade can direct migrated operations to the new implementation while leaving other operations on the old one. This is the purpose of the Strangler Fig pattern. The externally supported contract can remain stable throughout that replacement.

For example, move appointment reads for an explicitly selected tenant to the new service only after its data is available and the responses preserve the tenant’s supported contracts. Keep cancellation on the old service until ownership and consistency are settled. That split is a design decision to validate, not automatically a safe partition: reads must still reflect the authoritative cancellations.

Identify which implementation owns each write and how the other obtains current state. Blindly sending the same booking command to both systems risks duplicate reservations. Shadowing reads may help compare results under suitable privacy and load constraints; shadowing writes needs a design that prevents duplicate business effects. The routing facade also needs failure handling and operational ownership. A route switch alone does not make stale replicas, incompatible data or rollback safe. Strangler Fig can retire an implementation while a legacy API adapter remains necessary for installed clients.

A BFF is a client boundary, not an app-update mechanism

A Backend for Frontend tailors a backend to a particular frontend’s needs. A mobile BFF might compose a compact agenda from several services while an admin-web BFF exposes a richer scheduling workflow. This can give frontend teams a clearer boundary and release ownership.

It does not make installed mobile clients update together. A mobile BFF must still honor the contracts of supported mobile releases, perhaps through its own versioned adapters. Avoid creating a separately deployed BFF for every app release merely to rename a field. Client-oriented composition, semantic translation, explicit version selection and migration timing solve different problems.

6. Migrate across release clocks

Parallel Change makes the overlap explicit

The transition already demonstrated is an example of Parallel Change, also called expand and contract. Its three phases are expand, migrate and contract. Applying it to an external API requires a support decision about clients that the backend team cannot redeploy itself.

Parallel Change over time: expand the server to support old and new contracts, migrate clients while both remain available, and contract only after the support and retirement criteria are met. Returning to the old-only server is unsafe after new clients depend on the expanded contract.Select the diagram for full-size reading.

In the expand phase, release a provider that can honor both promises. For our permissive simulated readers, that means both aliases in one representation. For strict readers, it could mean two representations behind separate routes. Keep one authoritative appointment time so that the overlap does not create two independently editable sources of truth.

In the migrate phase, new releases adopt the new contract while supported old releases keep working. Measure adoption and failures, retain both sets of compatibility tests, and consider delayed reconnects. A feature flag may control a new flow, but disabling the flag does not erase dependencies already shipped in a client binary.

The contract phase removes the old promise only when the retirement criteria below permit it. If old supported releases still need it, remain in the overlap or revisit the support policy explicitly. The word “temporary” does not establish a retirement date. Likewise, successful deployment of the expanded server is not permission to roll back to an old-only server once new clients depend on it.

Migration flow: inventory clients, expand the backend, verify old and new clients, release gradually, observe and communicate, then decide whether retirement criteria are met.Select the diagram for full-size reading.

Start with an inventory of supported app releases and their assumptions. Include serializers, generated SDK revisions, error paths, pagination and queued request formats. Treat unidentified traffic as uncertainty to investigate. Client-version metadata can help operations; it is not an authorization credential.

Expand the server before releasing a client that requires the new field. Verify old and new readers against the same candidate server and retain the old path through staged rollout. For our transition, client 1.0 reads startsAt and client 2.0 reads startTime; both see the same instant. If an old reader is strict, use separate representations instead of sending it the extra property.

Observe requests by contract and app version, parser/recovery errors where responsibly available, and actual business outcomes. Canary traffic helps only if the old client population is represented. No recent request is not proof that no old client remains. A phone may be offline, unused or prevented from updating. Define the observation window from the product’s usage and reconnect behavior rather than inventing a universal number of days.

Offline operation adds another contract: a stored request created under old rules may be sent after the server changes. Preserve or translate its payload and operation meaning; define expiry, conflict handling and safe retries. A mandatory new request field cannot be repaired by a response alias. Test disconnect, queue, upgrade and reconnect against supported release combinations. The read-only lab does not implement this queue.

Communicate deprecation through release notes, developer contacts, documentation and an understandable in-app recovery/update path where appropriate. Do not rely on an old client to display a header it never implemented. RFC 9745 defines the Deprecation response field and a link relation for further information. Deprecation does not itself change resource behavior. RFC 8594 separately defines Sunset, indicating when a URI is likely to become unresponsive.

Use the specified wire formats: Deprecation uses a structured date such as @1794614400, while Sunset uses an HTTP date. Announce a sunset only when an actual retirement decision exists; it must not precede the deprecation date. Headers supplement communication and policy; they create neither an automatic migration nor a universal support period.

Retire an old contract only with recorded criteria: the support policy permits it, affected consumers have an actionable migration path, representative reconnect scenarios have been considered, usage evidence and its blind spots are reviewed, and the release owner accepts the remaining impact. If criteria are unmet, continue support, revise the plan or explicitly change the support decision. “Nobody complained” is weak evidence.

The database has a separate migration

Adding a database column and backfilling it changes storage. Keeping startsAt in JSON preserves a client contract. They are related implementation tasks with different consumers and lifetimes. Renaming a database column does not require renaming a public field; an adapter can preserve it indefinitely.

For a storage change, consider old and new server binaries, backfill correctness, writes during rollout and rollback compatibility. For an external contract, consider installed clients and delayed requests. Complete the storage expansion without prematurely removing the public representation. The lab has no database and does not demonstrate a database migration.

7. Rollback and urgent security work

Before mobile client 2.0 ships, returning to API A may be viable for this example. After client 2.0 ships, API A lacks its required startTime. A server rollback can restore client 1.0 while breaking client 2.0. The lab includes this precise failure.

Keep a rollback-capable server build that understands both contracts, or plan a forward fix that preserves the expanded surface. Check data written under the new behavior, not only response fields. A feature flag can stop adoption of a new workflow; it cannot remove an already installed client’s dependency. Store review and user uptake mean a mobile rollback is not a synchronized server rollback.

An actively exploitable vulnerability may require disabling a behavior before every client can migrate. Assess exposure and available containment, preserve safe operations where possible, provide clear failure/recovery behavior and record who accepts the disruption. Do not retain an unsafe authorization path simply to preserve compatibility. Security urgency changes the tradeoff; it does not make the client impact disappear. Support periods and notification obligations depend on the actual product and agreements; this guide gives no legal guarantee.

8. Can we ship this API change?

  • Which exact installed client releases and offline request formats are supported?
  • Which structural, semantic and behavioral assumptions change, including errors, defaults and ordering?
  • Have we checked additions against strict readers and closed enum models?
  • Do schema checks, retained consumer contracts and representative real-client tests cover the affected paths?
  • Does the transition serve both generations with consistent meaning, including reconnects?
  • Can observability distinguish versions and reveal failures, and have we recorded its blind spots?
  • Are communication, retirement criteria, ownership and a rollback or forward-fix plan explicit?
  • If security forces disruption, is the containment and recovery decision understood?

The release decision belongs to the whole interaction: supported clients, candidate server and the migration between them. Green provider tests are useful evidence. Keeping the old client’s assumptions in view makes that evidence relevant to the people using the app.

For a real mobile delivery account, read the Tagbogen workshop report. That report describes entries stored locally without an entry server; the appointment API incident here is fictional and unrelated to Tagbogen. For retry behavior during reconnects, see our Java/Spring idempotency guide.

Research checked November 14, 2026. Official specifications and tool documentation are linked next to their claims. The support policy, migration approach and review checklist are engineering recommendations. All appointment data, client versions and failures are illustrative. Original title graphic and diagrams created for this guide; no production incident, mobile-device run or performance benchmark is claimed.