No npm install required. Two files, one custom element — and every user who already has a BitLogin account elsewhere can sign in with the login they already know.
Build @bitlogin/widget from the reviewed source and copy bitlogin.js, cryptoWorker.js, and every bitlogin-shared-*.js chunk into a folder served by the same origin as your app, e.g. /vendor/bitlogin/. Keep every file together because the entrypoint resolves its worker and chunks by relative URL. Do not load the demo deployment directly from another origin.
<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);
exportIdentity, which backs the widget's own "reveal private key" button) and can visually overlay the consent UI. Treat the host page as trusted and embed BitLogin only in sites you control.
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.
If your app takes Lightning payments, stop asking users to paste an NWC string on every device. One call asks BitLogin for a wallet connection bound to your origin:
const uri = await bl.requestNwcConnection({
appName: 'Satisfied',
reason: 'Pay for meal analysis',
});
if (uri) payWith(uri); // full nostr+walletconnect:// URI, or null if declined
The widget owns the whole ceremony: sign-in first if nobody is signed in, a one-tap approval
when the account already has a connection bound to your origin (the connection follows the user to
every new device), or a guided first-time import — Bitcoin Connect's wallet chooser, or a pasted URI —
stored as an encrypted record on the user's account. A bitlogin-connection-granted event
fires on the element after each share, and users manage or revoke grants from the widget dashboard's
"Wallet connections" screen.
The inverse direction exists too: if your app obtained a connection by its own means (its own wallet chooser, its own paste box), offer the user a portable copy —
const outcome = await bl.offerNwcConnection(uri, { appName: 'Satisfied' });
// 'saved' | 'declined' | 'already-saved' | 'unavailable'
The widget shows its own consent screen ("Save this wallet to your BitLogin?") and never saves
silently — the write goes to the user's account, so it happens in BitLogin's chrome or not at all.
Duplicates (same wallet + secret) resolve already-saved with no UI, so it's safe to
fire after every successful connect. unavailable means nobody is signed in or the
account has no vault; treat it as a no-op.
style-src-attr 'unsafe-inline'
in your CSP (Bitcoin Connect's components set inline style attributes; hashes can't cover those), and
accounts created before the Connection Vault existed must enable it once from the account manager
(recovery phrase required) before connections can be saved — the flow still hands your app the URI
either way, just labeled as unsaved.
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.
If your sign-in screen lists BitLogin alongside other methods (an extension, a bunker, email) rather than embedding the full widget up front, use the brand mark instead — the same seal shown on this site and inside the widget's own header, as a plain SVG with a transparent background:
<button>
<img src="/assets/mark.svg" alt="" width="20" height="20" />
Sign in with BitLogin
</button>
Grab mark.svg (icon only), wordmark.svg
(the "BitLogin" logotype as vector paths — no font to load), or lockup.svg (both combined) from
this site's own assets/ folder. All three are plain SVG with no external dependencies, and
wordmark.svg's text is filled with currentColor so it follows your page's own text
color.
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.