Next.js App Router
Create one Node.js catch-all route handler that dispatches OpenReceive requests.
- Server package
@openreceive/next- API mount
/openreceive/v1- Secret location
- Server only
Common setup
First, get a receive-only NWC code so your server can create invoices and check payment status. You can switch NWC providers later without changing the browser checkout.
OPENRECEIVE_NWC=nostr+walletconnect://...
OPENRECEIVE_STORE=local-sqlite
OPENRECEIVE_NAMESPACE=default
Use postgres://... for production storage, or
sqlite:///abs/path/openreceive.sqlite3 for an explicit single-machine file.
The store initializes itself on boot.
npm install @openreceive/node @openreceive/next @openreceive/react pg
npx openreceive doctor
Next.js App Router server route
Live checkout always needs a server component. Browser code never receives
OPENRECEIVE_NWC.
import {
createAlbyNwcReceiveClient,
formatOpenReceiveMissingNwcMessage,
resolveOpenReceiveStore
} from "@openreceive/node";
import {
createOpenReceiveNextRuntime,
dispatchOpenReceiveNextRoute
} from "@openreceive/next";
let runtime;
const store = await resolveOpenReceiveStore();
function getRuntime() {
if (runtime) return runtime;
const nwc = process.env.OPENRECEIVE_NWC;
if (!nwc) {
const message = formatOpenReceiveMissingNwcMessage();
console.error(message);
throw new Error(message);
}
runtime = createOpenReceiveNextRuntime({
client: createAlbyNwcReceiveClient({
connectionString: nwc
}),
store,
merchantScope: () => "app:default",
auth: {
create: (req) => isAllowedToCreateInvoice(req),
read: (req, invoice) => ownsInvoice(req, invoice),
lookup: (req, invoice) => ownsInvoice(req, invoice),
refresh: (req, invoice) => ownsInvoice(req, invoice),
poll: (req) => isInternalScheduler(req)
},
csrf: {
verify: (req) => verifyCsrf(req)
},
settlementAction: async ({ invoice, metadata }) => {
await markOrderPaid({
invoiceId: invoice.invoice_id,
orderId: metadata.order_id
});
}
});
return runtime;
}
export function openReceiveRoute(request: Request, path: readonly string[]) {
const openreceive = getRuntime();
return dispatchOpenReceiveNextRoute({
runtime: openreceive,
request,
path
});
}
import { openReceiveRoute } from "@/server/openreceive";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
type Context = {
params: Promise<{
openreceive?: string[];
}>;
};
async function handle(request: Request, context: Context) {
const params = await context.params;
return openReceiveRoute(request, params.openreceive ?? []);
}
export const GET = handle;
export const POST = handle;
Browser checkout
Your UI creates an invoice by posting to your OpenReceive server route.
const response = await fetch("/openreceive/v1/invoices", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": orderId
},
body: JSON.stringify({
fiat: {
currency: "USD",
value: "10.00"
},
metadata: {
order_id: orderId
}
})
});
const invoice = await response.json();
import { OpenReceiveCheckout } from "@openreceive/react";
import "@openreceive/react/styles.css";
export function Checkout({ invoice }) {
return (
<OpenReceiveCheckout
{...invoice}
lookupUrl="/openreceive/v1/invoices/lookup"
/>
);
}
No-framework apps can use @openreceive/elements or lower-level @openreceive/browser helpers.
Recovery
OpenReceive does not need a daemon or wallet notification listener. Browser lookups and bounded route-triggered sweeps use backend invoice lookup, and an optional scheduler can run one extra recovery pass.
web npm start
optional scheduler npx openreceive poll --once
npx openreceive poll --once