Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Passkey Login

Passkey Login lets users access their wallet with biometrics (fingerprint, face scan, or device PIN) instead of writing down and safeguarding a seed phrase. The SDK uses the WebAuthn PRF extension to deterministically derive wallet keys from a passkey. Keys are never stored; they're regenerated on demand each time the user authenticates. The protocol also supports multiple wallets, each derived from a different label, with labels discoverable via Nostr relays.

For the full technical specification, see the Passkey Login spec.

Application configuration

Relying Party ID

The domain keys.breez.technology serves as a common Relying Party (RP) that enables cross-app passkey sharing. Applications that use this RP ID allow users to access the same passkey credentials across different platforms and apps.

To enable this cross-domain passkey sharing, keys.breez.technology serves three configuration files that declare which origins and apps are authorized to use it as an RP ID.

File: https://keys.breez.technology/.well-known/webauthn

Declares which web origins can use the centralized RP ID for WebAuthn operations:

{
  "related_origins": [
    "https://keys.breez.technology",
    "https://your-app.example.com"
  ]
}

To register your web origin, contact us to have it added to this file.

File: https://keys.breez.technology/.well-known/assetlinks.json

Establishes digital asset links between the domain and Android applications:

[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls",
      "delegate_permission/common.get_login_creds"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.yourapp",
      "sha256_cert_fingerprints": [
        "B6:16:AD:FE:C5:C6:D3:4C:93:01:5B:4A:79:20:21:4E:62:43:AB:29:28:EE:34:9A:F2:46:55:4B:54:FC:42:DF"
      ]
    }
  }
]

Replace com.example.yourapp with your application package name and the fingerprint with your app's signing certificate SHA256 fingerprint. See the Digital Asset Links documentation and Credential Manager prerequisites for details.

To register your Android app, contact us with the details outlined to have it added to this file.

iOS: Apple App Site Association

File: https://keys.breez.technology/.well-known/apple-app-site-association

Connects the domain to iOS applications for passkey sharing:

{
  "webcredentials": {
    "apps": [
      "TEAMID.com.example.yourapp"
    ]
  }
}

Replace TEAMID with your Apple Developer Team ID and com.example.yourapp with your application bundle identifier.

Your app must have the Associated Domains capability enabled. In Xcode, go to Signing & Capabilities → add Associated Domains → add the entry webcredentials:keys.breez.technology.

Expo Managed Workflow

If you're using Expo, the Breez SDK plugin can configure this automatically. See the React Native/Expo installation guide for details on the enablePasskey option.

To register your iOS app, contact us with the details outlined to have it added to this file.

Nostr relay configuration

The SDK uses Nostr relays to store and discover labels. Configure relay access by passing a NostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfigNostrRelayConfig when constructing the PasskeyPasskeyPasskeyPasskeyPasskeyPasskeyPasskeyPasskeyPasskey instance:

  • breez_api_keybreez_api_keybreezApiKeybreezApiKeybreezApiKeybreezApiKeybreezApiKeyBreezApiKeyBreezApiKey - Your Breez API key. When provided, the SDK connects to the Breez-managed relay with NIP-42 authentication.
  • timeout_secstimeout_secstimeoutSecstimeoutSecstimeoutSecstimeoutSecstimeoutSecsTimeoutSecsTimeoutSecs - Connection timeout in seconds (defaults to 30).

The SDK also implements NIP-65 to discover and publish to additional public relays for redundancy. See the Listing labels and Storing a label code examples below for usage.

Implementing the PRF provider

Your application must implement the PRF provider to interface with platform passkey APIs.

Rust
/// In practice, implement using platform-specific passkey APIs.
struct ExamplePasskeyPrfProvider;

#[async_trait::async_trait]
impl PasskeyPrfProvider for ExamplePasskeyPrfProvider {
    async fn derive_prf_seed(&self, _salt: String) -> Result<Vec<u8>, PasskeyPrfError> {
        // Call platform passkey API with PRF extension
        // Returns 32-byte PRF output
        todo!("Implement using WebAuthn or native passkey APIs")
    }

    async fn is_prf_available(&self) -> Result<bool, PasskeyPrfError> {
        // Check if PRF-capable passkey exists
        todo!("Check platform passkey availability")
    }
}
Swift
// In practice, implement using platform-specific passkey APIs.
class ExamplePasskeyPrfProvider: PasskeyPrfProvider {
    func derivePrfSeed(salt: String) async throws -> Data {
        // Call platform passkey API with PRF extension
        // Returns 32-byte PRF output
        fatalError("Implement using WebAuthn or native passkey APIs")
    }

