0036 - Internal API standards
| ID: | ADR-0036 |
|---|---|
| Status: | PROPOSED |
| Published: | 2026-09-22 |
Notation
This ADR uses RFC 2119 keywords (MUST, MUST NOT,
SHOULD, SHOULD NOT, MAY) deliberately. Anything marked MUST or MUST NOT is not negotiable
at team level; a team that needs an exception brings the case to the architecture group.
Internal API means a service-to-service API: one Bitwarden service calling another. Bitwarden's existing public API is out of scope and is not changing.
Context and problem statement
Service-to-service APIs are new. As server functionality is decomposed into independently deployable services, a service calling another service needs a wire contract, and no documented standard exists.
One important distinction to keep in mind as we consider various options is that the APIs we are talking about are internal APIs. We control both the service that declares them and every client that consumes them. While we do still want to adhere to the principle of "no breaking changes", we also do not want that to result in an ever-growing number of optional fields that ultimately make these contracts less clear.
In the same way we refactor our code mercilessly, we want the liberty to be able to do the same with our APIs. Especially when they are still under active development and rapidly evolving.
Said differently, we do not want the rules that restrict how public-facing APIs may evolve to restrict how, or the pace at which, we evolve internal-only APIs.
Considered options
- Adopt the existing public API conventions: apply the same shape and the same no-versioning posture to internal APIs.
- Keep the existing JSON shape and add versioning: retain today's conventions, introduce path versioning.
- Adopt JSON:API strictly: implement the specification in full,
including its media type and
relationshipsobjects, and add versioning. - Adopt JSON:API selectively: implement most of the specification but don't worry about 100% compliance, and add versioning.
Adopt the existing public API conventions
Pros
- One shape across every Bitwarden API, public and internal.
- Nothing new for engineers to learn, and no migration for code already written this way.
Cons
- Carries over the no-versioning posture, which forces additive-only evolution on APIs whose callers we control.
- Its envelope is undocumented and answers only pagination.
objectanddataexist only as properties on a response model class, so anything else the response needs to carry has nowhere defined to go. - The convention is not written down. A standard that exists only as precedent cannot be cited in review, generated from, or enforced.
Keep the existing JSON shape and add versioning
Pros
- Solves the versioning problem.
- Smallest change from current practice.
Cons
- Leaves us maintaining a proprietary standard. The questions an API standard answers (document shape, metadata, errors, pagination, filtering, sparse responses, and so on) are not Bitwarden-specific. Where a public specification already proposes answers to these common questions, our time and energy is better spent adopting it.
- The remaining gaps have to be filled by us, individually, as each one is discovered.
Adopt JSON:API strictly
Pros
- It is a public specification, not house style. Registered as a media type in 2013, now at v1.1, revised in the open, with implementations across major languages.
- It answers almost every question an API standard faces, not just the obvious ones: document shape, resource identity, pagination, filtering, sorting, sparse responses, compound documents, metadata, and errors.
- Responses standardize where to find the resource
type,id, andattributes, which enables generic tooling over the entire API surface. - Error responses are richly described to facilitate both human-readable details and a JSON Pointer that identifies the exact member of the request that failed (which, again, enables generic tooling).
Cons
- In code, model objects are typically "flat"; on the JSON:API wire, they are nested inside
dataandattributesenvelopes. A good framework will mask these envelopes from the actual classes developers work with, but a direct projection will result in request and response models that feel awkward. - Parts of the specification are a real implementation burden for little return (e.g.
relationshipsobjects, with their resource linkage and related-resource links). - Every API must advertise that it accepts and returns the
application/vnd.api+jsonmedia type, which inevitably creates problems for clients that expect to send and receiveapplication/json. Advertisingapplication/jsonwhile still acceptingapplication/vnd.api+jsonis only available to us because we are not claiming full conformance.
Adopt JSON:API selectively
Pros
- Almost all the pros of Adopt JSON:API strictly with just one con.
Cons
- Our APIs will walk like and talk like JSON:API but are not quite JSON:API, which could be surprising to a human. It is hard to imagine any negative impact to the machine that consumes them.
Decision outcome
Chosen option: Adopt JSON:API selectively with versioning.
- We believe an established standard, with thoughtful answers to every API question, will be more robust than any standard we might invent ourselves. It is widely adopted among some of the largest SaaS vendors in the industry including ART19 (an Amazon company) and Datadog.
- We feel strongly that internal APIs should be formally versioned. Without formal versioning, a change to an existing contract has to be either additive or coordinated with every consumer's release cycle, which in practice means the shape rarely changes and every new field is optional. Contracts constrained like that get weaker over time, and what we want to express eventually cannot be expressed, because we have committed ourselves to "additive changes only". This rules out adopting the existing public API conventions, which are expressly unversioned.
- We adopt the standard selectively to get most of the benefits of JSON:API without the burden of full conformance.
The specific standards, including where we deviate from JSON:API, are published here: Internal API standards. That page is the living reference: its rules evolve by pull request without superseding this decision, and this ADR is superseded only if the model itself changes.
Positive consequences
- One document shape across every internal API, so a consumer calling five services learns one error format, one pagination scheme, and one filter grammar.
- Versioning gives a contract a way to change shape. A breaking change becomes a new version with a migration, instead of more optional parameters.
- The envelope gives pagination state and other response metadata somewhere to live that is not mixed into the resource.
- Decisions we would otherwise have to make are already made, in public, by a specification that has been maturing since 2013.
Negative consequences
- Internal APIs "look different" from public APIs.
- Additional work is required to ensure the "envelopes" are largely transparent to developers when working with request and response models in code.
- Partial conformance invites any deviation from the specification to be argued as allowed. We will mitigate this by documenting the specific exceptions; we will comply with all other aspects of the specification.
Plan
- Build out the framework that makes the JSON:API "envelope tax" disappear from daily development.
- Publish a standard for authentication, authorization, and how the current organization travels with a request.
- Publish a standard for jobs, the resource a
202 Acceptedreturns.