All docs/Add Sign in with Beagle to your app
IDENTITY & SERVICES

Add Sign in with Beagle to your app

One npm package, three calls, and about twenty lines on your server. What to install, what to import, and what to verify.

September 2026

The short version

One dependency, three client calls, one server check. If you only read one page before wiring this up, read this one — the reasoning behind it is in Sign in with Beagle.

  • signIn({ nonce }) — find out who the visitor is
  • readLaunch() — they opened your app from inside Beagle
  • addFriend({ address, name }) — introduce two people

Three identities that must not be mixed

One person often carries three keys. They are not aliases for each other, and mixing them is the most common integration bug.

FieldWhat it isWho signs with it
useridBeagle identity (Carrier X25519, base58). Friend requests, sign-in popup, Apps launch.Beagle
address on a wallet POSTA chain wallet — Solana pubkey or 0x… . The session id on the wallet path.The wallet
carrierAddressA Carrier destination, 52 characters. Used to message, add as friend, send a reward.Nobody — it is an unsigned extra, never proof
Never send a chain pubkey as userid

And never treat a Carrier address as a wallet. A Carrier address tells you where to reach someone; it proves nothing on its own.

Three doors a user can arrive through

All three mint the same session. Get a single-use nonce from your own host first, and always sign your origin — never app.beagle.chat.

Who is signingDoorSigned prefix
A browser on app.beagle.chat, or the local CLI on 127.0.0.1:8766Popup at /connectdecent-auth
Someone already inside Beagle who opened your app from the Apps tabURL fragment #beagle=decent-launch
iOS / Android Beagle, or any wallet holding Solana or ETHSigns locally, no popupbeagle-meet-wallet
GET  /api/auth/nonce          -> single-use, ~120s
POST /api/auth/decent         { userid, signature, ... }
POST /api/auth/beagle-launch  { assertion from #beagle= }
POST /api/auth/wallet         { address, signature, ... }
The prefixes are not interchangeable

Verifying a decent-launch assertion as if it were decent-auth will fail, and a service that accepts either interchangeably has no security. Also verify against location.origin, not a bare hostname — one integration was broken for months by exactly that mismatch.

Write the code

Do not hand-roll the popup or the crypto. @decentnetwork/beagle-connect is the client library, and it is the only dependency you add — it pulls @decentnetwork/peer itself, which is what your server verifies with.

npm install @decentnetwork/beagle-connect

# package.json — that is the only dependency you add.
# It pulls @decentnetwork/peer itself, which is what your server verifies with.

Sign in. A popup opens, the user sees your origin and their identity, and approves. signIn verifies the signature before it resolves, but that check is for your UI — verify again on your server before you mint a session.

import { signIn } from "@decentnetwork/beagle-connect";

// The nonce MUST come from your server and be single-use.
// That is the whole replay defence.
const { nonce } = await fetch("/api/auth/nonce").then((r) => r.json());

const who = await signIn({ nonce });
// { userid, address, name, avatar, sig, nonce, uiOrigin }

await fetch("/api/auth/beagle", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(who),
});

They arrived from the Apps tab. Beagle puts a signed assertion in the URL fragment, so it never reaches a server log or a Referer header. readLaunch verifies it, rejects anything older than two minutes, and strips the fragment from the address bar.

import { readLaunch } from "@decentnetwork/beagle-connect";

// null when there is nothing to read. Throws when a fragment is present
// but invalid or stale — surface that, do not treat it as signed out.
const who = await readLaunch();
if (who) {
  await fetch("/api/auth/beagle-launch", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(who),
  });
}

Verify on your server. Both flows are the same primitive with a different prefix — and it is XEdDSA, not Ed25519, because a Carrier key is X25519 and the public half *is* the userid.

import { verifyDetached, base58ToBytes } from "@decentnetwork/peer";

const hexToBytes = (h) => Uint8Array.from(h.match(/../g).map((b) => parseInt(b, 16)));

function check(prefix, userid, origin, salt, sigHex) {
  const msg = new TextEncoder().encode(`${prefix}\n${origin}\n${salt}`);
  return verifyDetached(base58ToBytes(userid), msg, hexToBytes(sigHex));
}

check("decent-auth",   userid, MY_ORIGIN, nonce, sig);  // from signIn
check("decent-launch", userid, MY_ORIGIN, ts,    sig);  // from readLaunch
Three rules the launch path needs and sign-in does not

A launch carries no nonce you issued, so your server must (1) verify the signature over the decent-launch prefix, (2) reject a timestamp more than 120s away, and (3) remember every sig it has accepted for that window and refuse a repeat. Skip the third and the assertion is a bearer token valid for two minutes — which is long enough.

Introduce two people. Consent is per action and per popup; there is no token your site can keep.

import { addFriend } from "@decentnetwork/beagle-connect";

// address is the `address` field from THEIR signIn — a userid alone
// is not enough to address a friend request.
await addFriend({ address: theirCarrierAddress, name: theirDisplayName });
Signed, and not signed

Signed: the prefix, your origin, and the nonce or timestamp. NOT signed: name, avatar, punkId, address. Those are display hints so you can render a person instead of a key. Treat them as profile data — never as identity, and never as authorisation.

Next guideWhat Beagle is