Adding new functionality
This guide walks through the steps for adding new functionality to the SDK. For guidance on whether something belongs in the SDK at all, see What belongs in the SDK.
The examples below use FoldersClient from bitwarden-vault as a reference. For guidance on how to
organize the files within a client, see Client patterns.
1. Readiness checklist
Before creating a new crate or adding to an existing one, work through these questions to identify any blockers or prerequisites.
-
Does the functionality depend on other code that is not yet in the SDK? Moving it is not recommended until those dependencies are available. Consider asking the team that owns the upstream code to migrate it first.
-
Does the functionality require authenticated API requests? The SDK supports authenticated requests through autogenerated bindings. See
bitwarden-vaultas an example of a crate that makes authenticated calls. -
Does the functionality require persistent state? Review the docs for
bitwarden-stateand seebitwarden-vaultfor an example of how state is managed. -
Does the functionality need the SDK to produce an observable or reactive value? Migrate the business logic to the SDK and build reactivity on top of it in TypeScript.
2. Create the crate
When the functionality warrants its own crate — typically when it represents a distinct domain — add
a new crate under the crates/ directory in the
SDK repository.
- Create the crate with
cargo initand add it to the workspaceCargo.toml. - Add
bitwarden-coreas a dependency for the shared runtime. - Configure
CODEOWNERSto ensure the appropriate team is assigned to review changes to the crate.
3. Define the client struct
Each feature crate exposes one or more client structs that group related operations. Create a struct
that holds the dependencies the client needs and use the #[derive(FromClient)] macro to
automatically populate fields from the SDK Client. See
Client patterns — FromClient and dependency injection
for details on how this works and which dependency types are available.
#[derive(FromClient)]
pub struct FoldersClient {
pub(crate) key_store: KeyStore<KeySlotIds>,
pub(crate) api_configurations: Arc<ApiConfigurations>,
pub(crate) repository: Option<Arc<dyn Repository<Folder>>>,
}
If the client will be exposed over WASM, annotate it with
#[cfg_attr(feature = "wasm", wasm_bindgen)].
4. Wire into the application interface
Connect the feature client to the SDK Client by defining an
extension trait. This makes the feature accessible without
modifying Client itself.
pub trait VaultClientExt {
fn vault(&self) -> VaultClient;
}
impl VaultClientExt for Client {
fn vault(&self) -> VaultClient {
VaultClient::new(self.clone())
}
}
While there is a team called vault, VaultClient refers to the vault-domain, not the team. You
should not create team-clients in the SDK. Instead, organize clients by domain or feature area, and
assign ownership to the team that maintains that domain or feature.
For larger domains, the application interface client delegates to sub-clients rather than
implementing every method itself. For example, VaultClient exposes FoldersClient,
CiphersClient, and others through accessor methods:
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl VaultClient {
pub fn folders(&self) -> FoldersClient {
FoldersClient::from_client(&self.client)
}
}
Finally, expose the new client in the application interface entry point so consumers can reach it.
For the Password Manager SDK, this means adding an accessor method to
PasswordManagerClient:
impl PasswordManagerClient {
pub fn vault(&self) -> bitwarden_vault::VaultClient {
self.0.vault()
}
}
Steps 2–4 (create crate, define client, wire into application interface) should be submitted as a single pull request. Keep it small and focused on scaffolding — the Platform team reviews additions to the application interface clients, so a narrow scope helps move that review along quickly.
5. Implement methods
With the crate scaffolding merged, add methods in subsequent pull requests. Each method should own its logic directly on the client struct — avoid thin passthroughs to free functions. For larger clients, split methods into separate files or subdirectories as described in Client patterns.
Every public method on a client is a contract with consumers and must have test coverage. Treat client methods as a public API boundary — changes to their behavior can break downstream consumers across multiple platforms. See Client patterns — Testing for how to set up test doubles.
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl FoldersClient {
pub async fn get(&self, folder_id: FolderId) -> Result<FolderView, GetFolderError> {
let folder = self
.repository
.require()?
.get(folder_id)
.await?
.ok_or(ItemNotFoundError)?;
Ok(self.key_store.decrypt(&folder)?)
}
}
Consumers access the feature through the application interface:
let folders = client.vault().folders().list().await?;
6. Add mobile bindings
If the new functionality needs to be available on mobile platforms (Android / iOS), add a
UniFFI wrapper in the bitwarden-uniffi crate.
Expose the client
Add an accessor method on the appropriate UniFFI client — typically in
bitwarden-uniffi/src/lib.rs or a sub-client — that returns a new wrapper struct:
impl Client {
pub fn vault(&self) -> Arc<VaultClient> {
Arc::new(VaultClient(self.0.clone()))
}
}
Create the wrapper
Create a wrapper struct that holds the SDK Client and delegates to the underlying Rust client. See
bitwarden-uniffi/src/tool/sends.rs for a complete example.
use crate::Result;
pub struct FoldersClient(pub(crate) SharedClient);
#[uniffi::export]
impl FoldersClient {
pub async fn get(&self, folder_id: FolderId) -> Result<FolderView> {
Ok(self.0.vault().folders().get(folder_id).await?)
}
}
The wrapper should convert errors into BitwardenError. When introducing a new error type, add a
variant for it in bitwarden-uniffi/src/error.rs and implement the From
conversion.
Ownership
Feature and domain crates are usually owned and maintained by individual teams. When creating a new crate, coordinate with the Platform team to establish ownership and review expectations.