    func isPrfAvailable() async throws -> Bool {
        // Check if PRF-capable passkey exists
        fatalError("Check platform passkey availability")
    }
}
Kotlin
// In practice, implement PRF provider using platform passkey APIs
class ExamplePasskeyPrfProvider : PasskeyPrfProvider {
    override suspend fun derivePrfSeed(salt: String): ByteArray {
        // Call platform passkey API with PRF extension
        // Returns 32-byte PRF output
        TODO("Implement using WebAuthn or native passkey APIs")
    }

    override suspend fun isPrfAvailable(): Boolean {
        // Check if PRF-capable passkey exists
        TODO("Check platform passkey availability")
    }
}
C#
// In practice, implement using platform-specific passkey APIs.
class ExamplePasskeyPrfProvider : PasskeyPrfProvider
{
    public async Task<byte[]> DerivePrfSeed(string salt)
    {
        // Call platform passkey API with PRF extension
        // Returns 32-byte PRF output
        throw new NotImplementedException("Implement using WebAuthn or native passkey APIs");
    }

    public async Task<bool> IsPrfAvailable()
    {
        // Check if PRF-capable passkey exists
        throw new NotImplementedException("Check platform passkey availability");
    }
}
Javascript
// In practice, implement PRF provider using WebAuthn API
class ExamplePasskeyPrfProvider {
  derivePrfSeed = async (salt: string): Promise<Uint8Array> => {
    // Call platform passkey API with PRF extension
    // Returns 32-byte PRF output
    throw new Error('Implement using WebAuthn or native passkey APIs')
  }

  isPrfAvailable = async (): Promise<boolean> => {
    // Check if PRF-capable passkey exists
    throw new Error('Check platform passkey availability')
  }
}
React Native
// In practice, implement PRF provider using platform passkey APIs
class ExamplePasskeyPrfProvider {
  derivePrfSeed = async (salt: string): Promise<ArrayBuffer> => {
    // Call platform passkey API with PRF extension
    // Returns 32-byte PRF output
    throw new Error('Implement using WebAuthn or native passkey APIs')
  }

  isPrfAvailable = async (): Promise<boolean> => {
    // Check if PRF-capable passkey exists
    throw new Error('Check platform passkey availability')
  }
}
Flutter
// Implement these functions using platform passkey APIs.
Future<Uint8List> derivePrfSeed(String salt) async {
  // Call platform passkey API with PRF extension
  // Returns 32-byte PRF output
  throw UnimplementedError('Implement using platform passkey APIs');
}

Future<bool> isPrfAvailable() async {
  // Check if PRF-capable passkey exists
  throw UnimplementedError('Check platform passkey availability');
}
Python
# In practice, implement using platform-specific passkey APIs.
class ExamplePasskeyPrfProvider(PasskeyPrfProvider):
    async def derive_prf_seed(self, salt: str):
        # Call platform passkey API with PRF extension
        # Returns 32-byte PRF output
        raise NotImplementedError("Implement using WebAuthn or native passkey APIs")

    async def is_prf_available(self):
        # Check if PRF-capable passkey exists
        raise NotImplementedError("Check platform passkey availability")
Go
// In practice, implement using platform-specific passkey APIs.
type ExamplePasskeyPrfProvider struct{}

func (p *ExamplePasskeyPrfProvider) DerivePrfSeed(salt string) ([]byte, error) {
	// Call platform passkey API with PRF extension
	// Returns 32-byte PRF output
	panic("Implement using WebAuthn or native passkey APIs")
}

func (p *ExamplePasskeyPrfProvider) IsPrfAvailable() (bool, error) {
	// Check if PRF-capable passkey exists
	panic("Check platform passkey availability")
}

Platform considerations

  • Web (browsers): Use the WebAuthn API with the prf extension. Browsers handle the salt transformation internally. Use discoverable credentials (residentKey: 'required') with empty allowCredentials for assertion so the browser discovers the credential by RP ID.

  • Android / iOS: Use native passkey APIs with PRF support. Ensure the Associated Domains / Asset Links configuration is in place for keys.breez.technology.

  • CLI / Desktop (CTAP2): Use the hmac-secret extension directly. Non-browser implementations must apply the WebAuthn salt transformation manually to produce the same PRF output as browsers:

    actualSalt = SHA-256("WebAuthn PRF" || 0x00 || developerSalt)
    

    This transformation is defined in the W3C WebAuthn PRF extension spec and ensures that the same passkey + salt produces identical seeds across browser and native implementations.

