Skip to content
262 changes: 262 additions & 0 deletions docs/architecture/adr/0047-move-sdk-into-monorepo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
---
adr: "0047"
status: Proposed
date: 2026-09-22
tags: [clients, mobile, sdk, server]
---

# 0047 - Move the SDK into the clients monorepo

<AdrTable frontMatter={frontMatter}></AdrTable>

## Context and problem statement

The `sdk-internal` repository lives on its own and reaches the clients as a published npm package
(and a Maven package for android, a Swift package for iOS). Every breaking change in the SDK has to
travel through that publish step before any consumer can adopt the change that follows it. That
turns delivery into a single serialized pipeline: many teams write to the SDK, everything funnels
through one published version, and many teams consume it on the other side. One team's unfinished

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One team's unfinished downstream fix holds up everyone waiting for a later SDK change

Does this still hold true with the auto-fix workflow treated by the AI team?

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.

Yes, codeowners. With timezones that's upwards of a 24h delay assuming the owning team prioritizes review.

downstream fix holds up everyone waiting for a later SDK change.

Put another way: every breaking change in the SDK adds to a queue of fixes that then have to merge

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎨/♻️ Please consider refactoring the following sentence because it reads as throat clearing.

Put another way: every breaking change in the SDK adds to a queue of fixes that then have to merge

to

Every breaking change in the SDK adds to a queue of fixes that then have to merge

on the clients side in the same order they merged in the SDK. If Team A merges a breaking change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I want to note, that in one of our slack channels, Brian fairly clearly stated that the SDK's purpose in not only internal teams (our mobile clients and clients), but external users too. This ADR currently ignores this fully, and the solution does not help those users.

The fact is that we make too many breaking changes, because our interfaces are designed ad-hoc for our clients rather than being designed from an "i want to design a stable SDK" perspective.

@coroiu coroiu Oct 12, 2026 •

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.

