Add BitLogin to your site

No npm install required. Two files, one custom element.

  1. Copy the widget files next to your site

    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.

  2. Include the script and the element

    <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.

    Mount it somewhere your own re-renders won't touch. Removing <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.
  3. React to sign-in and sign-out

    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');
    });
  4. Sign events from anywhere on the page

    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);
    Private keys never leave a dedicated Web Worker. The main thread — your page's JS — only ever sees public keys, signed events, and ciphertext. window.nostr calls are proxied into the worker and back.
  5. Or talk to the element directly — no 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.

  6. Supporting multiple signing methods (extensions, NIP-46, BitLogin)

    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.

    Don't give your <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).

    Letting a user switch away from BitLogin without reloading

    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.
  7. Choose your relays

    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.

  8. Make it look like your site

    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.

  9. That's the whole integration

    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.

Password-manager save prompts. Because the widget's login form lives inside a shadow root and submits via JavaScript (never a real form POST), browsers' passive "offer to save this password" heuristics don't reliably fire — and the generated-password screen never puts the password into a real input at all, since it's only shown as text. The widget works around this by explicitly calling the Credential Management API (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.