btxqid:// connect since v0.27Integrate Sign in with qID
Post-quantum wallet login for your BTX app in one sitting. One server middleware, one button. The user proves control of a BTX address and that address is their account — no email, no password, no relay server, and a login proof that cannot move funds, by construction.
01 · How it works
- Your server issues a one-time challenge
{nonce, rp_origin, ts}bound to your exact origin. - The user's wallet shows them your site and signs the challenge with their post-quantum login key (ML-DSA-44) — by QR scan from a phone, copy-paste, or a
btxqid://deep link into the installed desktop wallet. - Your server verifies the proof with the frozen qID Sign-In v1 verifier, rebuilds the address from the proof, and the verified address becomes (or finds) the account. Registration and login are the same action.
- A signed HttpOnly session cookie keeps them signed in. Sessions, logout, and account-by-address are included.
There is no relay server anywhere, unlike WalletConnect: your own backend is the rendezvous. The QR carries a proof URL on your server; the phone posts the proof there and the browser that showed the QR claims the session with a secret only it holds.
02 · Get the code
git clone https://github.com/MendeMatthias/qid-connect
cd qid-connect
bun install
bun test packages # pins, frozen v1 vectors, e2e, QR + rotation flows
bun examples/express-demo/server.js
open http://localhost:8788 # a complete working login site, running on your machine
Everything is MIT. Two pieces matter: packages/server
(@qid/connect-server — challenge, verify, accounts, sessions, SQLite stores)
and packages/widget (@qid/connect-widget — the button and
dialog, one vanilla JS file, zero dependencies). Vendor them or workspace
them; the examples show both ends wired.
03 · Wire your server
Express is one middleware line:
import express from "express";
import { createQidConnect } from "@qid/connect-server";
import { qidMiddleware, requireQidSession } from "@qid/connect-server/express";
const qid = createQidConnect({
origin: "https://yourapp.com", // your exact web origin
sessionSecret: process.env.QID_SESSION_SECRET, // 32+ random chars
});
const app = express();
app.use(express.json());
app.use("/qid", qidMiddleware(qid)); // the whole login system
app.get("/api/me", requireQidSession(qid), (req, res) => {
res.json(req.qidSession); // { address, account }
});
Next.js is one catch-all route file (see examples/next-demo/):
// app/api/qid/[action]/route.js
import { createQidConnect } from "@qid/connect-server";
import { qidNextHandlers } from "@qid/connect-server/next";
const qid = createQidConnect({
origin: process.env.QID_ORIGIN,
sessionSecret: process.env.QID_SESSION_SECRET,
apiPath: "/api/qid",
});
export const { GET, POST } = qidNextHandlers(qid);
04 · Mount the button
<div id="signin"></div>
<script type="module">
import { mountQidConnect } from "/qid-connect-widget.js";
mountQidConnect(document.getElementById("signin"), {
api: "/qid", // "/api/qid" on Next
appName: "Your app",
transports: ["paste", "qr", "deeplink"], // tabs + order; first opens active
onSignedIn({ address, account }) {
location.reload(); // session cookie is already set
},
});
</script>
The dialog is dynamic out of the box: every sign-in request shows a countdown, rotates itself before it goes stale, and the server burns the superseded nonce — a screenshotted QR or copied request is worthless minutes later. Nothing to configure.
05 · Test without a wallet
The repo ships the frozen v1 reference signer as a CLI, so you can fake a
wallet during development. Click your button, copy nonce and
ts from the shown request, then:
bun tools/signer/btx-sign-ownership.mjs --random \
--origin http://localhost:8788 --nonce <nonce> --ts <ts>
Paste the printed proof into the dialog's desktop tab — or POST it to
/qid/proof to simulate a phone scanning the QR; the open dialog
signs itself in within a second or two. With a real
BTX PQ Wallet (v0.21+): Settings → qID
Sign-In, or one click via the deep link on v0.27+.
06 · What your server now speaks
| route | what it does |
|---|---|
POST /qid/challenge | fresh one-time challenge bound to your origin (+ poll secret for QR); optional { retire } burns a superseded nonce on rotation |
POST /qid/verify | verifies a pasted proof, creates/finds the account by address, sets the session cookie |
POST /qid/proof | a remote signer (phone wallet) submits its proof; gets ok or a reason, never a session |
GET /qid/poll | the browser that issued the challenge claims the session once the remote proof lands |
GET /qid/session | { address, account, expiresAt } for the current session, else 401 |
POST /qid/logout | clears the session cookie |
07 · The security you inherit
- A login proof can never move funds. Sign-in signs under the
BTX-qID/login-v1domain tag; transactions sign underTapSighash. The domains cannot collide. Structural, not policy. - Origin-bound. The wallet shows the user your origin and signs exactly it; a proof for another site is worthless on yours.
- Single-use, short-lived, rotating. Nonces are consumed atomically, expire in minutes, and rotation burns whatever left the screen.
- Phishing-resistant QR. Conforming signers refuse any request whose proof URL is not the same origin as the site being shown.
- Login-CSRF guarded. A cross-site page cannot plant a session in a visitor's browser; both
/verifyand/pollcheck. - No third-party infrastructure. Nothing to go down, nothing to trust, nothing to subpoena.
Claims we make and none we do not: resistant to quantum attacks, built on NIST-standardized post-quantum cryptography (FIPS 204 ML-DSA-44), pending independent audit. Nothing here is "unhackable" and we never say so.
08 · Wallets your users can bring
09 · The fine print
The v1 proof format and verifier are frozen; future capability arrives as
new versions verified alongside v1, so a login you ship today keeps working
forever. Phase 1 is deliberately login-only: no transaction requests, no
permissions, no persistent connections — that is what makes it safe to adopt
now (later phases are gated behind an independent audit). The code is MIT;
the qID name and mark are not — forks take their own name. Keep the button
and dialog stock (docs/DESIGN.md) so "Sign in with qID" looks
the same on every BTX app, and never proxy or rewrite the challenge.