Connecting with a passkey API docs

To connect with a passkey, call Passkey.get_walletPasskey.get_walletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.GetWalletPasskey.GetWallet to derive a wallet, then pass its seed to connectconnectconnectconnectconnectconnectconnectConnectConnect. The label defaults to "Default" when omitted.

Rust
let prf_provider = Arc::new(ExamplePasskeyPrfProvider);
let passkey = Passkey::new(prf_provider, None);

// Derive the wallet from the passkey (pass None for the default wallet)
let wallet = passkey.get_wallet(Some("personal".to_string())).await?;

let config = default_config(Network::Mainnet);
let sdk = connect(ConnectRequest {
    config,
    seed: wallet.seed,
    storage_dir: "./.data".to_string(),
})
.await?;
Swift
let prfProvider = ExamplePasskeyPrfProvider()
let passkey = Passkey(prfProvider: prfProvider, relayConfig: nil)

// Derive the wallet from the passkey (pass nil for the default wallet)
let wallet = try await passkey.getWallet(label: "personal")

let config = defaultConfig(network: .mainnet)
let sdk = try await connect(
    request: ConnectRequest(
        config: config,
        seed: wallet.seed,
        storageDir: "./.data"
    ))
Kotlin
val prfProvider = ExamplePasskeyPrfProvider()
val passkey = Passkey(prfProvider, null)

// Derive the wallet from the passkey (pass null for the default wallet)
val wallet = passkey.getWallet("personal")

val config = defaultConfig(Network.MAINNET)
val sdk = connect(ConnectRequest(config, wallet.seed, "./.data"))
C#
var prfProvider = new ExamplePasskeyPrfProvider();
var passkey = new Passkey(prfProvider, null);

// Derive the wallet from the passkey (pass null for the default wallet)
var wallet = await passkey.GetWallet(label: "personal");

var config = BreezSdkSparkMethods.DefaultConfig(network: Network.Mainnet);
var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(
    config: config,
    seed: wallet.seed,
    storageDir: "./.data"
));
Javascript
const prfProvider = new ExamplePasskeyPrfProvider()
const passkey = new Passkey(prfProvider, undefined)

// Construct the wallet using the passkey (pass undefined for the default wallet)
const wallet = await passkey.getWallet('personal')

const config = defaultConfig('mainnet')
const sdk = await connect({ config, seed: wallet.seed, storageDir: './.data' })
React Native
const prfProvider = new ExamplePasskeyPrfProvider()
const passkey = new Passkey(prfProvider, undefined)

// Construct the wallet using the passkey (pass undefined for the default wallet)
const wallet = await passkey.getWallet('personal')

const config = defaultConfig(Network.Mainnet)
const sdk = await connect({ config, seed: wallet.seed, storageDir: './.data' })
Flutter
final passkey = Passkey(
  derivePrfSeed: derivePrfSeed,
  isPrfAvailable: isPrfAvailable,
);

// Derive the wallet from the passkey (pass null for the default wallet)
final wallet = await passkey.getWallet(label: "personal");

final config = defaultConfig(network: Network.mainnet);
final sdk = await connect(
    request: ConnectRequest(
        config: config, seed: wallet.seed, storageDir: "./.data"));
Python
prf_provider = ExamplePasskeyPrfProvider()
passkey = Passkey(prf_provider, None)

# Derive the wallet from the passkey (pass None for the default wallet)
wallet = await passkey.get_wallet("personal")

config = default_config(network=Network.MAINNET)
sdk = await connect(ConnectRequest(config=config, seed=wallet.seed, storage_dir="./.data"))
Go
prfProvider := &ExamplePasskeyPrfProvider{}
passkey := breez_sdk_spark.NewPasskey(prfProvider, nil)

// Derive the wallet from the passkey (pass nil for the default wallet)
label := "personal"
wallet, err := passkey.GetWallet(&label)
if err != nil {
	return nil, err
}

config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)
sdk, err := breez_sdk_spark.Connect(breez_sdk_spark.ConnectRequest{
	Config:     config,
	Seed:       wallet.Seed,
	StorageDir: "./.data",
})
if err != nil {
	return nil, err
}

