Quick start
Use this path when the same browser runs inside Nimiq Pay and needs a Better Auth session for the current Nimiq key.
The goal is to turn an injected Nimiq wallet into a normal application session. The browser waits for the mini-app provider, the server issues a nonce, the wallet signs it, and Better Auth owns the resulting session cookie and bearer token.
Do not use this page for desktop-to-phone QR login. Use the cross-device package when approval happens on another device.
1. Install packages
Use PKG.new for installation:
2. Register the server plugin
import { betterAuth } from 'better-auth'
import { bearer } from 'better-auth/plugins'
import { nimiqAuth } from '@onmax/better-auth-nimiq'
export const auth = betterAuth({
trustedOrigins: ['https://arcade.example'],
plugins: [
bearer({ requireSignature: true }),
nimiqAuth({
appName: 'Arcade Rewards',
endpointPrefix: '/nimiq',
tokenHeaderName: 'set-auth-token',
}),
],
})
The plugin exposes two endpoints under the prefix:
| Endpoint | Purpose |
|---|---|
POST /nimiq/nonce | Issues a short-lived Nimiq login challenge |
POST /nimiq/verify | Verifies the signature and creates the Better Auth session |
Expected behavior: /nimiq/verify sets the Better Auth session cookie and returns { ok: true, token }.
3. Register the client plugin
import { createAuthClient } from 'better-auth/client'
import { getStoredNimiqAuthToken, nimiqAuthClient } from '@onmax/better-auth-nimiq/client'
const TOKEN_KEY = 'nimiq_auth_token'
export const authClient = createAuthClient({
plugins: [
nimiqAuthClient({
endpointPrefix: '/nimiq',
tokenStorageKey: TOKEN_KEY,
}),
],
fetchOptions: {
auth: {
type: 'Bearer',
token: () => getStoredNimiqAuthToken(TOKEN_KEY) || undefined,
},
},
})
The client helper persists the token in localStorage by default. The fetchOptions.auth block makes later authenticated requests send it as a bearer token.
4. Wait for the Nimiq Pay provider
import { initMiniAppProvider, isMiniAppProviderError } from '@onmax/nimiq-mini-app-kit'
export async function getSelectedNimiqAddress() {
const provider = await initMiniAppProvider({ timeout: 10_000 })
const accounts = await provider.listAccounts()
if (isMiniAppProviderError(accounts))
throw new Error(accounts.error.message)
return accounts[0] ?? null
}
Expected behavior: inside Nimiq Pay, the helper resolves the injected provider and returns the selected Nimiq address. Outside the host, it rejects after the timeout.
5. Trigger sign-in
import { signInWithNimiq } from '@onmax/better-auth-nimiq/client'
import { authClient } from './auth-client'
const result = await signInWithNimiq(authClient.$fetch.bind(authClient), {
appName: 'Arcade Rewards',
endpointPrefix: '/nimiq',
tokenStorageKey: 'nimiq_auth_token',
})
console.log(result.token)
Expected behavior: the wallet signs the challenge message, the server verifies the signature, and the browser stores the returned token.
6. Use the session
import { authClient } from './auth-client'
const session = await authClient.getSession()
console.log(session.data?.user.id)
Expected behavior: authenticated Better Auth calls work through the session cookie and through the bearer token when the app runtime needs header-based auth.
Common failure states
| Symptom | Check |
|---|---|
Nimiq provider not found on window.nimiq | Open the app inside Nimiq Pay or install simulator helpers for local development. |
Invalid Nimiq address | Pass a normalized NQ address or let the helper read the selected account from the provider. |
| Verify fails after signing | Keep appName, endpointPrefix, and request origin aligned between server and client. |
| Later API calls are unauthenticated | Send the Better Auth cookie or the stored token from set-auth-token. |