I agree and also want the SDK to be the basis for something we eventually release as a "public SDK" (I'm not sure if this is actually a company goal yet though), but I don't think an "everything SDK" is how that will look, and that it will instead be released as product SDKs. As an analogy, you don't have a "Google everything API" or "Atlassian API" instead you get APIs for Google Maps, Google Search, Jira, Confluence, etc. Either way, where the code lives does not dictate what we can and can not release

The fact is that we make too many breaking changes, because our interfaces are designed ad-hoc for our clients rather than being designed from an "i want to design a stable SDK" perspective.

Yes, and I think this follows the same principles as the current server APIs, we have an internal API for our clients and we have a public API that follows separate rules when it comes to breaking changes. I envision the SDK doing the same, but I don't want to say "we can't move fast and break stuff because we have a public API that nobody currently uses"

@mzieniukbw mzieniukbw Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it will instead be released as product SDKs

Maybe we should then split the SDK into individual product SDKs ?
Or rather, since PM remains the main offering that have multiple clients, sdk-intenal would only contain PM client, while SM, PAM, (AC?) would either fork or completely separate out.
For "platform" stuff, including crypto, we could have a versioned, stable release, that many product SDKs could consume.

@coroiu coroiu Oct 12, 2026 •

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.

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.

An external facing SDK is not currently on any roadmap. Such an SDK would have a very different release cadence and would most likely need to be a wrapper to facilitate LTS.

into the SDK, every other team waits for Team A's corresponding client fix before it can pull in any
SDK change that landed after the break.

We have tried to manage this without changing the structure. The breaking-change detection workflow
builds the latest `clients` `main` against SDK PRs so a breaking change can arrive with its fix PR
ready. It has not been enough: breaking changes have gone up, not down, even with the workflow in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Breaking changes is bad metric. Rather it should be, whether the team got impacted by the breaking change. In all clients, SDK update is a separate PR that does not immediately break main. AI's auto-fix only made it easier to handle.
Unless i am missing the point here, i don't feel like "breaking changes" should be treated as a root cause problem that we need to fix.

place. Doing nothing is not a stable position, because the pressure grows as more features and more
teams move into the SDK.

A 12-month measurement (2025-09-01 to 2026-08-31, read from merged PRs and git history) shows the
shape of the problem:

- **68 forced downstream breaking events**: clients 18, android 28, iOS 22. The earlier six months

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: Now that we have the LLM-auto-fix workflow in place, does this argument still hold?

Mots changes are auto-resolved. cc @SaintPatrck

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.

I spoke with @vvolkgang regarding how this affects mobile and the impression I got was that, while AI does help with minor things, there are still a lot of changes that require human intervention, especially breaking changes that only show up during runtime. As for clients, do we have an AI job there now?

@quexten quexten Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As for clients, do we have an AI job there now?

Yes, we had it sucecssfully auto-resolve some things already.

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.

Ok nice, I haven't seen it yet and the ADR has taking way to long to get published. I think I'll have to evaluate how it performs before I can comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copying from slack:

  1. What is the nature of these breaking changes?
    I.e trivial or hard to resolve?
  2. Were they instantly resolved?
  3. How long was development actually stalled by these?

Next, most of these. are on mobile, with clients just being a minority share. 18 out of 68. This ADR only addresses clients.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a problem with lack of any enforcements, rather than a problem that requires solving.

had 25; the recent six months had 43, about 1.7x, with August the peak at 16.
- **About 20% of merges to `sdk-internal` main are breaking**: 149 of 754. Distinct authors per

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are they breaking? Have we investigated that?

To me that suggests the interfaces are bad, which is an issue regardless of the PR management.

@Hinton Hinton Oct 13, 2026 •

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 SDK owns the data model, and new features modify the data model. At the moment that causes hard breakages since the conversion is not lossy as that may introduce data loss instead it forces the consumer to decide what to do with the change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎨 / ♻️ Please consider changing this sentence/phrase:

month grew from about 14 to about 29 over the year. More contributors means more breaking changes.

to

month grew from about 14 to about 29 over the year, alongside the growth in breaking merges.

month grew from about 14 to about 29 over the year. More contributors means more breaking changes.
- **Concurrency is the norm.** Every one of the 149 breaking merges had another breaking merge
within 14 days; 143 of 149 within 3 days. A consumer picking up a new version does not adopt one
break, it adopts a bundle of unrelated breaks at once.
- **Nothing gates the publish.** npm and Maven publish on every merge to main. The breaking-change
check runs on PRs, comments, and adds a `breaking-change` label, but never fails the job (a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I often found that the clients itself did not build due to issue not related to SDK change and added breaking-change label either way.
Have we filtered those out ?

known-breaking PR, [sdk-internal#906](https://github.com/bitwarden/sdk-internal/pull/906), passed
green). So a breaking change ships to consumers whether or not its downstream fix exists.
- **Code review is not the bottleneck.** On clients, review latency is negligible and total blocked
time is small (median 2.4 hours). Mobile looks different: android and iOS adopt on a rolling
branch, with median branch windows of about 80 and 165 hours, but that window is dominated by
adaptation effort and the branch sitting open between re-bumps, not by waiting for a reviewer, and
per-break approval time cannot be cleanly separated from it. The cost across all three repos is
coordination and adaptation, not code review.
Comment on lines +48 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

but that window is dominated by adaptation effort and the branch sitting open between re-bumps

The cost across all three repos is coordination and adaptation, not code review.

With AI auto-fix on mobile and AI driven development, the adaptation and code review efforts merged together.
I think we should re-consider this ADR entirely, since the amount of adaptation effort with AI auto-fix likely reduced from hours to minutes (adopt to all clients), which feels like the main driver of the ADR (effort / impact on teams).

- Keep in mind that this is absolute time and the real cost is person-hours across the blocked

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎨 / ♻️ Please consider changing this sentence/phrase:

Keep in mind that this is absolute time and the real cost is person-hours across the blocked

to

This is absolute time. The real cost is person-hours across the blocked

teams. On repositories like `clients` that can easily mean an order of magnitude more

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎨 Please consider changing this sentence/phrase:

 On repositories like clients that can easily mean an order of magnitude more

We need a count of people or teams blocked per break on clients to back "an order of magnitude," or perhaps we drop the magnitude and say the cost multiplies by the number of blocked people. πŸ€”

person-hours.
- **The churn is in the application layer.** Breaks across the dataset are dominated by SDK-owned
surface changes (struct and field reshapes, renames, unions, new APIs), and the clients coupling
points are mostly vault, tools, admin-console, and importer code.
Comment on lines +55 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Once mobile clients fully adopt the SDK on higher level designed SDK APIs, rather than low level Uniffi split structs and functions (mostly due to mTLS issue), the amount of breaking changes would reduce significantly.
Since this is upcoming effort planned by Platform team for this year, i would recommend to take an iterative approach: migrate to higher level SDK APIs in mobile clients and re-evaluate again in mid 2027.
We might find that 80% of a breaking changes for mobile were hidden behind this limitation.


The reason to act is not the size of the problem today, it is the trend. The main driver is upstream

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right, so here we are saying "We will move sdk to clients, because we recognize we are making breaking changes, and will make more of them, so to make breaking changes less impactful, everything living in one merge is better".

I want to warn about this stance. It does not consider mobile, and external consumers of the SDK. Worse, it encourages the breaking change pattern by making breaking changes more comfortable to the team implementing the feature, without regard for external consumers, or our mobile team.

in the SDK, and it grows with the number of contributing teams, so the problem gets worse as
adoption grows.

The server relationship is a separate matter. Server changes reach the SDK through generated API
bindings, but server-owned enum-variant breaks are rare in the data (clients 0, android 1, iOS 1).
And because the app and the server ship independently, they always have to stay compatible with each
other over the wire, which no source layout changes. That is separate work, not part of this
decision.

## Considered options
Comment thread
coroiu marked this conversation as resolved.

- **Do nothing, improve iteratively:** keep the repos separate and reduce breaks at the edges (SDK

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have we done a survey as to why breaking changes are breaking? Was the interface designed wrong in the first place? Is this just regular product development?

Can we address this by having guidelines as to what stable APIs should look like?

@coroiu coroiu Oct 12, 2026 •

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.

Yes, I have https://bitwarden.atlassian.net/wiki/spaces/~62955327d9eae9006f348ae9/pages/3289186311/Breaking+changes+across+repositories and https://bitwarden.atlassian.net/wiki/spaces/~62955327d9eae9006f348ae9/pages/3340435458/SDK+breaking-change+event+log+raw+data as the basis for this PR.

Can we address this by having guidelines as to what stable APIs should look like?

You can go further than guidelines and block breaking changes in CI, at least the ones the static types can detect. That's the "Ban breaking changes" option. However you'd be paying for it by having to carry a backwards-compatible surface, and some changes can't be made additive at all. For example, cipher blob encryption (bitwarden/sdk-internal#1122) made Cipher.name optional, because blob-encrypted ciphers keep the name inside the encrypted blob. It broke clients, android, and iOS, and there was no additive way to do it. Changes like that would still need an explicit, coordinated exception.

You can also re-design interfaces and how objects are constructed, I mention this under Reduce breaks by surface design in the confluence page above:

Shape the SDK's exported surface so it is hard to break: let the SDK own construction (builders/constructors) so added fields don't break callers, make new fields optional by default, and minimise the fine-grained types clients have to construct or exhaustively match. Most breaks today come from clients hand-building SDK input structs.

but by introducing builders you are weakening static typing usefulness:

Oscar: I think another gotcha here is the lack of compile errors, meaning it’s easy to miss setting the field somewhere.
Andreas: Yeah, I feel like whatever we do we’re just pushing the slider back and forth between static safety and dynamic flexibility

@audreyality audreyality Oct 12, 2026 •

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 analysis in those docs explains the proximate causes of breaking changes. I wonder whether there's a root cause that we're missing?

  • Are folks making mistakes because they're unfamiliar with Rust?
  • Or perhaps because they don't know it's a breaking change?
  • Or is our culture surrounding breaking changes too permissive? Dismissive?
  • Is this an up-front design issue?
  • Is it due to vulns or structural bugs?

owns construction so added fields do not break callers, new fields optional by default, fewer
fine-grained types for clients to construct or match, a client-side facade).
- **Ban breaking changes (additive-only):** never change the exported surface in place; add
alongside, deprecate, reap.
- **Coordinated merge gate:** keep the repos separate, make the existing breaking-change check
enforcing, and merge a breaking SDK change together with its client and mobile fixes as one gated
set (a distributed atomic merge across repos).
- **Versioned SDK releases:** keep the repos separate, but release the SDK as its own deliverable
every two weeks (semver, a changelog of breaking changes) instead of publishing on every merge to
main, and let consumers upgrade when it suits them.
- **Product repository:** split the SDK crates. Move the application-logic crates into a product

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pretty much all of these options are much lighter and reversible compared to the monorepo, which is irreversible. Should we find out that, as claimed in other comments, the monorepo does not have the intended effect, how do we revert that descision? Can we first try other, lighter options, and measuring their success?

repository with the clients, and keep the shared, lower-level crates in a separate repository.
- **Monorepo:** move the whole SDK into the `clients` repository, the way `jslib` was folded in
before, so the SDK and the clients build together.

### Do nothing, improve iteratively

Reducing breaks at the interface is worth doing regardless, and it composes with any other choice.
On its own it does not remove the bottleneck, and it does not stop breaking changes from happening.

### Ban breaking changes (additive-only)

If nothing ever breaks, the bottleneck disappears. The cost is the overhead of carrying and later
cleaning up backwards-compatible surface, and with Rust that overhead can grow sharply with each
change. Some changes (nullability relaxations, semantic restructures) cannot be made additive and
still have to be coordinated as a real break.

### Coordinated merge gate

This is the strongest alternative that keeps the repos separate. It reaches the same "a break cannot
merge without its fixes" guarantee as the monorepo, and it covers mobile without moving any
repository. The breaking-change detection it would rely on already exists (advisory today), so
making the check enforcing is a small change, but the cross-repo merge coordination is new work we
would have to build. It also does not remove the bottleneck. A cross-repo atomic merge still has to
wait for the SDK to build on main, publish to the package registry, the clients PR to update its SDK
version, and CI to re-run on that change. And it leaves the open question of what happens to other
merges in the window between an SDK merge and its downstream fix: block them, and for how long. The
coordination logic (ordering, timing, separate CIs) is fiddly to build and maintain.

### Versioned SDK releases

Treat the SDK as its own product with a release schedule, the way we already treat client versions.
Consumers get a known set of changes every two weeks and upgrade on their own schedule.

This is a valid option, but it does not fit how we work today. The bottleneck is between the SDK and
the clients. Many of the developers who work in the clients now also work in the SDK, because they
are moving features from TypeScript into Rust. A release cadence puts a version gap in the middle of
their own work, so a developer has to wait for a release before they can use their own SDK change in
the client. It also does not remove the bundling. Breaking merges land close together (143 of 149
within 3 days), so each release would carry a bundle of unrelated breaks, and adopting it would take
PRs with effort from several teams.

Mobile is in a different position. Mobile is also broken by SDK changes, but the cost there is
adaptation, not waiting. Mobile developers do not contribute to the SDK at the same scale, so they
rarely need to cross the gap to ship their own work, and they already adopt published versions when
it suits them, sometimes days or weeks later. In practice mobile already consumes the SDK this way.
Their problem is learning what changed, not the gap itself.

### Product repository

Split the crate graph instead of folding all of it in. The application-logic crates (for example
`bitwarden-vault`, `bitwarden-send`, `bitwarden-generators`, `bitwarden-exporters`) move into a
product repository and compile together with the clients that consume them, while the shared,
lower-level crates (for example `bitwarden-crypto`, `bitwarden-core`, `bitwarden-encoding`,
`bitwarden-state`) stay in a separate repository. This keeps the atomic-commit guarantee for the
application layer, where most of the breaks come from, but leaves the clients consuming the shared
crates across a version boundary. The trade-off is that it keeps that boundary, part of the overhead
this decision is trying to remove, to prepare for a split we may never need. It remains an option we
can revisit later if that need becomes real.

### Monorepo

Fold the whole SDK into the `clients` repository, the way `jslib` was folded in before. A change to
the SDK lands in the same commit as the consumer updates it requires, and main is never broken,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎨 Please consider changing this sentence/phrase:

the SDK lands in the same commit as the consumer updates it requires, and main is never broken,

to

the SDK lands in the same commit as the consumer updates it requires, and main keeps building,

because a change that broke a consumer would break CI. The cross-repo breaking change stops being a
separate thing to coordinate, the same way changing a class and its callers in one commit is one
change, and the concurrent-bundle problem goes away by construction. The crates are still published
as versioned packages that other consumers, such as the server, depend on; only the in-repo clients
drop the version boundary. The cost is a large one-time migration and a bigger repository.
Comment on lines +149 to +150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

only the in-repo clients drop the version boundary

⚠️ The biggest cost is compilation time and as a result dev's productivity. Should be mentioned in negative consequences.
Rust is known to be relatively slow to compile, hence additional 10-15 minutes compilation time every single time SDK changes is huge cost. In practice, that's every day. This will result in significant wait time to run lint, run tests or run client. All depends on he WASM build, which is not light.


## Decision outcome

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ We have been working lately to be more concise with the decision as simply the choice.

Please read through this and consider what's restated that can be removed; or needs to be moved to the plan; or belongs in the Jira/Confluence document that is the actual work. If that Jira/Confluence section doesn't exist, then one suggestion is to add that to the PR description as a follow-up things that will happen if the ADR is accepted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please see the last 4-5 ADRs that have been merged and accepted (starting with maybe the diagraming one) for potential ideas about what the direction has been as of late with this section in particular.


Chosen option: **Monorepo**. Fold the whole SDK into the `clients` repository.

Once the SDK and the clients build from the same source, there is no published package version
between them to keep in sync. A change to the SDK and the client code that uses it becomes one
change that only merges if it builds. The cross-repo breaking change stops being a separate thing to
Comment on lines +157 to +158

@mzieniukbw mzieniukbw Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A change to the SDK and the client code that uses it becomes one change that only merges if it builds.

This negatively impacts the PR, since both SDK and TS changes might have to bundled together. This would severely increase PR sizes and time required for a team to adopt them due to increased amount of dependent teams.
Teams might get discouraged by this (like KM team), and effectively will split the SDK and TS code, so neither breaks each other.
This is effectively the same as banning breaking changes in practise.

coordinate, and the concurrent-bundle problem goes away by construction.

The SDK stays its own project inside `clients`, with its own crates, ownership, and published
packages for consumers outside the repository.

The monorepo covers the SDK and the clients. Android and iOS stay in their own repositories and keep

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As per above, this will not solve the issue for iOS/Android and possibly will make it worse.

consuming the SDK as published packages. Code that the same people change together belongs in one
repository, and a versioned boundary fits where different teams own each side. The SDK and the
clients are changed together by the same product teams, often as part of the same feature. The
mobile apps are built by the mobile team, which consumes the SDK on its own schedule.

Moving mobile into the monorepo would also cause problems. CI would block an SDK change until the
mobile apps build, so every developer changing the SDK would have to learn the mobile toolchains,
Swift, and Kotlin before they can merge. That friction pushes people to rush fixes in code they do
not know, and the mobile team prefers to review changes to its apps in its own repositories. It
would also not catch runtime or behavior changes, which the mobile team names as a large part of its
problem.

Against the product repository, the difference is where the SDK source lives and how much of the
version boundary the clients still cross. Both layouts publish the SDK crates as versioned packages,
so external consumers like the server depend on them the same way. The product repository splits the
source across two repositories, which leaves the clients consuming the shared crates across a
version boundary, part of the same bottleneck, to prepare for a split we may never need. The
monorepo removes that boundary for the clients as well, and it does not make a later split any
harder: extracting a second repository stays a normal refactor if that need ever becomes real. The
product repository is recorded above as an option we can revisit then.

Against the coordinated merge gate, the difference is that this removes the serialized pipeline
rather than automating around it. The merge gate aims for the same "cannot merge unfixed" guarantee,
but it keeps the publish-and-bump round trip and leaves the unanswered "what do we do in the window"
question. Building from the same source solves both.

Against versioned SDK releases, the difference is who sits on each side of the boundary. A release
cadence fits a boundary between two teams, which is the situation for mobile. Between the SDK and
the clients it would put a two-week gap in the middle of a single team's work.

A public SDK does not change this. To be useful it has to expose product operations like vault and
send access, which is application logic, so like the client apps it is a consumer of the product,
not a shared crate. Its feasibility is unaffected by this decision.

The upfront migration is real work, but it is one-time work, and we have done a version of it before

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️/🎨 This paragraph reads as a repeat of several places; mostly in the positive and negative consequences. Let's consider removing it.

when `jslib` moved into `clients`. What it buys is an environment where we can write changes as
often as we want without carrying a compatibility burden or waiting on a cross-repo pipeline,
weighed against a coordination cost that otherwise grows with every new team contributing to the
SDK.

### Positive consequences

- Breaking changes between the SDK and the clients stop being cross-repo events. A change to the SDK

@quexten quexten Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would consider this bad framing / false. Yes, one client set is prevented from breaking. External clients and mobile will still break, and if we don't change the fact that we are making breaking changes, all we are doing here is enabling more frequent breaking changes, which is worse for both mobile and external consumers compared to now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With this stance, I would not consider that we can offer the SDK as something external customers can use.

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.

clients here being the clients in the clients repo, not in the general sense. Yeah, it's confusing, I should probably change this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Even if this referred to the clients repo, i would hold the same stance of framing this as a benefit being false framing, specifically because the benfit directly means us encouraging more breaking changes by making them easier.

What I'm saying is that this "benefit" is not a benefit but a downside, when viewed in context.

and the client code that uses it is one commit that only merges if it builds, so main stays green
by construction and there is no published version to bundle.
- Components that are meant to work together share one build graph and one place to reason about
them. AI review, for example, sees the SDK change and the client code that calls it in the same
diff, instead of an opaque package-version bump with a new API.
- The SDK crates are still published as packages, so the server and any future product keep
depending on them regardless of this layout.
- One-time migration cost, with a precedent (`jslib`), against a coordination cost that otherwise
grows with the number of contributing teams.

### Negative consequences

@audreyality audreyality Oct 12, 2026 •

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.

πŸ€” A few additional negative consequences:

  • Rust's build output is very large (multiple gigabytes) and people with many working copies will have a copy of all build artifacts in every repository.
  • Rust has long build times. People with a worktree-based workflows (including claude code run with --worktree <desc>) will experience on every worktree.
  • This encourages people to make breaking changes to the client interfaces by lowering its cost. We may see the rate of breaking changes to client interfaces increase as a result (The Jevons paradox).


- A large upfront migration.
- A bigger repository: git operations and CI get slower unless CI is scoped to build only the
projects affected by a change. Folding a Rust build graph into the `clients` Nx/TypeScript
monorepo is real build-system work.
- It does not remove the adaptation work. Changing a shared type still means updating every place
that uses it; co-locating turns that into one commit instead of a cross-repo sequence, but the
adaptation itself is no smaller.
- It does not touch compatibility between deployed apps and the server. They ship independently, so
they always have to stay compatible with each other, and that is separate work regardless of how
the source is organized.
- It does not solve mobile's problems. Android and iOS still take SDK breaks across a
published-package boundary, and the runtime and behavior changes that hurt mobile the most cannot
be detected with today's tools. With the SDK inside `clients`, it can also become easier to forget
that mobile depends on it.

### Plan

- Fold `sdk-internal` into the `clients` repository, following the `jslib` precedent, so the SDK and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copying out of DM; this is missing a few steps:

  • How are skills handled?
  • How are claude reviews handled? Do we have one reviewer per sub project somehow?
    • The context is too much and too different to fit across the whole repo

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.

Definitely a good point to look into!

When it comes to the context, we could have different reviewers for each language package, but having one reviewer for the entire thing also has it's benefits, like giving the agent the opportunity to review the entire feature from start to finish

the clients build together.
- Scope CI so a change builds and tests only the projects it affects, to keep the larger repository
workable.
- Keep crates buildable as packages that out-of-repo consumers, such as the server, can depend on.
Clients consume the crates in-repo with no version boundary; external consumers take them as
published, versioned packages.
- Draw package boundaries where product scope matters, for example splitting policies into a
suite-wide package the server and other products can depend on, and PM-specific pieces that stay
with the application.
- Keep publishing the Maven and Swift packages that android and iOS consume.
- Improve the boundary to mobile: enforce breaking-change notes on SDK changes, keep the android
breaking-change detection, and add iOS to it.
- Look into automatically generated mobile fixes together with the mobile team. These will not be a
merge requirement: requiring a working or merged mobile PR before an SDK change can merge would
bring back the cross-repo blocker this decision removes.
- Work closer with the mobile team, and address problems at the boundary as we run into them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❓ Who is working closer with the mobile team?


## Follow-ups

- **The server's dependency on the SDK.** `server` currently depends on `sdk-internal` for
cryptographic and policy code. Folding the SDK into `clients` should not turn that into a
Comment on lines +257 to +258

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.

I don't think the SDK has been added to server yet. If it is, it's not used for policy code. (I did mention this as a future possibility, but no more than that.)

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.

It's used for seeding.

dependency on the `clients` repository. Read as packages, the server depends on the crypto and
policy packages wherever they live, published from the monorepo, not on the repository. Whether
the server later joins the monorepo or keeps consuming those packages is open, as is compatibility
with the backend MSA direction.