This is the full file; follow it from Step 0.
OpenReceive agent directions (Lovable)
READ THIS FIRST: this file is 25 KB and a summary drops required steps. Read all of it:
https://openreceive.org/agent-directions/lovable/full.md
If your fetch tool gave you a summary, fetch that URL again for the full text.
Step 0 in brief (Step 0 below has the details):
- Before adding packages or editing anything, ask the user for a receive-only NWC code. One question per message.
- Next ask for the LSC code. If the user wants stablecoins, USDT, USDC, ETH, SOL or crypto, do not ask yes/no first.
- Ask for NWC_URI and LSC_URI_PRIMARY with Lovable’s secret input. Never put a code in .env, code or a VITE_ name.
- You have no terminal: never ask the user to run a command. Every step is a file, a package, a migration or a secret.
- Do not suggest rotating or revoking a code because it was pasted here.
- Start Step 1 only once NWC_URI is saved, and LSC_URI_PRIMARY is saved or the user said “Bitcoin only”.
These directions describe OpenReceive 0.4.23.
Add OpenReceive to this Lovable app: a TanStack Start app on Cloudflare Workers, with Lovable Cloud or a connected Supabase project as its database. These directions are written for Lovable’s agent. You have no terminal, so every step is a file edit, a package, a Supabase migration or a secret. Never ask the user to run a command. The TanStack Start recipe is appended to this file in full; its “On Supabase (Lovable)” section is the code to follow.
Do not clone the OpenReceive repository, and do not copy a demo’s models over tables that already exist. Find this app’s orders and products, whatever they are named, and map the hooks onto those. Keep this app’s UI, sign-in and database.
What OpenReceive is
A payment library that runs inside THIS app’s server code. It mounts as one
TanStack Start server route (src/routes/openreceive.$.ts), issues Lightning
invoices against a wallet the merchant already controls, and settles each
payment into this app’s Supabase database. There is no OpenReceive account
and no API key, and OpenReceive never holds the funds: the sats land in the
merchant’s wallet.
The one required credential is a receive-only NWC code (Nostr Wallet
Connect): a string from the merchant’s wallet that can create invoices and
read their status, and cannot spend. A swap provider (an “LSC” code)
optionally lets the payer send USDT, USDC, ETH or SOL instead, converted into
that same Lightning payment. Step 0 collects those credentials. You write two
hooks in TypeScript, amountFor and authorize, and one SQL function,
openreceive_on_paid, that marks the order paid. OpenReceive supplies
invoices, polling, settlement and the checkout UI. It never owns orders,
users, prices, or fulfillment.
On Lovable, OpenReceive keeps its payment rows in this app’s Supabase database and reaches it over Supabase’s HTTPS API with the project’s secret key, because a Worker cannot open a Postgres connection to Supabase.
Step 0 — ask for the two codes, one secret at a time
If the user says the codes are already saved as this project’s secrets
(NWC_URI, and LSC_URI_PRIMARY unless they want Bitcoin only), believe
them: do not ask for them, and go to Step 1. You cannot read a secret’s value,
and you do not need to.
Otherwise, before anything else, your next action is a question to the user. Do not add packages, write migrations or edit files before asking it. A new shop has neither code yet.
Two server-only secrets are needed before the integration:
NWC_URI— a receive-only Nostr Wallet Connect code,nostr+walletconnect://…. Required for Bitcoin.LSC_URI_PRIMARY— a Lightning Swap Connect URI,lightning+swapconnect://…. Required for USDT, USDC, ETH and SOL. Skip it only when the user says they want Bitcoin alone.
Ask for each one with Lovable’s secure secret input, named exactly as above. The user never edits a file: they paste each code into this chat or into that secret input, and Lovable stores it as a project secret. A code pasted into the chat is saved as a secret too. Never repeat a code back. Ask one question per message.
-
First message — the NWC code, and nothing else. Ask for the secret
NWC_URIand walk them through getting it:To receive payments I need a receive-only wallet code. In Rizful: open the menu, tap NWC, choose Receive-only NWC code, and tap Copy (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would rather run your own wallet, Alby Hub works too: Connections → Add Connection → Read Only. Paste it into the secret input for NWC_URI.
- When it is saved. If the user pasted it into the chat and it does not
start with
nostr+walletconnect://, ask them to copy the receive-only code again. Reply only that it is saved, then ask the next question. -
Second message — swaps. If the user asked for stablecoins, USDT, USDC, ETH, SOL, altcoins or “crypto” (as in “Bitcoin and stablecoin payments”), this message IS the walkthrough below: send it as it is, and do not ask yes or no first. Otherwise ask whether payers should also be able to pay with USDT, USDC, ETH or SOL, then give the walkthrough. The walkthrough:
Go to https://lightning-swap.com, sign in for API keys, create a key, and copy the whole URI (https://openreceive.org/set_up_swap_provider). Paste it into the secret input for LSC_URI_PRIMARY — or say “Bitcoin only” and I will continue without it.
Mention FixedFloat only if they already use it. Swaps on means the refund route back is part of this integration (the swap non-negotiable below).
- Supabase. Lovable sets
SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEYfor server code when Lovable Cloud, or a connected Supabase project, is on. Do not ask for them or try to create them: Lovable reserves theSUPABASE_prefix. If this project has no backend yet, ask the user to turn on Lovable Cloud, and wait until they have.
Never give a code a VITE_ prefix, never put one in .env (Lovable commits
that file), and never write one into code or a log. Do not invent placeholder
codes. Start Step 1 only once NWC_URI is saved and LSC_URI_PRIMARY is
saved or explicitly declined.
Step 1 — the packages
Add @openreceive/http and @openreceive/react at 0.4.23 or newer.
Nothing else: no pg, no Postgres driver, no OpenReceive scaffold. Do not
change the Vite or Wrangler configuration.
Step 2 — the migration
Fetch https://openreceive.org/guides/supabase-migration.md and copy its SQL
block, unchanged, into one new Supabase migration, then apply it. Do not edit
it, split it, rename anything in it, or drop its revoke lines. It creates
openreceive_payments and openreceive_meta with row level security on and
every grant revoked from anon and authenticated, the functions the server
calls to write them, and openreceive_on_paid as a placeholder that refuses
every settlement.
Step 3 — the order and its buyer
- The order’s id is the
reference. Create the order before checkout, keep it across retries, never reuse it. A fresh id per page load lets one order be paid twice. - The order row is the price. Order creation copies live prices into the
order.
amountForreads only that order and returns{ currency, value, description }, the value a decimal string such as"12.50", andnullfor an order that is not payable. - The buyer is a cookie. The checkout calls the payment routes from the
browser with the page’s cookies and nothing else, and a Lovable app keeps
its Supabase sign-in in the browser, not in a cookie. So in a migration add
buyer_token textto the orders table, and create orders in a server function (createServerFn) that reads thebuyercookie, or sets one tocrypto.randomUUID()(HttpOnly, Secure, SameSite=Lax, path/, one year) withgetCookieandsetCookiefrom@tanstack/react-start/server, then inserts the order withbuyer_tokenset to it throughsupabaseAdminfrom@/integrations/supabase/client.server, and returns the id. If the app creates orders in the browser today, move that insert into this function. Keep the order’s user column too, if the app has sign-in. - The order is unpaid or paid. Do not copy attempt statuses (
pending,expired,failed,attention) onto it, and do not add a relation toopenreceive_payments.
Step 4 — openreceive_on_paid
In a second new migration, replace the placeholder with the SQL that marks
this app’s order paid. Use create or replace, and this app’s own table,
column and status names:
create or replace function public.openreceive_on_paid(
p_reference text, p_payment_hash text, p_paid_at bigint
) returns void language plpgsql as $$
begin
update public.orders
set status = 'paid'
where id = p_reference::uuid -- or just p_reference, if the ids are text
and status = 'pending';
end
$$;
It runs inside the transaction that records the payment, for the order’s
first settled payment only. If it raises, nothing is recorded, and the next
request retries it. Its WHERE on the unpaid status is the guard against a
second fulfillment. Keep it to database writes.
Step 5 — the payment route
Write src/routes/openreceive.$.ts and src/lib/openreceive.server.ts
exactly as the recipe’s “On Supabase (Lovable)” section shows:
storage: { supabase: { url: process.env.SUPABASE_URL, key: process.env.SUPABASE_SERVICE_ROLE_KEY } }, read inside the handler, never at module scope.- No
onPaidand nodb:openreceive_on_paidis the fulfillment. findOrderreads the order withsupabaseAdmin;authorizecompares the request’sbuyercookie with the order’sbuyer_token.- Build the stack inside each request and close it in
finally. - Import the server module only inside the route’s handlers, so it never reaches the browser bundle.
Step 6 — the checkout page
Add src/routes/checkout.$reference.tsx with <Checkout reference={reference}
prefix="/openreceive" /> from @openreceive/react, and import
@openreceive/react/styles.css there. After the order is created, navigate
to /checkout/<id>. Use the drop-in; do not build your own checkout UI.
After setup — hand over, then stop
You cannot pay the invoice: the code is receive-only. Do not pay, settle or mark an order paid, and do not look for a way to.
If the checkout answers 503, the server log names the fix: usually the
migration is not applied, or openreceive_on_paid is still the placeholder.
Setup ends here. Your last message starts “Setup is finished” and has at most
five short lines: where to place an order in the preview, the methods the
checkout offers, and that a small real payment from their own wallet marks
the order paid. Send nothing after it. Do not list what changed, offer more
work, or end the message on a question. Name the methods: Bitcoin, plus
USDT, USDC, ETH and SOL when LSC_URI_PRIMARY is saved. Do not mention
minimums, and never say a coin will not work or will not be offered.
Non-negotiables
The recipe below has the code. These are the rules it cannot state for itself.
- OpenReceive never owns orders, users, prices, or fulfillment.
- Keep
NWC_URI/LSC_URI_*server-only: secrets read withprocess.envin.server.tsmodules. Never aVITE_variable,.env, browser code or a log. - Do not suggest rotating, revoking or replacing a code because it was pasted into this chat; that is the supported path.
SUPABASE_SERVICE_ROLE_KEYreads and writes every table. Read it only in server modules.- Never grant
anonorauthenticatedanything on anopenreceive_*table or function, and never add a row level security policy to those tables. The server refuses to serve while they can reach them. - The host owns the price.
amountForreads it from the order; reject payer-supplied amounts. authorizeruns on every request, and theresourceit receives is a CLAIM the payer made, not proof. Check thebuyercookie against the order.- Receive-only NWC is required; a spend-capable code fails closed unless explicitly overridden. Never set that override.
- There is NO merchant-initiated refund of a settled Lightning payment, because the wallet cannot spend. Swap refunds — a payer reclaiming a deposit that never converted — are the only refund OpenReceive performs. Do not build, promise, or imply a Lightning refund path.
- IF YOU TURN SWAPS ON, BUILD THE ROUTE BACK. A deposit that arrives short or
late is refunded on a SECOND visit, so the order needs its own URL
(
/checkout/$reference, withsyncUrlon<Checkout>), and the attempt must come back with it: keep thepayment_hashfromonStateand pass it asresumePaymentHash. https://openreceive.org/guides/swap-refunds.md - Show the payer what they are buying: return a
descriptionbeside the price fromamountFor. - HTTP JSON is snake_case; TypeScript APIs are camelCase.
- Money is integers or decimal strings — never binary floats.
More documentation
Fetch one when the moment comes. Each is raw markdown, so a plain GET is enough.
- https://openreceive.org/guides/lovable.md — what the user does on Lovable around these steps
- https://openreceive.org/guides/supabase.md — Supabase over HTTPS: how the storage works, and what the server checks before it serves
- https://openreceive.org/guides/supabase-migration.md — the migration for Step 2
- https://openreceive.org/guides/authorization.md — before you write
authorize - https://openreceive.org/guides/storage.md — the payment tables and the attempt state machine
- https://openreceive.org/guides/frontend-checkout.md — the drop-in’s props, including
syncUrlandresumePaymentHash - https://openreceive.org/guides/checkout-ux.md — the rules the drop-in already follows
- https://openreceive.org/guides/provider-registry.md — where the wallet logos and pay tutorials come from: inside the JavaScript, nothing to serve
- https://openreceive.org/guides/automated-swaps.md — only if
LSC_URI_PRIMARYis set - https://openreceive.org/guides/swap-refunds.md — the refund flow, and the route back to it
- https://openreceive.org/guides/lightning-swap-connect.md — what an
LSC_URI_*code actually is - https://openreceive.org/guides/environment-variables.md — every variable, and what is deliberately not one
- https://openreceive.org/guides/price-feeds.md — where the fiat→sats rate comes from
- https://openreceive.org/guides/rate-limiting.md — before a public shop goes live
- https://openreceive.org/guides/security.md and https://openreceive.org/guides/deploying.md — before this goes anywhere real
- https://openreceive.org/guides/api-reference.md — every route, option and error code
- https://openreceive.org/guides.md — the index, if what you need is not above
Questions, or a problem with the library itself: https://openreceive.org/contact
The quickstart, in full
Inlined verbatim so this file needs no network access. Steps 0–6 above are the setup on Lovable and this recipe is their reference: follow its On Supabase (Lovable) section, not its pg pool, and skip its npm commands. Where the two differ, the steps win. The page it comes from is https://openreceive.org/guides/tanstack-start-recipe.
TanStack Start recipe
OpenReceive ships no TanStack Start package. A TanStack Start server route
takes a Web-standard Request and returns a Response, and so does
@openreceive/http, so the integration is the route below. Your server code
creates each Lightning invoice, the payment goes straight to your wallet, and
payment attempts are stored in your app’s Postgres database. There is no
OpenReceive account and no background worker.
Optional swaps let customers pay with USDT, USDC, SOL, and ETH. A swap provider you configure converts the payment to BTC over Lightning, and it settles into the same wallet. Available assets and networks depend on the provider.
This recipe was checked with @tanstack/react-start 1.168 and
@openreceive/* 0.4.19. The app was built with Lovable’s Vite config for
Cloudflare Workers and run under workerd: a buyer’s checkout showed a real
Lightning invoice, and a stranger was refused. The Supabase route below runs
in OpenReceive’s CI as a Worker, against Supabase’s own Postgres image and
API server: a buyer’s invoice, a stranger refused, one live attempt under
concurrent requests, and a payment that openreceive_on_paid fulfills once.
npm install @openreceive/http @openreceive/react pg
Where it runs
- Node (the
node-serverpreset, or any Node host): any Postgres. - Cloudflare Workers (the
cloudflare-modulepreset, Lovable’s default): with Node compatibility, and a Postgres whose certificate a public authority signed, such as Neon. - Cloudflare Workers with Supabase, which is every Lovable app: a Worker cannot open a Postgres connection to Supabase, whose certificate comes from a private authority. Use On Supabase below, which stores payments through Supabase’s HTTPS API instead.
Workers ties every socket to the request that opened it, and forbids network calls at import. So the route builds its database pool and its OpenReceive stack inside each request, and closes both before it responds. Each request then opens its own wallet connection; in our test a checkout request took one to four seconds. The same code works on Node.
The payment route
Keep the handler in a .server.ts module. Route files are also bundled for
the browser, so the route loads the module only when a request arrives:
// src/routes/openreceive.$.ts — every route under /openreceive/
import { createFileRoute } from "@tanstack/react-router";
async function openReceive({ request }: { request: Request }): Promise<Response> {
const { handleOpenReceive } = await import("@/lib/openreceive.server");
return handleOpenReceive(request);
}
export const Route = createFileRoute("/openreceive/$")({
server: { handlers: { GET: openReceive, POST: openReceive } },
});
// src/lib/openreceive.server.ts
import { createStack } from "@openreceive/http";
import pg from "pg";
import { findOrder, currentVisitor } from "./orders.server"; // your own orders
export async function handleOpenReceive(request: Request): Promise<Response> {
// Read process.env inside the handler: on Workers it is empty at import.
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 1 });
const stack = createStack({
wallet: { nwc: process.env.NWC_URI ?? "" },
storage: {
db: pool,
// Runs once, inside the transaction that records the payment.
onPaid: async ({ reference, paidAt, query }) => {
await query(
"UPDATE orders SET state = 'paid', paid_at = $1 WHERE id = $2 AND state = 'awaiting_payment'",
[paidAt, reference],
);
},
},
// The price comes from the order row, never from the request.
amountFor: async (reference) => {
const order = await findOrder(pool, reference);
return order?.state === "awaiting_payment"
? { currency: order.currency, value: order.amount, description: order.title }
: null;
},
// Only the buyer may pay for their order.
authorize: async ({ request: incoming, resource }) => {
const order = resource.reference ? await findOrder(pool, resource.reference) : undefined;
const visitor = currentVisitor(incoming);
return Boolean(order && visitor && order.visitor === visitor);
},
// Cloudflare sets cf-connecting-ip on every request, and a client cannot.
// On Node behind your own proxy, read the header that proxy sets.
rateLimiting: {
ip: ({ request: incoming }) => incoming.headers.get("cf-connecting-ip") ?? undefined,
},
});
try {
return await stack.handler(request, { native: request });
} finally {
await stack.close();
await pool.end();
}
}
authorize sees the incoming request with its cookies, so check it with
the session your app already has. The Next.js quickstart
explains each hook. They are the same here.
Create OpenReceive’s two tables with your migrations, or run
paymentsSchemaSql("postgres") from @openreceive/http once. It is
idempotent. See Payment storage.
On Supabase (Lovable)
On Supabase the route keeps payments in your Supabase database through its
HTTPS API, with the project’s secret key. There is no pg pool. Use
@openreceive/* 0.4.23 or newer:
npm install @openreceive/http @openreceive/react
- Apply the migration from
npx openreceive scaffold payments --supabase. It creates OpenReceive’s tables, locked away from the browser’s Supabase key, and the functions that write them. -
Replace its placeholder
openreceive_on_paidwith the SQL that marks your order paid. It runs inside the transaction that records the payment, for the order’s first payment only, so a failure there records nothing:create or replace function public.openreceive_on_paid( p_reference text, p_payment_hash text, p_paid_at bigint ) returns void language plpgsql as $$ begin update public.orders set status = 'paid' where id = p_reference::uuid and status = 'pending'; end $$; - Write the route’s server module:
// src/lib/openreceive.server.ts
import { createStack } from "@openreceive/http";
import { findOrder, currentBuyer } from "./orders.server"; // your own orders
export async function handleOpenReceive(request: Request): Promise<Response> {
// Read process.env inside the handler: on Workers it is empty at import.
// Lovable sets SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY for server code.
const stack = createStack({
wallet: { nwc: process.env.NWC_URI ?? "" },
storage: {
supabase: {
url: process.env.SUPABASE_URL ?? "",
key: process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
},
},
// The price comes from the order row, never from the request.
amountFor: async (reference) => {
const order = await findOrder(reference);
return order?.status === "pending"
? { currency: order.currency, value: order.total, description: order.title }
: null;
},
// Only the buyer may pay for their order.
authorize: async ({ request: incoming, resource }) => {
const order = resource.reference ? await findOrder(resource.reference) : null;
const buyer = currentBuyer(incoming);
return Boolean(order && buyer && order.buyer_token === buyer);
},
rateLimiting: {
ip: ({ request: incoming }) => incoming.headers.get("cf-connecting-ip") ?? undefined,
},
});
try {
return await stack.handler(request, { native: request });
} finally {
await stack.close();
}
}
There is no onPaid: openreceive_on_paid is the fulfillment. findOrder
reads the order with your server-side Supabase client; in a Lovable app that
is supabaseAdmin from @/integrations/supabase/client.server.
The checkout calls the payment routes from the browser with the page’s
cookies, and nothing else. A Lovable app keeps its Supabase sign-in in the
browser, not in a cookie, so authorize cannot see who is signed in. Give
the buyer a cookie of their own instead: create orders in a server function
that sets an HttpOnly buyer cookie (a random value, kept across orders) and
stores the same value on the order as buyer_token. currentBuyer reads it
back from the request’s cookie header.
Before it serves, the server checks the database, and the payment routes
answer 503 until the migration is applied and openreceive_on_paid is
yours; the log names the fix. More: Supabase over HTTPS.
The checkout page
// src/routes/checkout.$reference.tsx
import { Checkout } from "@openreceive/react";
import "@openreceive/react/styles.css";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/checkout/$reference")({
component: OrderCheckout,
});
function OrderCheckout() {
const { reference } = Route.useParams();
return <Checkout reference={reference} prefix="/openreceive" />;
}
It renders on the server and starts the checkout in the browser. The payer picks Bitcoin and gets the invoice.
Settings
NWC_URI: a receive-only code from your wallet (get one). Optional:LSC_URI_PRIMARYfor swaps (set one up).DATABASE_URL: your Postgres. On serverless hosts use the pooled URL. The storage is tested through a transaction pooler.- On Supabase over HTTPS, instead of
DATABASE_URL:SUPABASE_URLand the secret key inSUPABASE_SERVICE_ROLE_KEY. Lovable sets both itself.
Set them as server secrets, never with a VITE_ prefix, which would put them
in the browser bundle.
No worker or cron job is needed. Each request to the payment routes also checks the wallet for settled invoices, through a lock in your database. A payer who closes the tab is settled on the next request.