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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/lib/openreceive/version.rb +1 -1
  4. data/skills/integrate-openreceive/SKILL.md +18 -1
  5. data/skills/integrate-openreceive/references/astro.md +2 -2
  6. data/skills/integrate-openreceive/references/bolt.md +580 -0
  7. data/skills/integrate-openreceive/references/btcpay.md +1 -1
  8. data/skills/integrate-openreceive/references/django.md +1 -1
  9. data/skills/integrate-openreceive/references/edd.md +407 -0
  10. data/skills/integrate-openreceive/references/fastapi.md +1 -1
  11. data/skills/integrate-openreceive/references/fastify.md +6 -5
  12. data/skills/integrate-openreceive/references/flask.md +1 -1
  13. data/skills/integrate-openreceive/references/fossbilling.md +466 -0
  14. data/skills/integrate-openreceive/references/laravel.md +1 -1
  15. data/skills/integrate-openreceive/references/lovable.md +2 -2
  16. data/skills/integrate-openreceive/references/medusa.md +468 -0
  17. data/skills/integrate-openreceive/references/mobile.md +1 -1
  18. data/skills/integrate-openreceive/references/nestjs.md +3 -3
  19. data/skills/integrate-openreceive/references/next.md +7 -6
  20. data/skills/integrate-openreceive/references/node.md +6 -5
  21. data/skills/integrate-openreceive/references/nuxt.md +2 -2
  22. data/skills/integrate-openreceive/references/paymenter.md +369 -0
  23. data/skills/integrate-openreceive/references/php.md +1 -1
  24. data/skills/integrate-openreceive/references/rails.md +1 -1
  25. data/skills/integrate-openreceive/references/react-router.md +2 -2
  26. data/skills/integrate-openreceive/references/sveltekit.md +2 -2
  27. data/skills/integrate-openreceive/references/tanstack-start.md +2 -2
  28. data/skills/integrate-openreceive/references/woocommerce.md +8 -6
  29. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a007fb3bf94216489a9c7032bb6beb18f963ad3013969c123ec29e01a9048a60
4
- data.tar.gz: 4c395312f806eb907994b9f23cee941c75489e5c3bb49b96d3621b05734edc7c
3
+ metadata.gz: d1a81bab2d7f01fe8c723b24b3d22b5ae1b2cfa705dbf9f52001b2dc3259af89
4
+ data.tar.gz: d46ece12f35400fccfc15365cbe61854d2ad9a6dd8641ab42dd2b6bc8d157829
5
5
  SHA512:
6
- metadata.gz: 4e027f450e5ffaef88a802e3f3912bef2fa7f83acf21d113b263e0db6c60e901f6515207fca72828589ee950535c6b3ffdbc395846b860d29367b962ddede38f
7
- data.tar.gz: 5d4c092240053fbda37ccc29588f7759875bd26140d87ccc0534d757a4fab5700d9f8da17809f6685b1022c462fe6c9a409059f70dc1202041a775aad6a8bd9c
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenReceive
4
- VERSION = "0.4.27"
4
+ VERSION = "0.4.29"
5
5
  end
@@ -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
- Lovable, Supabase, React, Vue, Svelte, Angular, or plain HTML applications,
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.27.
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).