Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
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

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.

```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!"
}
```

> Unlike the CRUD APIs, _actions_ impose no restrictions on the shape of the JSON posted.

**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. |

> A field that changes only through actions (e.g. a status whose transitions follow business rules)
> `MUST` be read-only, so that no update can bypass the action.
76 changes: 76 additions & 0 deletions docs/architecture/server/apis/advanced-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Advanced queries

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`.

:::faq[Why are we standardizing on JSON Logic instead of OData?]

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 active or invited users whose KDF iterations are below the current minimum:
Comment thread
theMickster marked this conversation as resolved.
Resolved

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

```json
{
"filter": {
"and": [
{ "<": [{ "var": "kdfIterations" }, 600000] },
{ "in": [{ "var": "status" }, ["ACTIVE", "INVITED"]] }
]
}
}
```

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.md#targeting).

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

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

1. The first element(s) 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" (e.g. `users`).
[Bulk-processing](./bulk-operations.md) APIs are an exception to this rule.
- APIs that create resources stop here.
4. If the API targets a specific resource (e.g. update or delete), 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.

:::faq[Why versioning in the path?]

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

:::

**Examples**

```text
/api/v1/users
/api/v1/users/123
/api/v1/users/123/addresses
```

Paths `SHOULD` be traversable. If `GET /api/v1/users/123/addresses/456` returns the details about
Comment thread
audreyality marked this conversation as resolved.
Resolved
address 456 of user 123, then every parent path would resolve as follows:

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

:::tip

To be clear, we are not saying every β€œparent path” must actually exist as a callable API, or even
that the current user should be authorized to call them. We are only trying to show how API paths
should be cohesive, and how they _would_ resolve if APIs existed for every parent path.

:::

## Which identifiers belong in the path

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

- The **subject** is what the request acts on (i.e. a resource or a collection of resources). It
`MUST` be named in the path.
- 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 (standard forthcoming).

**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.

:::faq[Why keep the scope out?]

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.

:::

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.

## 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 allow the caller to specify what organizations to return data for (e.g.
`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,43 @@
# Breaking changes and versioning

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. 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.

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, `SHOULD` be fixed "in place", without creating new versions of the API, even if the
Comment thread
theMickster marked this conversation as resolved.
Resolved
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).

:::faq[Why are service-to-service APIs versioned? Public APIs aren't.]

We feel strongly that service-to-service APIs should be formally versioned. 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, because we have committed ourselves to "additive changes
only". This rules out adopting the existing public API conventions, which are expressly unversioned.

:::

:::note

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.

:::
Loading