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.
<bitlogin-auth> from the DOM (including indirectly, by resetting an
ancestor's innerHTML — a common pattern in hand-rolled UI without a
framework's reconciler) tears down its crypto Web Worker. If that happens mid-session,
every signing call made afterward just hangs forever with no error, because the worker
is gone and nothing ever replies — a call already in flight when it happens can even
look like it succeeded. If your app re-renders the container this element lives in,
give it a container that's never replaced wholesale and toggle the hidden
attribute instead of removing the element. See the account manager demo's source for a
single-page example, or BitRoad's src/nostr/bitloginAdapter.mjs /
#bitloginMount (a sibling Nostr commerce app that embeds BitLogin) for a
worked example of keeping the element alive across a host app's own re-renders.
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 contention<bitlogin-auth> mirrors the same four calls as instance methods, scoped to that specific
element rather than the single shared window.nostr slot:
const bl = document.querySelector('bitlogin-auth');
const pubkey = await bl.getPublicKey();
const signed = await bl.signEvent({ kind: 1, content: 'hello', tags: [] });
const cipher = await bl.nip44Encrypt(peerPubkeyHex, 'hi there');
const plain = await bl.nip44Decrypt(peerPubkeyHex, cipher);
Prefer this if your app already holds a reference to the element — e.g. you built the sign-in UI yourself
instead of relying on window.nostr-reading app code. It keeps working even if something else
currently owns window.nostr (another extension, a different widget instance), and it's what a host
app should use if it wants a specific, known signer rather than "whichever provider is active this moment."
BitRoad's src/nostr/bitloginAdapter.mjs is a real example: its signer adapter calls these four
methods directly and never touches window.nostr at all.
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.
claimSigner() can fail without breaking anything. Some NIP-07 extensions
install window.nostr as a non-configurable, non-writable property specifically to stop another
script from overwriting it. When that happens, claimSigner() returns false instead
of throwing (and bitlogin-signer-claimed's event.detail carries
{ windowNostrClaimed: false, error }), and every BitLogin sign-in flow completes normally
regardless — the failure only means window.nostr still points at that other extension. If your
app talks to the element directly (previous step) this never matters at all; only code that reads
window.nostr itself is affected, and only for as long as the other extension holds the slot.
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.
The widget renders in a shadow root, but every color, radius, font, and its max-width are all read from CSS
custom properties on :host — and custom properties inherit through the shadow boundary, so your
page's own CSS can override every one of them without touching the widget's internals:
bitlogin-auth {
--bl-accent: #3d9bff; /* primary button / link / focus ring color */
--bl-accent-hover: #2f86e0; /* primary button hover */
--bl-accent-fg: #04101f; /* text color on top of --bl-accent */
--bl-bg: #12151f; /* card background */
--bl-fg: #eaeef6; /* body text */
--bl-muted: #98a2b6; /* secondary text */
--bl-border: rgba(255, 255, 255, 0.10);
--bl-input-bg: #1a1e2b; /* input / credential-box background */
--bl-danger: #ff6b6b;
--bl-danger-bg: rgba(255, 107, 107, 0.16);
--bl-radius: 14px;
--bl-font-family: Inter, ui-sans-serif, system-ui, sans-serif;
--bl-max-width: none; /* default 380px keeps it a fixed-width card;
override to fill a wider container */
}
Overriding none of these is a valid choice — the widget falls
back to its own light theme, a prefers-color-scheme: dark media query, and a
data-theme="dark"|"light" attribute override for pages with their own theme toggle. See BitRoad's
bitlogin-auth { ... } rule in its src/styles.css for a real integration that maps
every one of these onto its own design tokens.
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.