Skip to main content

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-vault as an example of a crate that makes authenticated calls.

  • Does the functionality require persistent state? Review the docs for bitwarden-state and see bitwarden-vault for 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.

  1. Create the crate with cargo init and add it to the workspace Cargo.toml.
  2. Add bitwarden-core as a dependency for the shared runtime.
  3. Configure CODEOWNERS to 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<KeyIds>,
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())
}
}
note

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()
}
}
tip

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.

important

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.