openreceive 0.4.27 → 0.4.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/openreceive/version.rb +1 -1
- data/skills/integrate-openreceive/SKILL.md +18 -1
- data/skills/integrate-openreceive/references/astro.md +2 -2
- data/skills/integrate-openreceive/references/bolt.md +580 -0
- data/skills/integrate-openreceive/references/btcpay.md +1 -1
- data/skills/integrate-openreceive/references/django.md +1 -1
- data/skills/integrate-openreceive/references/edd.md +407 -0
- data/skills/integrate-openreceive/references/fastapi.md +1 -1
- data/skills/integrate-openreceive/references/fastify.md +6 -5
- data/skills/integrate-openreceive/references/flask.md +1 -1
- data/skills/integrate-openreceive/references/fossbilling.md +466 -0
- data/skills/integrate-openreceive/references/laravel.md +1 -1
- data/skills/integrate-openreceive/references/lovable.md +2 -2
- data/skills/integrate-openreceive/references/medusa.md +468 -0
- data/skills/integrate-openreceive/references/mobile.md +1 -1
- data/skills/integrate-openreceive/references/nestjs.md +3 -3
- data/skills/integrate-openreceive/references/next.md +7 -6
- data/skills/integrate-openreceive/references/node.md +6 -5
- data/skills/integrate-openreceive/references/nuxt.md +2 -2
- data/skills/integrate-openreceive/references/paymenter.md +369 -0
- data/skills/integrate-openreceive/references/php.md +1 -1
- data/skills/integrate-openreceive/references/rails.md +1 -1
- data/skills/integrate-openreceive/references/react-router.md +2 -2
- data/skills/integrate-openreceive/references/sveltekit.md +2 -2
- data/skills/integrate-openreceive/references/tanstack-start.md +2 -2
- data/skills/integrate-openreceive/references/woocommerce.md +8 -6
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1a81bab2d7f01fe8c723b24b3d22b5ae1b2cfa705dbf9f52001b2dc3259af89
|
|
4
|
+
data.tar.gz: d46ece12f35400fccfc15365cbe61854d2ad9a6dd8641ab42dd2b6bc8d157829
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b90b00d878b6cc2a6800337a096d98790ebdc5402717814f620c45042f21ec31347fa6c1448b9ff77d5e524b1e61b6d3a954c3e8a3d75d2e1813b7a28da96a6
|
|
7
|
+
data.tar.gz: 4d209c7a0cdc243d7d3e801a0c3617aa8be4a567ea2f78b057ab6ba8de13ce302f1bbe2f42d9fd870bf872ce128ef90dc018681de8370a5ebd1a576ba3a7c894
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.29 - 2026-10-19
|
|
4
|
+
|
|
5
|
+
Release with the complete 0.4.29 package family. The bundled agent skill's
|
|
6
|
+
FOSSBilling reference covers the extension directory install. No Ruby runtime
|
|
7
|
+
changes from 0.4.28.
|
|
8
|
+
|
|
9
|
+
## 0.4.28 - 2026-10-18
|
|
10
|
+
|
|
11
|
+
Release with the complete 0.4.28 package family. The bundled agent skill adds
|
|
12
|
+
references for Paymenter, FOSSBilling, Easy Digital Downloads and Medusa. No
|
|
13
|
+
Ruby runtime changes from 0.4.27.
|
|
14
|
+
|
|
3
15
|
## 0.4.27 - 2026-10-15
|
|
4
16
|
|
|
5
17
|
Release with the complete 0.4.27 package family. The bundled agent skill
|
data/lib/openreceive/version.rb
CHANGED
|
@@ -5,7 +5,8 @@ description: >
|
|
|
5
5
|
SOL, and ETH swaps. Use for Node.js, Express, Fastify, Next.js, NestJS,
|
|
6
6
|
SvelteKit, Nuxt, Astro, React Router, TanStack Start, Rails,
|
|
7
7
|
Python, Django, FastAPI, Flask, Laravel, plain PHP, WordPress/WooCommerce,
|
|
8
|
-
|
|
8
|
+
Easy Digital Downloads,
|
|
9
|
+
Paymenter, FOSSBilling, Medusa, Lovable, Bolt, Supabase, React, Vue, Svelte, Angular, or plain HTML applications,
|
|
9
10
|
mobile apps (React Native, Expo, Flutter, Swift, Kotlin) through their
|
|
10
11
|
server, or connecting BTCPay Server to a receive-only NWC wallet. A configured swap provider
|
|
11
12
|
converts these payments to BTC over Lightning in the merchant's connected
|
|
@@ -50,9 +51,25 @@ connected wallet; asset and network availability depends on the provider.
|
|
|
50
51
|
copied from the quickstart; there is no `openreceive.flask` package.
|
|
51
52
|
- Laravel: [references/laravel.md](references/laravel.md)
|
|
52
53
|
- WordPress + WooCommerce: [references/woocommerce.md](references/woocommerce.md) — the packaged gateway and merchant settings.
|
|
54
|
+
- WordPress + Easy Digital Downloads: [references/edd.md](references/edd.md) — the same
|
|
55
|
+
WordPress.org plugin, its EDD gateway and WP-CLI setup.
|
|
56
|
+
- Paymenter: [references/paymenter.md](references/paymenter.md) — a gateway
|
|
57
|
+
extension from the GitHub release, configured with artisan commands; no
|
|
58
|
+
Composer or npm packages.
|
|
59
|
+
- FOSSBilling: [references/fossbilling.md](references/fossbilling.md) — a
|
|
60
|
+
module and payment gateway from the GitHub release, configured with
|
|
61
|
+
console commands; no Composer or npm packages.
|
|
62
|
+
- Medusa: [references/medusa.md](references/medusa.md) — the
|
|
63
|
+
`@openreceive/medusa` plugin and payment provider in the Medusa server,
|
|
64
|
+
and the React checkout in the storefront; no hooks to write.
|
|
53
65
|
- Lovable (TanStack Start on Cloudflare Workers, with Supabase):
|
|
54
66
|
[references/lovable.md](references/lovable.md) — no terminal; codes are
|
|
55
67
|
Lovable secrets and payments go through Supabase's HTTPS API.
|
|
68
|
+
- Bolt (a Vite React app with Bolt Database or Supabase, and Supabase
|
|
69
|
+
Edge Functions as server functions):
|
|
70
|
+
[references/bolt.md](references/bolt.md) — codes are Bolt secrets; one
|
|
71
|
+
server function serves the payment routes, and the page sends the
|
|
72
|
+
buyer's Supabase token.
|
|
56
73
|
- A mobile app (React Native or Expo, Flutter, Swift, Kotlin):
|
|
57
74
|
[references/mobile.md](references/mobile.md) — OpenReceive goes on the
|
|
58
75
|
app's server, through that server's reference; the app opens its checkout
|
|
@@ -17,7 +17,7 @@ curl -fsSL https://openreceive.org/agent-directions/astro/full.md
|
|
|
17
17
|
- Do not suggest rotating or revoking a code because it was pasted here.
|
|
18
18
|
- Start the quickstart only once the NWC code is saved, and the LSC code is saved or the user said "Bitcoin only".
|
|
19
19
|
|
|
20
|
-
These directions describe OpenReceive 0.4.
|
|
20
|
+
These directions describe OpenReceive 0.4.29.
|
|
21
21
|
|
|
22
22
|
Add OpenReceive to an Astro site — the one you are already working in, on
|
|
23
23
|
`@astrojs/node`. OpenReceive ships no Astro package: the integration is one
|
|
@@ -340,7 +340,7 @@ products, and you do not join them.
|
|
|
340
340
|
app's equivalent) through the `query` the library hands you, on that same
|
|
341
341
|
settlement transaction, not a second connection from your ORM. Database writes
|
|
342
342
|
only in the hook; emails, jobs, and pushes after commit. Placeholder style is
|
|
343
|
-
the dialect you declared: `?` on sqlite, `$1` on postgres.
|
|
343
|
+
the dialect you declared: `?` on sqlite and mysql, `$1` on postgres.
|
|
344
344
|
|
|
345
345
|
## If you build your own checkout UI
|
|
346
346
|
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
This is the full file; follow it from Step 0.
|
|
2
|
+
|
|
3
|
+
# OpenReceive agent directions (Bolt)
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
READ THIS FIRST: this file is 28 KB and a summary drops required steps. Read all of it:
|
|
7
|
+
https://openreceive.org/agent-directions/bolt/full.md
|
|
8
|
+
If your fetch tool gave you a summary, fetch that URL again for the full text.
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Step 0 in brief** (Step 0 below has the details):
|
|
12
|
+
|
|
13
|
+
- Before adding packages or editing anything, ask the user to add a receive-only NWC code as a secret.
|
|
14
|
+
- Next ask for the LSC code. If the user wants stablecoins, USDT, USDC, ETH, SOL or crypto, do not ask yes/no first.
|
|
15
|
+
- The user adds NWC_URI and LSC_URI_PRIMARY in Bolt's Secrets tab. Never put a code in .env, code or a VITE_ name.
|
|
16
|
+
- Never ask the user to run a command. Every step is a file, a package, a migration, a function or a secret.
|
|
17
|
+
- Do not suggest rotating or revoking a code because it was pasted here.
|
|
18
|
+
- Start Step 1 only once NWC_URI is saved, and LSC_URI_PRIMARY is saved or the user said "Bitcoin only".
|
|
19
|
+
|
|
20
|
+
These directions describe OpenReceive 0.4.29.
|
|
21
|
+
|
|
22
|
+
Add OpenReceive to this Bolt app: a Vite React app on Bolt Cloud, with Bolt
|
|
23
|
+
Database or a connected Supabase project as its database, and Supabase Edge
|
|
24
|
+
Functions as its server functions. These directions are written for Bolt's
|
|
25
|
+
agent. Every step is a file edit, a package, a Supabase migration, a server
|
|
26
|
+
function or a secret. Never ask the user to run a command. The Supabase Edge
|
|
27
|
+
Functions recipe is appended to this file in full: its code is the code to
|
|
28
|
+
follow.
|
|
29
|
+
|
|
30
|
+
An app with its own Node server (Express, Next.js and the like) is not this
|
|
31
|
+
shape: follow that framework's directions from https://openreceive.org/agents
|
|
32
|
+
instead.
|
|
33
|
+
|
|
34
|
+
Do not clone the OpenReceive repository, and do not copy a demo's models over
|
|
35
|
+
tables that already exist. Find this app's orders and products, whatever they
|
|
36
|
+
are named, and map the hooks onto those. Keep this app's UI, sign-in and
|
|
37
|
+
database.
|
|
38
|
+
|
|
39
|
+
## What OpenReceive is
|
|
40
|
+
|
|
41
|
+
A payment library that runs inside THIS app's server code. On Bolt it is one
|
|
42
|
+
server function, `openreceive`, which issues Lightning invoices against a
|
|
43
|
+
wallet the merchant already controls and settles each payment into this
|
|
44
|
+
app's database. The page shows the drop-in checkout and calls the function.
|
|
45
|
+
There is no OpenReceive account and no API key, and OpenReceive never holds
|
|
46
|
+
the funds: the sats land in the merchant's wallet.
|
|
47
|
+
|
|
48
|
+
The one required credential is a receive-only NWC code (Nostr Wallet
|
|
49
|
+
Connect): a string from the merchant's wallet that can create invoices and
|
|
50
|
+
read their status, and cannot spend. A swap provider (an "LSC" code)
|
|
51
|
+
optionally lets the payer send USDT, USDC, ETH or SOL instead, converted into
|
|
52
|
+
that same Lightning payment. Step 0 collects those credentials. You write two
|
|
53
|
+
hooks in TypeScript, `amountFor` and `authorize`, and one SQL function,
|
|
54
|
+
`openreceive_on_paid`, that marks the order paid. OpenReceive supplies
|
|
55
|
+
invoices, polling, settlement and the checkout UI. It never owns orders,
|
|
56
|
+
users, prices, or fulfillment.
|
|
57
|
+
|
|
58
|
+
The function keeps OpenReceive's payment rows in this app's database through
|
|
59
|
+
Supabase's HTTPS API, with the service role key Supabase gives every
|
|
60
|
+
function. The page is on another site than the function, so it sends the
|
|
61
|
+
buyer's Supabase access token, and `authorize` checks it.
|
|
62
|
+
|
|
63
|
+
## Step 0 — the codes are secrets the user adds
|
|
64
|
+
|
|
65
|
+
If the user says the codes are already saved as this project's secrets
|
|
66
|
+
(`NWC_URI`, and `LSC_URI_PRIMARY` unless they want Bitcoin only), believe
|
|
67
|
+
them: do not ask for them, and go to Step 1. You cannot read a secret's value,
|
|
68
|
+
and you do not need to.
|
|
69
|
+
|
|
70
|
+
Otherwise, before anything else, your next action is a question to the user.
|
|
71
|
+
Do not add packages, write migrations or edit files before asking it. A new
|
|
72
|
+
shop has neither code yet.
|
|
73
|
+
|
|
74
|
+
Two secrets are needed. Only the user can add a secret: in Bolt, the database
|
|
75
|
+
icon at the top → **Secrets** → Name and Value → **Create secret**.
|
|
76
|
+
|
|
77
|
+
- `NWC_URI` — a receive-only Nostr Wallet Connect code,
|
|
78
|
+
`nostr+walletconnect://…`. Required for Bitcoin.
|
|
79
|
+
- `LSC_URI_PRIMARY` — a Lightning Swap Connect URI,
|
|
80
|
+
`lightning+swapconnect://…`. Required for USDT, USDC, ETH and SOL. Skip it
|
|
81
|
+
only when the user says they want Bitcoin alone.
|
|
82
|
+
|
|
83
|
+
Ask one question per message, and never repeat a code back.
|
|
84
|
+
|
|
85
|
+
1. **First message — the NWC code, and nothing else.** Ask the user to add
|
|
86
|
+
the secret `NWC_URI` and walk them through getting it:
|
|
87
|
+
|
|
88
|
+
> To receive payments I need a receive-only wallet code. In Rizful: open
|
|
89
|
+
> the menu, tap NWC, choose Receive-only NWC code, and tap Copy
|
|
90
|
+
> (https://openreceive.org/get_a_nwc_code_to_receive_payments). If you would
|
|
91
|
+
> rather run your own wallet, Alby Hub works too: Connections → Add
|
|
92
|
+
> Connection → Read Only. Then click the database icon at the top, open
|
|
93
|
+
> Secrets, and create a secret named NWC_URI with the code as its value.
|
|
94
|
+
> Tell me when it is saved.
|
|
95
|
+
|
|
96
|
+
2. **If the user pastes a code into the chat** instead, do not write it into
|
|
97
|
+
any file, code or log: you have no place to keep it. Thank them, and ask
|
|
98
|
+
them to create the secret with that value in Secrets. Do not suggest
|
|
99
|
+
replacing the code.
|
|
100
|
+
3. **Second message — swaps.** If the user asked for stablecoins, USDT, USDC,
|
|
101
|
+
ETH, SOL, altcoins or "crypto" (as in "Bitcoin and stablecoin payments"),
|
|
102
|
+
this message IS the walkthrough below: send it as it is, and do not ask yes
|
|
103
|
+
or no first. Otherwise ask whether payers should also be able to pay with
|
|
104
|
+
USDT, USDC, ETH or SOL, then give the walkthrough. The walkthrough:
|
|
105
|
+
|
|
106
|
+
> Go to https://lightning-swap.com, sign in for API keys, create a key, and
|
|
107
|
+
> copy the whole URI (https://openreceive.org/set_up_swap_provider). Add it
|
|
108
|
+
> in Secrets as LSC_URI_PRIMARY — or say "Bitcoin only" and I will continue
|
|
109
|
+
> without it.
|
|
110
|
+
|
|
111
|
+
Mention FixedFloat only if they already use it. Swaps on means the refund
|
|
112
|
+
route back is part of this integration (the swap non-negotiable below).
|
|
113
|
+
4. **The database.** Supabase gives every server function `SUPABASE_URL`,
|
|
114
|
+
`SUPABASE_ANON_KEY` and `SUPABASE_SERVICE_ROLE_KEY`. Do not ask for them
|
|
115
|
+
or add them as secrets. If this project has no database yet, create it the
|
|
116
|
+
way Bolt does when an app needs one.
|
|
117
|
+
|
|
118
|
+
Never put a code in `.env` (Vite builds `VITE_` values into the page, and the
|
|
119
|
+
file travels with the project), never give one a `VITE_` prefix, and never
|
|
120
|
+
write one into code or a log. Do not invent placeholder codes. Start Step 1
|
|
121
|
+
only once the user says `NWC_URI` is saved and `LSC_URI_PRIMARY` is saved or
|
|
122
|
+
explicitly declined.
|
|
123
|
+
|
|
124
|
+
## Step 1 — the packages
|
|
125
|
+
|
|
126
|
+
Add `@openreceive/react` at 0.4.29 or newer to the app's
|
|
127
|
+
`package.json`. The function imports `npm:@openreceive/http@^0.4.29`
|
|
128
|
+
and `npm:@supabase/supabase-js@2` directly; add nothing else. No `pg`, no
|
|
129
|
+
Postgres driver, no OpenReceive scaffold.
|
|
130
|
+
|
|
131
|
+
## Step 2 — the migration
|
|
132
|
+
|
|
133
|
+
Fetch https://openreceive.org/guides/supabase-migration.md and copy its SQL
|
|
134
|
+
block, unchanged, into one new migration in `supabase/migrations/`, then
|
|
135
|
+
apply it the way this project applies migrations. Do not edit it, split it,
|
|
136
|
+
rename anything in it, or drop its `revoke` lines. It creates
|
|
137
|
+
`openreceive_payments` and `openreceive_meta` with row level security on and
|
|
138
|
+
every grant revoked from `anon` and `authenticated`, the functions the server
|
|
139
|
+
calls to write them, and `openreceive_on_paid` as a placeholder that refuses
|
|
140
|
+
every settlement.
|
|
141
|
+
|
|
142
|
+
## Step 3 — the order and its buyer
|
|
143
|
+
|
|
144
|
+
- **The order's id is the `reference`.** Create the order before checkout,
|
|
145
|
+
keep it across retries, never reuse it. A fresh id per page load lets one
|
|
146
|
+
order be paid twice.
|
|
147
|
+
- **The price is the database's, never the browser's.** In a Bolt app the
|
|
148
|
+
page usually inserts the order itself with supabase-js, so the browser
|
|
149
|
+
chose its total and status. In your second migration (Step 4), add the
|
|
150
|
+
recipe's `before insert` trigger that copies each price from the products
|
|
151
|
+
table and sets the unpaid status, adapted to this app's tables; with order
|
|
152
|
+
lines, price each line and sum them. Keep the app's order flow as it is.
|
|
153
|
+
Check the orders table's policies: a buyer may insert and read their own
|
|
154
|
+
orders, never update an order's total or status.
|
|
155
|
+
- **The buyer is the signed-in user.** The page sends the buyer's Supabase
|
|
156
|
+
access token, and `authorize` allows only the order's own user. If the app
|
|
157
|
+
has no sign-in yet, add email sign-up and log-in with Supabase Auth first,
|
|
158
|
+
and store the user's id on each order. Keep the app's sign-in if it has
|
|
159
|
+
one.
|
|
160
|
+
- **The order is unpaid or paid.** Do not copy attempt statuses (`pending`,
|
|
161
|
+
`expired`, `failed`, `attention`) onto it, and do not add a relation to
|
|
162
|
+
`openreceive_payments`.
|
|
163
|
+
- **A paid order's page says it is paid and shows no checkout.** Nothing is
|
|
164
|
+
left to pay, and a checkout cannot start a new payment for a paid order.
|
|
165
|
+
Mount the checkout only while the order is unpaid. When it settles on the
|
|
166
|
+
page, update the page's status line from `onSettled` on `<Checkout>`:
|
|
167
|
+
`openreceive_on_paid` has already marked the order paid. Keep that
|
|
168
|
+
checkout mounted, because it is the payer's receipt.
|
|
169
|
+
|
|
170
|
+
## Step 4 — `openreceive_on_paid` and the price trigger
|
|
171
|
+
|
|
172
|
+
In a second new migration, replace the placeholder with the SQL that marks
|
|
173
|
+
this app's order paid, and add the price trigger from Step 3. Use
|
|
174
|
+
`create or replace`, and this app's own table, column and status names:
|
|
175
|
+
|
|
176
|
+
```sql
|
|
177
|
+
create or replace function public.openreceive_on_paid(
|
|
178
|
+
p_reference text, p_payment_hash text, p_paid_at bigint
|
|
179
|
+
) returns void language plpgsql as $$
|
|
180
|
+
begin
|
|
181
|
+
update public.orders
|
|
182
|
+
set status = 'paid'
|
|
183
|
+
where id = p_reference::uuid -- or just p_reference, if the ids are text
|
|
184
|
+
and status = 'awaiting_payment';
|
|
185
|
+
end
|
|
186
|
+
$$;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
It runs inside the transaction that records the payment, for the order's
|
|
190
|
+
first settled payment only. If it raises, nothing is recorded, and the next
|
|
191
|
+
request retries it. Its `WHERE` on the unpaid status is the guard against a
|
|
192
|
+
second fulfillment. Keep it to database writes.
|
|
193
|
+
|
|
194
|
+
## Step 5 — the server function
|
|
195
|
+
|
|
196
|
+
Write `supabase/functions/openreceive/index.ts` exactly as the recipe's
|
|
197
|
+
"The function" section shows:
|
|
198
|
+
|
|
199
|
+
- Name the function `openreceive`, so the routes live under
|
|
200
|
+
`/functions/v1/openreceive/`.
|
|
201
|
+
- `storage: { supabase: { url: SUPABASE_URL, key: SUPABASE_SERVICE_ROLE_KEY } }`,
|
|
202
|
+
read with `Deno.env.get` inside the request handler.
|
|
203
|
+
- No `onPaid` and no `db`: `openreceive_on_paid` is the fulfillment.
|
|
204
|
+
- `findOrder` reads the order with the service role client, its total as
|
|
205
|
+
`total::text`; `authorize` checks the request's bearer token with
|
|
206
|
+
`supabase.auth.getUser(token)` and compares the user with the order's.
|
|
207
|
+
- `cors: { origins: shopOrigins(request) }`, with the recipe's `BOLT_PAGE`
|
|
208
|
+
pattern: Bolt's preview changes host every session. A custom domain goes in
|
|
209
|
+
a `SHOP_ORIGINS` secret the user adds; tell them so only if they mention
|
|
210
|
+
one.
|
|
211
|
+
- Build the stack inside each request and close it in `finally`.
|
|
212
|
+
- Never add CORS headers of your own to these routes, and never wrap the
|
|
213
|
+
handler in the usual `corsHeaders` boilerplate: OpenReceive answers its own
|
|
214
|
+
preflights.
|
|
215
|
+
|
|
216
|
+
## Step 6 — the checkout page
|
|
217
|
+
|
|
218
|
+
On the order's page, render `<Checkout>` from `@openreceive/react` with
|
|
219
|
+
`prefix` set to `` `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/openreceive` ``
|
|
220
|
+
and `fetch` set to the recipe's `withSupabaseToken`, and import
|
|
221
|
+
`@openreceive/react/styles.css` there. The order's page must have its own
|
|
222
|
+
URL (a route such as `/orders/:id`). After the order is created, navigate to
|
|
223
|
+
it. Use the drop-in; do not build your own checkout UI.
|
|
224
|
+
|
|
225
|
+
## After setup — hand over, then stop
|
|
226
|
+
|
|
227
|
+
You cannot pay the invoice: the code is receive-only. Do not pay, settle or
|
|
228
|
+
mark an order paid, and do not look for a way to.
|
|
229
|
+
|
|
230
|
+
If the checkout answers 503, the function's log names the fix: usually the
|
|
231
|
+
migration is not applied, or `openreceive_on_paid` is still the placeholder.
|
|
232
|
+
A 403 "Cross-site requests are not accepted" means the page's origin is not
|
|
233
|
+
in `shopOrigins`; a 403 from `authorize` means the buyer is not signed in, or
|
|
234
|
+
the order is someone else's.
|
|
235
|
+
|
|
236
|
+
Setup ends here. Your last message starts "Setup is finished" and has at most
|
|
237
|
+
five short lines: that a buyer signs in and places an order in the preview,
|
|
238
|
+
the methods the checkout offers, and that a small real payment from their own
|
|
239
|
+
wallet marks the order paid. Send nothing after it. Do not list what changed,
|
|
240
|
+
offer more work, or end the message on a question. Name the methods: Bitcoin,
|
|
241
|
+
plus USDT, USDC, ETH and SOL when `LSC_URI_PRIMARY` is saved. Do not mention
|
|
242
|
+
minimums, and never say a coin will not work or will not be offered.
|
|
243
|
+
|
|
244
|
+
## Non-negotiables
|
|
245
|
+
|
|
246
|
+
The recipe below has the code. These are the rules it cannot state for
|
|
247
|
+
itself.
|
|
248
|
+
|
|
249
|
+
- OpenReceive never owns orders, users, prices, or fulfillment.
|
|
250
|
+
- Keep `NWC_URI` / `LSC_URI_*` in Bolt's Secrets, read with `Deno.env.get` in
|
|
251
|
+
the server function. Never a `VITE_` variable, `.env`, page code or a log.
|
|
252
|
+
- Do not suggest rotating, revoking or replacing a code because it was pasted
|
|
253
|
+
into this chat; ask the user to save it as a secret instead.
|
|
254
|
+
- `SUPABASE_SERVICE_ROLE_KEY` reads and writes every table. Use it only in
|
|
255
|
+
the server function, never in the page.
|
|
256
|
+
- Never grant `anon` or `authenticated` anything on an `openreceive_*` table
|
|
257
|
+
or function, and never add a row level security policy to those tables.
|
|
258
|
+
The server refuses to serve while they can reach them.
|
|
259
|
+
- The host owns the price. The database sets it, `amountFor` reads it from
|
|
260
|
+
the order, and payer-supplied amounts are rejected.
|
|
261
|
+
- `authorize` runs on every request, and the `resource` it receives is a CLAIM
|
|
262
|
+
the payer made, not proof. Check the bearer token against the order's user.
|
|
263
|
+
- Receive-only NWC is required; a spend-capable code fails closed unless
|
|
264
|
+
explicitly overridden. Never set that override.
|
|
265
|
+
- There is NO merchant-initiated refund of a settled Lightning payment, because
|
|
266
|
+
the wallet cannot spend. Swap refunds — a payer reclaiming a deposit that
|
|
267
|
+
never converted — are the only refund OpenReceive performs. Do not build,
|
|
268
|
+
promise, or imply a Lightning refund path.
|
|
269
|
+
- IF YOU TURN SWAPS ON, BUILD THE ROUTE BACK. A deposit that arrives short or
|
|
270
|
+
late is refunded on a SECOND visit, so the order needs its own URL, with
|
|
271
|
+
`syncUrl` on `<Checkout>`, and the attempt must come back with it: keep the
|
|
272
|
+
`payment_hash` from `onState` and pass it as `resumePaymentHash`.
|
|
273
|
+
https://openreceive.org/guides/swap-refunds.md
|
|
274
|
+
- Show the payer what they are buying: return a `description` beside the
|
|
275
|
+
price from `amountFor`.
|
|
276
|
+
- HTTP JSON is snake_case; TypeScript APIs are camelCase.
|
|
277
|
+
- Money is integers or decimal strings — never binary floats.
|
|
278
|
+
|
|
279
|
+
## More documentation
|
|
280
|
+
|
|
281
|
+
Fetch one when the moment comes. Each is raw markdown, so a plain GET is
|
|
282
|
+
enough.
|
|
283
|
+
|
|
284
|
+
- https://openreceive.org/guides/bolt.md — what the user does on Bolt around these steps
|
|
285
|
+
- https://openreceive.org/guides/supabase.md — Supabase over HTTPS: how the storage works, and what the server checks before it serves
|
|
286
|
+
- https://openreceive.org/guides/supabase-migration.md — the migration for Step 2
|
|
287
|
+
- https://openreceive.org/guides/authorization.md — before you write `authorize`, and how cross-site requests are checked
|
|
288
|
+
- https://openreceive.org/guides/storage.md — the payment tables and the attempt state machine
|
|
289
|
+
- https://openreceive.org/guides/frontend-checkout.md — the drop-in's props, including `syncUrl` and `resumePaymentHash`
|
|
290
|
+
- https://openreceive.org/guides/checkout-ux.md — the rules the drop-in already follows
|
|
291
|
+
- https://openreceive.org/guides/provider-registry.md — where the wallet logos and pay tutorials come from: inside the JavaScript, nothing to serve
|
|
292
|
+
- https://openreceive.org/guides/automated-swaps.md — only if `LSC_URI_PRIMARY` is set
|
|
293
|
+
- https://openreceive.org/guides/swap-refunds.md — the refund flow, and the route back to it
|
|
294
|
+
- https://openreceive.org/guides/lightning-swap-connect.md — what an `LSC_URI_*` code actually is
|
|
295
|
+
- https://openreceive.org/guides/environment-variables.md — every variable, and what is deliberately not one
|
|
296
|
+
- https://openreceive.org/guides/price-feeds.md — where the fiat→sats rate comes from
|
|
297
|
+
- https://openreceive.org/guides/rate-limiting.md — before a public shop goes live
|
|
298
|
+
- https://openreceive.org/guides/security.md and https://openreceive.org/guides/deploying.md — before this goes anywhere real
|
|
299
|
+
- https://openreceive.org/guides/api-reference.md — every route, option and error code
|
|
300
|
+
- https://openreceive.org/guides.md — the index, if what you need is not above
|
|
301
|
+
|
|
302
|
+
Questions, or a problem with the library itself:
|
|
303
|
+
https://openreceive.org/contact
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## The quickstart, in full
|
|
308
|
+
|
|
309
|
+
Inlined verbatim so this file needs no network access. Steps 0–6 above are the setup on Bolt and this recipe is their reference: its function and page are the code to write, and its supabase CLI commands are Bolt's Secrets tab and Bolt's own deploy. Where the two differ, the steps win.
|
|
310
|
+
The page it comes from is https://openreceive.org/guides/supabase-edge-functions-recipe.
|
|
311
|
+
|
|
312
|
+
## Supabase Edge Functions recipe
|
|
313
|
+
|
|
314
|
+
OpenReceive ships no Edge Functions package. A Supabase Edge Function takes a
|
|
315
|
+
Web-standard `Request` and returns a `Response`, and so does
|
|
316
|
+
`@openreceive/http`, so the integration is one function named `openreceive`
|
|
317
|
+
plus the React checkout on your page. The function creates each Lightning
|
|
318
|
+
invoice, the payment goes straight to your wallet, and payment attempts are
|
|
319
|
+
stored in your Supabase database through its HTTPS API. There is no
|
|
320
|
+
OpenReceive account and no background worker.
|
|
321
|
+
|
|
322
|
+
Optional swaps let customers pay with **USDT, USDC, SOL, and ETH**. A swap
|
|
323
|
+
provider you configure converts the payment to **BTC over Lightning**, and it
|
|
324
|
+
settles into the same wallet. Available assets and networks depend on the
|
|
325
|
+
provider.
|
|
326
|
+
|
|
327
|
+
This is how Bolt apps take payments: Bolt's server functions are Supabase
|
|
328
|
+
Edge Functions, and its pages are a Vite React app on another site. The code
|
|
329
|
+
below was checked with `@openreceive/*` 0.4.27 in Supabase's edge-runtime
|
|
330
|
+
v1.77.4, behind a stand-in for Supabase's API gateway with `verify_jwt` on,
|
|
331
|
+
with Supabase Auth v2.197 and PostgREST 14. A signed-in buyer's page, served
|
|
332
|
+
as `https://<name>.bolt.host`, got a Lightning invoice through the function,
|
|
333
|
+
the same invoice after a reload, and a paid order once the wallet settled it.
|
|
334
|
+
A stranger, the bare anon key and an unlisted origin were refused, and a
|
|
335
|
+
price the browser wrote into its order was replaced by the catalog's.
|
|
336
|
+
|
|
337
|
+
Use `@openreceive/*` 0.4.27 or newer: the function needs the `cors` option.
|
|
338
|
+
|
|
339
|
+
### How it fits together
|
|
340
|
+
|
|
341
|
+
- The page is on one site (`https://shop.example`, `<name>.bolt.host`), the
|
|
342
|
+
function on another (`https://<project-ref>.supabase.co/functions/v1/openreceive`).
|
|
343
|
+
A browser sends no cookies across sites, so the page sends the buyer's
|
|
344
|
+
Supabase access token, and `authorize` checks it.
|
|
345
|
+
- The function builds an OpenReceive stack for each request and closes it
|
|
346
|
+
before it answers. Edge Functions run each request in a short-lived
|
|
347
|
+
worker, and a stack's wallet connection must not outlive it.
|
|
348
|
+
- Payments live in your database's `openreceive_payments` table, written
|
|
349
|
+
through Supabase's HTTPS API with the service role key. That key is set for
|
|
350
|
+
every function by Supabase; it never reaches the page.
|
|
351
|
+
- Your SQL function `openreceive_on_paid` marks the order paid, inside the
|
|
352
|
+
transaction that records the payment.
|
|
353
|
+
|
|
354
|
+
### 1. The migration
|
|
355
|
+
|
|
356
|
+
Save the SQL from Supabase migration,
|
|
357
|
+
unchanged, as a new migration (`supabase/migrations/<timestamp>_openreceive.sql`)
|
|
358
|
+
and apply it. It creates OpenReceive's tables with row level security on and
|
|
359
|
+
every grant revoked from `anon` and `authenticated`, and the functions the
|
|
360
|
+
server calls to write them.
|
|
361
|
+
|
|
362
|
+
### 2. `openreceive_on_paid`
|
|
363
|
+
|
|
364
|
+
In a migration of your own, replace the migration's placeholder with the SQL
|
|
365
|
+
that marks your order paid:
|
|
366
|
+
|
|
367
|
+
```sql
|
|
368
|
+
create or replace function public.openreceive_on_paid(
|
|
369
|
+
p_reference text, p_payment_hash text, p_paid_at bigint
|
|
370
|
+
) returns void language plpgsql as $$
|
|
371
|
+
begin
|
|
372
|
+
update public.orders
|
|
373
|
+
set status = 'paid'
|
|
374
|
+
where id = p_reference::uuid
|
|
375
|
+
and status = 'awaiting_payment';
|
|
376
|
+
end
|
|
377
|
+
$$;
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
It runs for the order's first settled payment only, inside the transaction
|
|
381
|
+
that records it, so a failure here records nothing and the next request
|
|
382
|
+
retries. The `status` condition is the guard against a second fulfillment.
|
|
383
|
+
|
|
384
|
+
### 3. Keep the price out of the browser's hands
|
|
385
|
+
|
|
386
|
+
`amountFor` charges what the order row says. In many Supabase apps the page
|
|
387
|
+
inserts the order itself, under a row level security policy, so the browser
|
|
388
|
+
also chose its price and status. Make the database set both. With one product
|
|
389
|
+
per order:
|
|
390
|
+
|
|
391
|
+
```sql
|
|
392
|
+
create or replace function public.orders_take_catalog_price() returns trigger
|
|
393
|
+
language plpgsql set search_path = public as $$
|
|
394
|
+
begin
|
|
395
|
+
select p.name, p.price, p.currency
|
|
396
|
+
into new.title, new.total, new.currency
|
|
397
|
+
from public.products p
|
|
398
|
+
where p.id = new.product_id;
|
|
399
|
+
if not found then
|
|
400
|
+
raise exception 'unknown product %', new.product_id;
|
|
401
|
+
end if;
|
|
402
|
+
new.status := 'awaiting_payment';
|
|
403
|
+
return new;
|
|
404
|
+
end
|
|
405
|
+
$$;
|
|
406
|
+
|
|
407
|
+
create trigger orders_take_catalog_price
|
|
408
|
+
before insert on public.orders
|
|
409
|
+
for each row execute function public.orders_take_catalog_price();
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
With order lines, do the same for each line and sum them into the order. Or
|
|
413
|
+
compute the amount in `amountFor` from the catalog instead of reading the
|
|
414
|
+
order's total. Either way, the browser must not be able to update an order's
|
|
415
|
+
total or status: no `update` policy for `authenticated` on those columns.
|
|
416
|
+
|
|
417
|
+
### 4. The function
|
|
418
|
+
|
|
419
|
+
`supabase/functions/openreceive/index.ts`. Name it `openreceive`: Supabase
|
|
420
|
+
hands a function its requests under `/<name>/`, and the routes live under
|
|
421
|
+
`/openreceive/`.
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
import { createStack } from "npm:@openreceive/http@^0.4.27";
|
|
425
|
+
import { createClient } from "npm:@supabase/supabase-js@2";
|
|
426
|
+
|
|
427
|
+
const env = (name: string) => Deno.env.get(name) ?? "";
|
|
428
|
+
|
|
429
|
+
// The pages that call this function from another site. Bolt serves its
|
|
430
|
+
// preview from a new *.local-credentialless.webcontainer-api.io host each
|
|
431
|
+
// session and a published app from <name>.bolt.host; drop BOLT_PAGE when
|
|
432
|
+
// your pages are elsewhere. List any other origin in the SHOP_ORIGINS
|
|
433
|
+
// secret, comma-separated: "https://shop.example".
|
|
434
|
+
const BOLT_PAGE = /^https:\/\/[a-z0-9-]+\.(?:bolt\.host|local-credentialless\.webcontainer-api\.io)$/;
|
|
435
|
+
|
|
436
|
+
function shopOrigins(request: Request): string[] {
|
|
437
|
+
const listed = env("SHOP_ORIGINS").split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
438
|
+
const origin = request.headers.get("origin") ?? "";
|
|
439
|
+
return BOLT_PAGE.test(origin) ? [...listed, origin] : listed;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
Deno.serve(async (request) => {
|
|
443
|
+
// Supabase sets SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY for every function.
|
|
444
|
+
const supabase = createClient(env("SUPABASE_URL"), env("SUPABASE_SERVICE_ROLE_KEY"), {
|
|
445
|
+
auth: { persistSession: false },
|
|
446
|
+
});
|
|
447
|
+
const findOrder = async (id: string) => {
|
|
448
|
+
const { data } = await supabase
|
|
449
|
+
.from("orders")
|
|
450
|
+
.select("id, user_id, status, title, currency, total::text")
|
|
451
|
+
.eq("id", id)
|
|
452
|
+
.maybeSingle();
|
|
453
|
+
return data;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const stack = createStack({
|
|
457
|
+
wallet: { nwc: env("NWC_URI") },
|
|
458
|
+
storage: {
|
|
459
|
+
supabase: { url: env("SUPABASE_URL"), key: env("SUPABASE_SERVICE_ROLE_KEY") },
|
|
460
|
+
},
|
|
461
|
+
// The price comes from the order row, never from the request.
|
|
462
|
+
amountFor: async (reference) => {
|
|
463
|
+
const order = await findOrder(reference);
|
|
464
|
+
return order?.status === "awaiting_payment"
|
|
465
|
+
? { currency: order.currency, value: order.total, description: order.title }
|
|
466
|
+
: null;
|
|
467
|
+
},
|
|
468
|
+
// Only the signed-in buyer may pay for their order.
|
|
469
|
+
authorize: async ({ request, resource }) => {
|
|
470
|
+
const token = request.headers.get("authorization")?.replace(/^Bearer /, "");
|
|
471
|
+
if (!token || !resource.reference) return false;
|
|
472
|
+
const { data } = await supabase.auth.getUser(token);
|
|
473
|
+
const order = await findOrder(resource.reference);
|
|
474
|
+
return Boolean(data.user && order && order.user_id === data.user.id);
|
|
475
|
+
},
|
|
476
|
+
cors: { origins: shopOrigins(request) },
|
|
477
|
+
});
|
|
478
|
+
try {
|
|
479
|
+
return await stack.handler(request);
|
|
480
|
+
} finally {
|
|
481
|
+
await stack.close();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
- `total::text` hands `amountFor` the price as a decimal string: PostgREST
|
|
487
|
+
returns a `numeric` column as a JSON number otherwise.
|
|
488
|
+
- There is no `onPaid`: `openreceive_on_paid` is the fulfillment.
|
|
489
|
+
- `LSC_URI_PRIMARY`, when set, turns swaps on; OpenReceive reads it from the
|
|
490
|
+
environment.
|
|
491
|
+
|
|
492
|
+
### 5. Which pages may call it
|
|
493
|
+
|
|
494
|
+
`cors` takes exact origins: scheme, host and port, with no path or trailing
|
|
495
|
+
slash. A listed origin passes the cross-site check, and the function answers
|
|
496
|
+
its preflights. Every other origin is refused. `shopOrigins` lists the
|
|
497
|
+
request's own origin when it is a Bolt page, because a Bolt preview's host
|
|
498
|
+
changes from session to session.
|
|
499
|
+
|
|
500
|
+
The origin list is not what keeps one buyer out of another's order: the token
|
|
501
|
+
is. OpenReceive never allows credentials across sites, so no cookie reaches
|
|
502
|
+
the function, and a page on any listed origin can only send the tokens it
|
|
503
|
+
holds. `authorize` decides with the token, on every request. Never answer
|
|
504
|
+
these routes with CORS headers of your own, and never remove the token check.
|
|
505
|
+
More: Cross-site requests.
|
|
506
|
+
|
|
507
|
+
### 6. The secrets
|
|
508
|
+
|
|
509
|
+
The function reads the wallet code, and the swap provider's code for swaps,
|
|
510
|
+
from the project's secrets:
|
|
511
|
+
|
|
512
|
+
```sh
|
|
513
|
+
supabase secrets set NWC_URI="nostr+walletconnect://…"
|
|
514
|
+
supabase secrets set LSC_URI_PRIMARY="lightning+swapconnect://…"
|
|
515
|
+
supabase functions deploy openreceive
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
On Bolt, add them under the database icon → **Secrets**; Bolt deploys the
|
|
519
|
+
function itself. A secret never goes in `.env`, which Vite builds into the
|
|
520
|
+
page, or in a `VITE_` variable.
|
|
521
|
+
|
|
522
|
+
### 7. The checkout page
|
|
523
|
+
|
|
524
|
+
The page points the checkout at the function and sends the buyer's token on
|
|
525
|
+
every request it makes, polling and swaps included. Only the React
|
|
526
|
+
`<Checkout>` takes a `fetch`, so the page must use it.
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
import { Checkout } from "@openreceive/react";
|
|
530
|
+
import "@openreceive/react/styles.css";
|
|
531
|
+
import { supabase } from "../lib/supabase";
|
|
532
|
+
|
|
533
|
+
// The payment routes run in the `openreceive` server function.
|
|
534
|
+
const prefix = `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/openreceive`;
|
|
535
|
+
|
|
536
|
+
// Every checkout request carries the buyer's Supabase session.
|
|
537
|
+
const withSupabaseToken: typeof fetch = async (input, init) => {
|
|
538
|
+
const { data } = await supabase.auth.getSession();
|
|
539
|
+
const headers = new Headers(init?.headers);
|
|
540
|
+
if (data.session) headers.set("Authorization", `Bearer ${data.session.access_token}`);
|
|
541
|
+
return fetch(input, { ...init, headers });
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// On the order's page, while the order is unpaid:
|
|
545
|
+
<Checkout
|
|
546
|
+
reference={order.id}
|
|
547
|
+
prefix={prefix}
|
|
548
|
+
fetch={withSupabaseToken}
|
|
549
|
+
onSettled={() => setOrder({ ...order, status: "paid" })}
|
|
550
|
+
/>;
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
The buyer must be signed in: the token is their Supabase session. Mount the
|
|
554
|
+
checkout only for an unpaid order, and keep it mounted once it settles on the
|
|
555
|
+
page, as the payer's receipt.
|
|
556
|
+
|
|
557
|
+
### How it runs
|
|
558
|
+
|
|
559
|
+
- **No worker or cron job.** Each request to the payment routes also checks
|
|
560
|
+
the wallet for settled invoices. The open checkout polls, so a payer who
|
|
561
|
+
waits sees the order paid; a payer who closes the tab is settled on the
|
|
562
|
+
next request.
|
|
563
|
+
- **About a second a request.** Each request boots a stack, connects to the
|
|
564
|
+
wallet's relay and asks Supabase Auth about the token. The first request
|
|
565
|
+
after a deploy also downloads the packages, which took about 5 s in our
|
|
566
|
+
check.
|
|
567
|
+
- **Inside the limits.** Supabase gives a request 2 s of CPU. One checkout
|
|
568
|
+
request used 100 to 600 ms in our benchmark, the high end with a wallet
|
|
569
|
+
holding 980 recent transactions.
|
|
570
|
+
|
|
571
|
+
### What we test
|
|
572
|
+
|
|
573
|
+
OpenReceive's CI runs both storage routes in Supabase's edge-runtime under
|
|
574
|
+
the hosted limits, including a cross-site page with a bearer token. The trial
|
|
575
|
+
harness runs coding agents through a Bolt-shaped shop on a local Supabase
|
|
576
|
+
stack (see How we test platforms). We
|
|
577
|
+
have not deployed to Supabase's hosted platform: its own API gateway, its
|
|
578
|
+
`verify_jwt` check and its handling of preflights are untested. If something
|
|
579
|
+
there does not work as this recipe says,
|
|
580
|
+
[open an issue](https://github.com/OpenReceive/openreceive/issues/new).
|