No npm install required. Two files, one custom element.
Grab bitlogin.js and cryptoWorker.js (plus the shared chunk they both import) from a BitLogin release (or build @bitlogin/widget yourself) and drop them in a folder your site serves, e.g. /vendor/bitlogin/. All of them must live in the same directory — the main script loads the worker by a relative URL.
<script type="module" src="/vendor/bitlogin/bitlogin.js"></script>
<bitlogin-auth
vault-relays="wss://relay-one.example,wss://relay-two.example,wss://relay-three.example"
discovery-relays="wss://discovery-one.example,wss://discovery-two.example">
</bitlogin-auth>
That's it — the element renders its own sign-in/create/recover UI inside a shadow root, so it won't collide with your site's CSS.
const bl = document.querySelector('bitlogin-auth');
bl.addEventListener('bitlogin-login', (e) => {
console.log('signed in as', e.detail.publicKey);
});
bl.addEventListener('bitlogin-logout', () => {
console.log('signed out');
});
Once a user is signed in, BitLogin installs a window.nostr provider shaped like the de facto NIP-07 interface used by browser extensions (Alby, nos2x, …). Any existing Nostr web app code that already calls window.nostr works unmodified.
const pubkey = await window.nostr.getPublicKey();
const signed = await window.nostr.signEvent({
kind: 1,
content: 'hello from my app',
tags: [],
created_at: Math.floor(Date.now() / 1000),
});
// NIP-44 encrypted DMs
const cipher = await window.nostr.nip44.encrypt(peerPubkeyHex, 'hi there');
const plain = await window.nostr.nip44.decrypt(peerPubkeyHex, cipher);
window.nostr calls are proxied into the worker
and back.
window.nostr is a single global slot — only one provider can occupy it at a time. If a user also
has a NIP-07 extension (Alby, nos2x, …) installed, BitLogin will not overwrite it: it only installs
window.nostr when nothing is already there. The reverse isn't in BitLogin's control — an extension
that loads after BitLogin can still overwrite it, same as any two NIP-07 providers would collide.
To know specifically whether BitLogin is the active signer (as opposed to an extension), check
window.bitlogin?.isActiveSigner() rather than just typeof window.nostr !== 'undefined' —
the latter only tells you a signer is present, not which one. window.bitlogin is installed
as soon as the script loads, independent of whether <bitlogin-auth> has signed anyone in yet,
so it's safe to feature-detect immediately.
if (window.bitlogin) {
console.log('BitLogin is available, version', window.bitlogin.version);
}
if (window.bitlogin?.isActiveSigner()) {
// window.nostr is currently backed by BitLogin specifically
}
NIP-46 (remote "bunker" signers) is a different transport — it doesn't typically hook
window.nostr directly, so it isn't something BitLogin needs to avoid colliding with. A site can
offer "Sign in with BitLogin," "Connect an extension," and "Connect a bunker" as separate options; whichever
the user picks becomes the active signer, and your app code should call whatever's currently active rather
than assume a specific one.
<bitlogin-auth> element id="bitlogin".
Browsers automatically expose any element with a matching id or name attribute as
a same-named global — an element with id="bitlogin" becomes window.bitlogin,
shadowing the API object described above. Use something like id="bitlogin-widget" instead
(as this demo does).
By default, once <bitlogin-auth> mounts it claims window.nostr and keeps it —
even after the user logs out of BitLogin, since they might just be about to log back in with the same widget.
If your site offers a signer picker and the user switches to a different method, call
window.bitlogin.releaseSigner() to hand the slot back (it's a no-op, safe to call speculatively,
if BitLogin doesn't currently hold it):
// User picked a different signing method in your own UI
if (window.bitlogin?.releaseSigner()) {
// window.nostr is now undefined; install the other method's provider
}
If they switch back to BitLogin later, call claimSigner() on the element instance to reclaim
the slot (this also happens automatically every time a user completes sign-in, sign-up, or recovery through
the widget itself, so you only need this for a picker UI that lives outside the widget):
document.querySelector('bitlogin-auth').claimSigner();
Both claimSigner()/releaseSigner() on the element and
window.bitlogin.releaseSigner() also fire bitlogin-signer-claimed /
bitlogin-signer-released events (on the element and on window, respectively) if you'd
rather react to the change than check a return value.
BitLogin ships with a small built-in bootstrap relay list, but you should point vault-relays and
discovery-relays at relays you trust for your users. Use at least three vault relays for
redundancy — capsule publication and reads use a quorum, so no single relay is a single point of failure.
No account database, no password-reset email, no server-side session. The account manager demo shows the full loop: create an account, sign a test event, rotate the password, and export a recovery file.
navigator.credentials.store()) after every successful registration, login,
password rotation, and recovery. This is Chromium-only (Chrome, Edge, Brave, Opera) —
Firefox and Safari never implemented PasswordCredential, so users on those
browsers will need to save their generated password manually.