Listing labels API docs

Discover labels associated to the passkey using Nostr.

Rust
let prf_provider = Arc::new(ExamplePasskeyPrfProvider);
let relay_config = NostrRelayConfig {
    breez_api_key: Some("<breez api key>".to_string()),
    ..NostrRelayConfig::default()
};
let passkey = Passkey::new(prf_provider, Some(relay_config));

// Query Nostr for labels associated with this passkey
let labels = passkey.list_labels().await?;

for label in &labels {
    println!("Found label: {}", label);
}
Swift
let prfProvider = ExamplePasskeyPrfProvider()
let relayConfig = NostrRelayConfig(breezApiKey: "<breez api key>")
let passkey = Passkey(prfProvider: prfProvider, relayConfig: relayConfig)

// Query Nostr for labels associated with this passkey
let labels = try await passkey.listLabels()

for label in labels {
    print("Found label: \(label)")
}
Kotlin
val prfProvider = ExamplePasskeyPrfProvider()
val relayConfig = NostrRelayConfig(breezApiKey = "<breez api key>")
val passkey = Passkey(prfProvider, relayConfig)

// Query Nostr for labels associated with this passkey
val labels = passkey.listLabels()

for (label in labels) {
    // Log.v("Breez", "Found label: $label")
}
C#
var prfProvider = new ExamplePasskeyPrfProvider();
var relayConfig = new NostrRelayConfig(
    breezApiKey: "<breez api key>"
);
var passkey = new Passkey(prfProvider, relayConfig);

// Query Nostr for labels associated with this passkey
var labels = await passkey.ListLabels();

foreach (var label in labels)
{
    Console.WriteLine($"Found label: {label}");
}
Javascript
const prfProvider = new ExamplePasskeyPrfProvider()
const relayConfig: NostrRelayConfig = {
  breezApiKey: '<breez api key>'
}
const passkey = new Passkey(prfProvider, relayConfig)

// Query Nostr for labels associated with this passkey
const labels = await passkey.listLabels()

for (const label of labels) {
  console.log(`Found label: ${label}`)
}
React Native
const prfProvider = new ExamplePasskeyPrfProvider()
const relayConfig: NostrRelayConfig = {
  breezApiKey: '<breez api key>',
  timeoutSecs: undefined
}
const passkey = new Passkey(prfProvider, relayConfig)

// Query Nostr for labels associated with this passkey
const labels = await passkey.listLabels()

for (const label of labels) {
  console.log(`Found label: ${label}`)
}
Flutter
final relayConfig = NostrRelayConfig(
  breezApiKey: '<breez api key>',
);
final passkey = Passkey(
  derivePrfSeed: derivePrfSeed,
  isPrfAvailable: isPrfAvailable,
  relayConfig: relayConfig,
);

// Query Nostr for labels associated with this passkey
final labels = await passkey.listLabels();

for (final label in labels) {
  print("Found label: $label");
}
Python
prf_provider = ExamplePasskeyPrfProvider()
relay_config = NostrRelayConfig(breez_api_key="<breez api key>")
passkey = Passkey(prf_provider, relay_config)

# Query Nostr for labels associated with this passkey
labels = await passkey.list_labels()

for label in labels:
    print(f"Found label: {label}")
Go
prfProvider := &ExamplePasskeyPrfProvider{}
breezApiKey := "<breez api key>"
relayConfig := &breez_sdk_spark.NostrRelayConfig{
	BreezApiKey: &breezApiKey,
}
passkey := breez_sdk_spark.NewPasskey(prfProvider, relayConfig)

// Query Nostr for labels associated with this passkey
labels, err := passkey.ListLabels()
if err != nil {
	return nil, err
}

for _, label := range labels {
	log.Printf("Found label: %s", label)
}

Storing a label API docs

Publish a label to Nostr so it can be discovered later.

Rust
let prf_provider = Arc::new(ExamplePasskeyPrfProvider);
let relay_config = NostrRelayConfig {
    breez_api_key: Some("<breez api key>".to_string()),
    ..NostrRelayConfig::default()
};
let passkey = Passkey::new(prf_provider, Some(relay_config));

// Publish the label to Nostr for later discovery
passkey.store_label("personal".to_string()).await?;
Swift
let prfProvider = ExamplePasskeyPrfProvider()
let relayConfig = NostrRelayConfig(breezApiKey: "<breez api key>")
let passkey = Passkey(prfProvider: prfProvider, relayConfig: relayConfig)

