Skip to content
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
25d4aef
docs: Add service-to-service API standards
mike-gorman-bitwarden Oct 15, 2026
2ddfcf0
docs: Restore examples and details from the draft standards page
mike-gorman-bitwarden Oct 15, 2026
a4e73f8
docs: Order standards sidebar alphabetically and group contents
mike-gorman-bitwarden Oct 15, 2026
ab4645e
docs: Flatten the standards table of contents
mike-gorman-bitwarden Oct 15, 2026
2e28cfc
docs: Shorten the standards folder name to soa
mike-gorman-bitwarden Oct 15, 2026
6a35bcf
docs: Move server architecture to Service-oriented Architecture with …
mike-gorman-bitwarden Oct 15, 2026
107a68b
docs: Align API standards with the Confluence source
mike-gorman-bitwarden Oct 19, 2026
2c55b84
docs: Say API paths "follow the pattern" rather than "be like"
mike-gorman-bitwarden Oct 19, 2026
82d661b
docs: Explain why operations outside CRUD are modeled as actions
mike-gorman-bitwarden Oct 19, 2026
2fa1eb3
docs: Show answers to anticipated questions as FAQ panels
mike-gorman-bitwarden Oct 19, 2026
db0bf7f
docs: Restore payload shape page with envelope and resource fields se…
mike-gorman-bitwarden Oct 19, 2026
cdc76da
docs: Restore API standards feedback incorporated in earlier PRs
mike-gorman-bitwarden Oct 19, 2026
e20fa79
docs: Keep API standards under Server Architecture
mike-gorman-bitwarden Oct 19, 2026
9deef72
docs: Rename API standards to APIs and clarify scope
mike-gorman-bitwarden Oct 19, 2026
f789188
Update docs/architecture/server/apis/jobs.md
mike-gorman-bitwarden Oct 20, 2026
a8f050a
docs: Show nested email addresses in payload shape examples
mike-gorman-bitwarden Oct 20, 2026
dc95e26
docs: Add reasons for always including id and type
mike-gorman-bitwarden Oct 20, 2026
d11c32f
docs: Require documented defaults for page number and size
mike-gorman-bitwarden Oct 20, 2026
ac7f3a0
docs: Default page number to 1
mike-gorman-bitwarden Oct 20, 2026
ae424d3
Apply batched suggestions from code review
mike-gorman-bitwarden Oct 20, 2026
5b3ffdb
docs: Explain when it is safe not to page
mike-gorman-bitwarden Oct 20, 2026
ce55ad1
Merge remote-tracking branch 'origin/main' into docs/service-to-servi…
mike-gorman-bitwarden Oct 20, 2026
5fb5550
docs: Rewrap payload shape envelope rule to fix lint
mike-gorman-bitwarden Oct 21, 2026
5191cfd
docs: Spell out key derivation function in advanced queries example
mike-gorman-bitwarden Oct 23, 2026
2a913ab
docs: Require bugs, including security issues, to be fixed in place
mike-gorman-bitwarden Oct 23, 2026
a079fba
docs: State the audience of the APIs standards
mike-gorman-bitwarden Oct 23, 2026
d90e4f3
docs: Write out contractions in the APIs standards
mike-gorman-bitwarden Oct 23, 2026
138a32a
docs: Link ADR-0039 from the APIs index
mike-gorman-bitwarden Oct 23, 2026
28b76ed
docs: Link ADR-0035 and ADR-0036 to the published API standards
mike-gorman-bitwarden Oct 23, 2026
20336c4
docs: Scope the read-only rule and reject version in bulk updates
mike-gorman-bitwarden Oct 23, 2026
375a886
docs: Count returning fewer results as a breaking change
mike-gorman-bitwarden Oct 23, 2026
c440d75
docs: Use a cursor before the first result in the cursor paging example
mike-gorman-bitwarden Oct 23, 2026
9dc5649
docs: Say what the partial updates link to the JSON page points at
mike-gorman-bitwarden Oct 23, 2026
6fe9770
docs: Lead the unrecognized input page with its rules
mike-gorman-bitwarden Oct 23, 2026
6b5200d
docs: Lead the APIs index with notation and point each page to it
mike-gorman-bitwarden Oct 23, 2026
bded71d
docs: Replace parenthetical, semicolon, and dramatizing asides
mike-gorman-bitwarden Oct 23, 2026
850899a
docs: Move 500 response rules out of the errors note
mike-gorman-bitwarden Oct 23, 2026
9fcc239
docs: Move rationale into numbered notes or attach it with because
mike-gorman-bitwarden Oct 23, 2026
07fcfa0
docs: Model examples on existing Bitwarden data and cite the source
mike-gorman-bitwarden Oct 23, 2026
d0a4ec7
docs: Split bulk operations into one page per topic
mike-gorman-bitwarden Oct 23, 2026
c870f42
docs: Name date fields with a Date suffix
mike-gorman-bitwarden Oct 23, 2026
ff15d1e
docs: Suffix reference fields with Id
mike-gorman-bitwarden Oct 23, 2026
608dfc8
docs: Mark the undefined job resource as an info note
mike-gorman-bitwarden Oct 23, 2026
7ed0d24
Revert "docs: Mark the undefined job resource as an info note"
mike-gorman-bitwarden Oct 23, 2026
fb52e2b
docs: Base the advanced query example on PBKDF2 users
mike-gorman-bitwarden Oct 24, 2026
47caafa
docs: Cover hidden resources in bulk error statuses
mike-gorman-bitwarden Oct 24, 2026
41400f9
docs: Require request order only for bulk create results
mike-gorman-bitwarden Oct 24, 2026
65a6270
docs: Pair each per-resource bulk action body with its target id
mike-gorman-bitwarden Oct 24, 2026
3a1f5d9
docs: Point reactive service-to-service work toward messaging
mike-gorman-bitwarden Oct 26, 2026
94a5f62
Merge remote-tracking branch 'origin/main' into docs/service-to-servi…
mike-gorman-bitwarden Oct 27, 2026
3f03637
docs: Say each API is versioned independently of its parent paths
mike-gorman-bitwarden Oct 27, 2026
a8672e2
docs: Say the APIs borrow REST vocabulary but are not RESTful
mike-gorman-bitwarden Oct 27, 2026
c0f5ba4
Merge remote-tracking branch 'origin/main' into docs/service-to-servi…
mike-gorman-bitwarden Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Iterm
Jetpack
jslib
jumpcloud
kebab
keychain
keypair
keyserver
Expand Down
10 changes: 5 additions & 5 deletions docs/architecture/adr/0035-service-oriented-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@ The rules:
1. Services `MUST` document their APIs in [OpenAPI format](https://www.openapis.org/). A
**service-to-service API** β€” one whose callers are other Bitwarden services, rather than the
clients and third parties that call the private and public APIs today β€” `MUST` additionally
conform to the forthcoming **Service-to-Service API Standards**.
conform to the [Service-to-Service API Standards](../server/apis/index.md).
1. Services `SHOULD NOT` make breaking changes.
- If the changes that need to be made _would_ be breaking to a service-to-service API, such
services `SHOULD` follow the API versioning process as outlined by the forthcoming
Service-to-Service API Standards.
services `SHOULD` follow the API versioning process as outlined by the
[Service-to-Service API Standards](../server/apis/breaking-changes-and-versioning.md).
- Fixing bugs, including security issues, are not subject to this rule and `MUST` be fixed "in
place".
1. Services `MUST` provide a **service client** for consumers.
Expand Down Expand Up @@ -185,8 +185,8 @@ The rules:

### Plan

- Publish the **Service-to-Service API Standards**. Rules 4 and 5 reference it normatively and it
does not exist yet on this site.
- Publish the [Service-to-Service API Standards](../server/apis/index.md), which rules 4 and 5
reference normatively.
- Publish a client strategy page covering how a service client is generated, wrapped, versioned, and
cached.
- Publish a service identity and context page covering service-to-service authentication,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ path. To be useful, the standards have to be comprehensive. At a minimum, they n
Chosen option: **Documented standards**.

Service-to-service APIs `MUST` conform to a comprehensive, documented set of standards covering, at
a minimum, the topics listed above. The standards will be published separately as a living
reference: their rules evolve by pull request without superseding this decision, and this ADR is
superseded only if the decision to have documented standards itself changes.
a minimum, the topics listed above. The standards are published separately as a
[living reference](../server/apis/index.md): their rules evolve by pull request without superseding
this decision, and this ADR is superseded only if the decision to have documented standards itself
changes.

### Positive consequences

Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/server/apis/_category_.yml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 The category is very long. I'd consider whether introducing a second grouping, consistent with the TOC, would help with organizing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't know what you mean by "The category is very long."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the child pages?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the page list within the "apis" category has a ton of entries in it. It would be helpful if the grouping in the sidebar reflected the grouping in the index.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
label: "APIs"
position: 1
37 changes: 37 additions & 0 deletions docs/architecture/server/apis/acting-upon-resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Acting upon resources

Notation follows the [APIs index](./index.md#notation).

Operations that do not fit creating, reading, updating, or deleting a resource are exposed as
**actions** on the resource. Modeling each operation as a distinct API makes it easy to discover,
and lets it be authorized independently of the standard update.

- HTTP verb `MUST` be `POST`.
- API path `SHOULD` follow the pattern `/api/v1/{resource plural}/{id}/actions/{action}`, with the
action named in kebab-case.
- Unlike the CRUD APIs, _actions_ impose no restrictions on the shape of the JSON posted.
- A field that changes only through actions, such as a status whose transitions follow business
rules, `MUST` be read-only, so that no update can bypass the action.

```http
POST /api/v1/users/62bed180-1f78-45d4-8a56-c996936a2947/actions/send-email
Accept: application/json
Content-Type: application/json
```

```json
{
"subject": "Hello",
"body": "World!"
}
```

**Success responses**

Upon success, APIs `SHOULD` return `204` but `MAY` return `200` or `202`.

| Status | Description | Response |
| ---------------- | -------------------------- | -------------------------------------------------------------- |
| `200 OK` | The action was successful. | The latest representation of the resource. |
| `202 Accepted` | The action is scheduled. | The [job](./jobs.md) that was scheduled to execute the action. |
| `204 No Content` | The action was successful. | Nothing. |
73 changes: 73 additions & 0 deletions docs/architecture/server/apis/advanced-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Advanced queries

Notation follows the [APIs index](./index.md#notation).

The `filter` query parameters cover simple predicates, but they cannot express arbitrary `AND` and
`OR` combinations, and a long expression will exceed the practical limit on URL length. APIs that
need richer queries `MUST` expose a **search** operation that carries the expression in the request
body.

- HTTP verb `MUST` be `POST`.
- API path `MUST` follow the pattern `/api/v1/{resource plural}:search`.

The expression is written in [JSON Logic](https://jsonlogic.com/) and placed in a top-level `filter`
field. JSON Logic is adopted for its notation only. Its full operator set includes control flow,
iteration, and arithmetic, none of which belong in a query, so only the following operators are
supported:

| Operator | Meaning |
| -------------------- | --------------------------------------------------- |
| `var` | Reference a field of the resource. |
| `==`, `!=` | Equal, not equal. |
| `>`, `>=`, `<`, `<=` | Numeric and date comparison. |
| `in` | Membership in a list, or a substring match. |
| `and`, `or` | Boolean composition, each taking two or more terms. |
| `!` | Negation. |

A request that uses any other operator, or references a field the API does not support, `MUST` be
rejected with `422`.

JSON Logic is used rather than OData's `$filter` because:

1. OData's `$filter` is a string expression language, so both building and parsing it require string
handling where JSON is already right there.
2. Composing a filter by concatenating strings means quoting and escaping, which is how injection
bugs happen. Composing a JSON object does not.
3. OData brings a great deal more than filtering - metadata documents, `$expand`, batch, its own
conventions - and we only want the filtering.

**Example**

Find premium users whose key derivation function (KDF) iterations are below the current minimum:
Comment thread
mzieniukbw marked this conversation as resolved.
Resolved

```http
POST /api/v1/users:search?page[size]=50&sort=-creationDate
Accept: application/json
Content-Type: application/json
```

```json
{
"filter": {
"and": [{ "<": [{ "var": "kdfIterations" }, 600000] }, { "==": [{ "var": "premium" }, true] }]
}
}
```

The response `MUST` be identical in shape to the equivalent
[read-many](./reading-resources.md#read-many) request.

[Learn more about JSON Logic](https://jsonlogic.com/)

**Success responses**

Upon success, APIs `MUST` return `200`.

| Status | Description | Response |
| -------- | -------------------------- | ----------------------------------------- |
| `200 OK` | The search was successful. | The matching resources, paged and sorted. |

The same notation selects resources for [bulk operations](./bulk-operations/targeting.md).

The `:search` syntax is credited to [Google AIP-136](https://google.aip.dev/136), and the query
notation to [JSON Logic](https://jsonlogic.com/).
101 changes: 101 additions & 0 deletions docs/architecture/server/apis/api-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# API paths

Notation follows the [APIs index](./index.md#notation).

API paths `MUST` use **lowercase "kebab-case"** and conform as follows:

1. The first one or more elements of the API path `MUST` be a namespace. By default, the namespace
`SHOULD` be `api`.
2. The next element of the API path `MUST` be the version number starting with `v1`.
3. The next element of the API path `MUST` specify the resource or resources it targets. For
resource-oriented APIs, this element `SHOULD` be the "resource plural", such as `users`.
[Bulk-processing](./bulk-operations/index.md) APIs are an exception to this rule.
- APIs that create resources stop here.
4. If the API targets a specific resource, as an update or delete does, the next element of the API
path `SHOULD` be the ID of the resource it targets.
5. Additional elements `MAY` be specified to target sub-resources.

**Examples**

```text
/api/v1/users
/api/v1/users/123
/api/v1/users/123/devices
```

Paths `SHOULD` be traversable. If `GET /api/v1/users/123/devices/456` returns the details about
device 456 of user 123, then every parent path would resolve as follows:

- `GET /api/v1/users/123/devices` should return all devices of user 123.
- `GET /api/v1/users/123` should return details about user 123.
- `GET /api/v1/users` should return all users.

Not every parent path has to exist as a callable API, and a caller is not necessarily authorized to
call one that does. Traversable paths show how paths fit together, and how they _would_ resolve if
APIs existed for every parent path.

:::note

2. The version is in the path because it is then visible in logs, traces, routing rules, and curl
commands, needs no content negotiation to read, and lets two versions coexist behind one host.
Header and media-type versioning are both defensible but harder to operate.

:::

## Which identifiers belong in the path

Every request targets a **subject** and runs inside a **scope**:

1. The **subject** is what the request acts on, which is a resource or a collection of resources. It
`MUST` be named in the path.
2. The **scope** is what the request runs inside - the current organization, the current user. It
`MUST NOT` be named in the path. It travels as context.

That is the whole of the path rule. A path says what is acted on or fetched. It does not say what a
call returns: which parts of the scope an operation requires, permits or refuses is declared by the
operation, and belongs to authentication and authorization, which a forthcoming standard will cover.

**Examples**

| Path | Subject |
| --------------------------- | ------------------------ |
| `GET /api/v1/users` | users |
| `GET /api/v1/users/123` | user 123 |
| `GET /api/v1/users/current` | the user the scope names |

`current` `SHOULD` be used where the subject is whatever the scope names, rather than repeating an
identifier the caller has already supplied.

Callers `SHOULD` send the current organization and the current user on every request where each
exists. Two services may publish the same path and declare different things about the scope - one
`GET /api/v1/organizations` requiring a user and returning theirs, another requiring none and
returning all. Both name the same subject. The path is not what distinguishes them.

:::note

2. The scope stays out of the path because an identifier in a path is something a caller can try
other values of, and a path that accepts one must prove on every request that this caller may use
this value. A scope taken from context is established once, where the caller is authenticated. It
also keeps one path for a resource that can belong to more than one kind of owner.

:::

## Working across several scopes

A caller that reaches several organizations does not turn the organization into a subject, nor does
it necessarily mean one call per organization. Options include:

- If it is somewhat rare and typically only "a few", then making N calls with a different
organization in context is probably fine.
- The API may be enhanced to let the caller specify which organizations to return data for, as in
`GET /api/v1/users?filter[organization]=123,456,789`.
- An [advanced query](./advanced-queries.md) may be used where the list travels in the body rather
than the URL.

An operation can also be declared so that supplying less scope means more.

For example, `GET /api/v1/users` called with a user and an organization in context returns every
user within the specified organization, but that _same_ API called with only a user in context might
return every user for every organization the user is a member of. Whether it behaves that way is the
operation's declaration rather than the path's - and the option exists without any of these patterns
putting the organization in the path.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Breaking changes and versioning

Notation follows the [APIs index](./index.md#notation).

APIs `SHOULD NOT` make **breaking changes**. A breaking change is any change that causes a request
that is valid today to be rejected tomorrow, or a response that a caller can process today to become
unprocessable or incomplete. In practice, that means:

1. Adding a new, required field.
2. Making an optional field required.
3. Changing the datatype of a field.
4. Adding additional constraints to a field.
5. Removing a field from a response.
6. Returning fewer results for the same request, such as introducing paging or lowering a default
page size.

The verb is `SHOULD NOT` rather than `MUST NOT` because these are service-to-service APIs and we own
every caller. A team `MAY` make a breaking change in place when it can account for every caller -
either because the change has been coordinated with them, or because the rejection surfaces
somewhere the caller or the user can act on it.

Bugs, however, including security issues, `MUST` be fixed "in place", without creating new versions
of the API, even if the changes would technically be considered breaking changes.

Otherwise, if changes need to be made that _would_ be breaking changes, a new version of the API
`MUST` be created and the old one [deprecated](./deprecation.md).

Service-to-service APIs are versioned, unlike the existing public API, because without formal
versioning every change must be additive, which means the shape can never change and every new field
is optional. Contracts constrained like that get weaker over time, and what we _want_ to express
eventually cannot be expressed.

## Adding values to constrained fields

Adding a value to a constrained field deserves a second look. It is additive, so it is not a
breaking change by the definition above, and a caller that treats the field as an open string is
unaffected. But a generated client that deserializes the field into a closed enumeration will fail
on a value it has never seen - and it will fail at the client, on a change that looked safe from the
service. Consider whether the callers of that field are tolerant of unknown values before adding
one.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# All-or-nothing and best-effort

Notation follows the [APIs index](../index.md#notation).

- **All-or-nothing:** no resource changes unless every resource succeeds. If any resource fails, the
request fails and the response reports every failure, not just the first.
- **Best-effort:** each resource succeeds or fails on its own. One resource's failure `MUST NOT`
prevent or undo another's success.

A service `MAY` support either mode or both. Callers choose with the `atomic` query parameter:
`atomic=true` for all-or-nothing, `atomic=false` for best-effort.

- A service that supports both modes `MUST` default to all-or-nothing.
- Every bulk API `MUST` recognize `atomic`, even if it supports only one mode, and `MUST` reject
with `422` a value it does not support. Ignoring the parameter would change the outcome rather
than merely do less - see
[Unrecognized fields, query parameters, and headers](../unrecognized-fields-query-parameters-and-headers.md).
- Problems with the request itself - a malformed `filter`, too many items, both `ids` and `filter` -
are not per-resource failures. They reject the whole request in either mode, before any resource
is processed.

**Example**

```http
POST /api/v1/users:bulk-delete?atomic=false
```
64 changes: 64 additions & 0 deletions docs/architecture/server/apis/bulk-operations/bulk-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Bulk actions

Notation follows the [APIs index](../index.md#notation).

Bulk actions are the equivalent of invoking an [action](../acting-upon-resources.md) upon each
targeted resource.

- API path `MUST` follow the pattern `/api/v1/{resource plural}:bulk-{action}`, where `{action}` is
the name of the single-resource action - e.g. `/api/v1/users:bulk-send-email` for
`/api/v1/users/{id}/actions/send-email`.
- Because bulk actions share a namespace with the other bulk operations, actions `MUST NOT` be named
`create`, `update`, `replace`, or `delete`.
- `data` holds exactly what the single-resource action would accept as its body, and is applied to
every targeted resource. It `MAY` be omitted if the action takes no body.

**Example**

```http
POST /api/v1/users:bulk-send-email
Accept: application/json
Content-Type: application/json
```

```json
{
"ids": ["62bed180-1f78-45d4-8a56-c996936a2947", "cacba8c1-29fa-4018-8950-acd400ec76b7"],
"data": {
"subject": "Hello",
"body": "World!"
}
}
```

## Input that differs per resource

Actions that require per-resource bodies specify the data as an array.

- It `MUST` be used with `ids`, not `filter`, and `MUST` be the same length. `data[n]` is the body
Comment thread
mzieniukbw marked this conversation as resolved.
Resolved
for `ids[n]`.
- A failure is reported against `/data/{n}`.

**Example**

```http
POST /api/v1/users:bulk-send-email
Accept: application/json
Content-Type: application/json
```

```json
{
"ids": ["62bed180-1f78-45d4-8a56-c996936a2947", "cacba8c1-29fa-4018-8950-acd400ec76b7"],
"data": [
{
"subject": "Hello.",
"body": "Hi, John!"
},
{
"subject": "Hello",
"body": "Hi, Mary!"
}
]
}
```
Loading