Node framework quickstart

Next.js App Router payments without the scary bits

Pick your framework and only the relevant route code stays in view. Each framework has its own crawlable URL, so you can link straight to the docs your team needs.

App Router

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.

Server environment
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.

Install packages
npm install @openreceive/node @openreceive/next @openreceive/react pg
Setup check
npx openreceive doctor

Next.js App Router server route

Live checkout always needs a server component. Browser code never receives OPENRECEIVE_NWC.

Create src/server/openreceive.ts
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
  });
}
Create src/app/openreceive/v1/[...openreceive]/route.ts
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.

Create an invoice
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();
React checkout UI
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.

Processes
web                 npm start
optional scheduler  npx openreceive poll --once
One recovery pass
npx openreceive poll --once
Storage note: Production today should use package-owned Postgres storage. SQLite is for single-machine self-hosting, local development, demos, and small apps.