// Publish the label to Nostr for later discovery
try await passkey.storeLabel(label: "personal")
Kotlin
val prfProvider = ExamplePasskeyPrfProvider()
val relayConfig = NostrRelayConfig(breezApiKey = "<breez api key>")
val passkey = Passkey(prfProvider, relayConfig)

// Publish the label to Nostr for later discovery
passkey.storeLabel("personal")
C#
var prfProvider = new ExamplePasskeyPrfProvider();
var relayConfig = new NostrRelayConfig(
    breezApiKey: "<breez api key>"
);
var passkey = new Passkey(prfProvider, relayConfig);

// Publish the label to Nostr for later discovery
await passkey.StoreLabel(label: "personal");
Javascript
const prfProvider = new ExamplePasskeyPrfProvider()
const relayConfig: NostrRelayConfig = {
  breezApiKey: '<breez api key>'
}
const passkey = new Passkey(prfProvider, relayConfig)

// Publish the label to Nostr for later discovery
await passkey.storeLabel('personal')
React Native
const prfProvider = new ExamplePasskeyPrfProvider()
const relayConfig: NostrRelayConfig = {
  breezApiKey: '<breez api key>',
  timeoutSecs: undefined
}
const passkey = new Passkey(prfProvider, relayConfig)

// Publish the label to Nostr for later discovery
await passkey.storeLabel('personal')
Flutter
final relayConfig = NostrRelayConfig(
  breezApiKey: '<breez api key>',
);
final passkey = Passkey(
  derivePrfSeed: derivePrfSeed,
  isPrfAvailable: isPrfAvailable,
  relayConfig: relayConfig,
);

// Publish the label to Nostr for later discovery
await passkey.storeLabel(label: "personal");
Python
prf_provider = ExamplePasskeyPrfProvider()
relay_config = NostrRelayConfig(breez_api_key="<breez api key>")
passkey = Passkey(prf_provider, relay_config)

# Publish the label to Nostr for later discovery
await passkey.store_label(label="personal")
Go
prfProvider := &ExamplePasskeyPrfProvider{}
breezApiKey := "<breez api key>"
relayConfig := &breez_sdk_spark.NostrRelayConfig{
	BreezApiKey: &breezApiKey,
}
passkey := breez_sdk_spark.NewPasskey(prfProvider, relayConfig)

// Publish the label to Nostr for later discovery
err := passkey.StoreLabel("personal")
if err != nil {
	return err
}

Best practices

Cache the user-selected label

Store the label locally (e.g., localStorage on web, SharedPreferences on Android, UserDefaults on iOS) if selected by the user. This allows the app to skip the label selection step on subsequent launches and go straight to passkey authentication.

Never store the derived mnemonic

The mnemonic should always be re-derived from the passkey and label on each session. The passkey authentication (biometric, PIN, etc.) is the security boundary — storing the mnemonic would bypass it. On app restart, check for a cached label and prompt the user for passkey authentication to derive the seed.

Allow manual mnemonic backup

Provide a way for users to reveal their derived 12-word mnemonic as an emergency backup. This should be user-initiated (e.g., behind a "Show recovery phrase" button) and derived on-demand via Passkey.get_walletPasskey.get_walletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.getWalletPasskey.GetWalletPasskey.GetWallet with the cached label. This gives users a safety net if they lose access to their passkey.

Offer a mnemonic fallback

Not all devices support the PRF extension. Check Passkey.is_availablePasskey.is_availablePasskey.isAvailablePasskey.isAvailablePasskey.isAvailablePasskey.isAvailablePasskey.isAvailablePasskey.IsAvailablePasskey.IsAvailable at startup and present the appropriate flow — seedless for capable devices, traditional mnemonic backup/restore for others.

Handle label discovery failures

When discovering labels, Passkey.list_labelsPasskey.list_labelsPasskey.listLabelsPasskey.listLabelsPasskey.listLabelsPasskey.listLabelsPasskey.listLabelsPasskey.ListLabelsPasskey.ListLabels may return an empty list if relays are unreachable or the label events have been pruned. Always allow manual label entry as a fallback alongside the Nostr-discovered list.

Supported specs

  • Seedless Restore Passkey-based wallet derivation and discovery
  • Nostr Relay-based event protocol for label storage
  • NIP-42 Authentication of clients to relays
  • NIP-65 Relay List Metadata