integration guide · phase 1: sign-in

Integrate 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

  1. Your server issues a one-time challenge {nonce, rp_origin, ts} bound to your exact origin.
  2. 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.
  3. 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.
  4. 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

routewhat it does
POST /qid/challengefresh one-time challenge bound to your origin (+ poll secret for QR); optional { retire } burns a superseded nonce on rotation
POST /qid/verifyverifies a pasted proof, creates/finds the account by address, sets the session cookie
POST /qid/proofa remote signer (phone wallet) submits its proof; gets ok or a reason, never a session
GET /qid/pollthe 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/logoutclears the session cookie

07 · The security you inherit

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

BTX PQ Wallet · desktopavailable now — copy-paste since v0.21, one-click btxqid:// connect since v0.27
Bonuz Wallet · mobilecoming soon — scans the QR tab; when it ships, your integration needs zero changes
Building a wallet? The wallet-side signer SDK (request parsing, the normative signer rules, a reference mobile approval screen) is available to wallet builders — reach out through qid.dev.

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.