# Recipe: Lovable

For an app built in Lovable: a React front end with its server code in Supabase Edge Functions (Lovable Cloud, or a Supabase project connected to Lovable). The code is the [Supabase recipe](https://checkouts.md/recipes/supabase.md); this file covers what's different in Lovable and the prompt to paste. [The playbook](https://checkouts.md/playbook.md) has the reasons and every error code.

| | |
|---|---|
| Server code runs in | Supabase Edge Functions, which Lovable writes and deploys |
| Secrets | Edge Function secrets, entered through Lovable's secret form (never the chat) |
| Front end | React, Vite and TypeScript; the Supabase client is `@/integrations/supabase/client` |
| Webhook URL | `https://<project-ref>.supabase.co/functions/v1/railbed-webhook` |
| Connect | The person creates the keys in Railbed and enters them in Lovable's secret form (below) |

## 1. Connect

Lovable's chat can't run `npx checkouts.md login`. Pick the first that applies:

1. **If your Lovable agent can call an HTTP API and save a secret straight from the response into Edge Function secrets, without the value appearing in the chat, a log or a file**, use the HTTP connect flow in the playbook's [3.2](https://checkouts.md/playbook.md#32-with-http-when-npx-cant-run) with `"client": "Lovable"` and `"modes": ["test"]` (a Live key comes later, at go-live, and is delivered only once), and save `keys.test.secret` as the secret `RAILBED_SECRET_KEY`.
2. **Otherwise**, the approval happens in Railbed's dashboard. Ask the person to:
   - sign in or sign up at https://app.railbed.io/signup, and switch on **Test mode** in the business menu at the top of the sidebar;
   - open **Developers**, choose **Create key** with purpose **Checkout API** and **Full access**, name it "Lovable", and copy it;
   - paste it into Lovable's secret form when you ask for `RAILBED_SECRET_KEY`. Never into the chat.

Say it like this:

> To connect payments I need a Railbed key, which only you can create. Sign in at app.railbed.io (or create an account), turn on Test mode in the menu at the top left, open Developers and choose Create key. Then paste the key into the secure form I'm opening for `RAILBED_SECRET_KEY`, not into this chat. Nothing is charged in Test mode.

## 2. The prompt to paste

The person pastes this into the Lovable chat (with the product and price filled in). Lovable reads the recipe if it can open web pages:

```text
Read checkouts.md/recipes/lovable.md and add a Railbed checkout to this app, following it exactly.
Sell: Pro plan for 49.00 USD.
Keep the secret key and the webhook secret only in Edge Function secrets (RAILBED_SECRET_KEY,
RAILBED_WEBHOOK_SECRET), never in code or in this chat. Mark an order paid only from a verified Railbed
webhook after re-reading the payment. Test it in Railbed's Test mode before anything goes live.
```

If Lovable can't open the link, paste this instead, which says the same thing in full:

```text
Add card checkout with Railbed to this app, using Supabase Edge Functions.

1. Migration: table orders (id text primary key, product_id text, amount text, currency text,
   livemode boolean, status text default 'awaiting_payment', payment_id text unique, user_id uuid,
   created_at timestamptz default now(), paid_at timestamptz) and table railbed_events (id text primary key,
   type text, payment_id text, received_at timestamptz default now()), both with row level security on and
   no policies. A plpgsql function railbed_apply_payment(event id, event type, order id, payment id, status,
   paid_at) that inserts the event id (on conflict do nothing, and returns 'duplicate' if it existed), then for
   status 'paid' updates the order to paid only where status <> 'paid', and for held, failed or expired sets
   that status unless the order is paid. Execute granted to service_role only.
2. Edge Function railbed-checkout (POST): body {product} looks up the price in a server-side PRODUCTS map
   (amounts are strings like "49.00"), inserts an order with id "order_" + a UUID, then calls
   POST https://pay.railbed.io/v1/checkout_sessions with Authorization: Bearer RAILBED_SECRET_KEY,
   Idempotency-Key = the order id, and JSON {amount, currency, description, reference: order id,
   success_url: APP_URL + "/checkout/success?order={REFERENCE}", cancel_url: APP_URL + "/"}, saves the
   returned id as payment_id, and returns {url}. Body {order} returns that order's status.
3. Edge Function railbed-webhook (POST, verify_jwt = false in supabase/config.toml): read the raw body as
   text, verify the Railbed-Signature header (t=<unix seconds>,v1=<hex>, one or more v1): HMAC-SHA256 with
   RAILBED_WEBHOOK_SECRET over "<t>.<raw body>", constant-time compare with every v1, reject if t is more
   than 300 seconds off, answer 400 if invalid. Answer 200 and do nothing for events that aren't payment.*.
   For payment.* events, check the event's livemode matches the key (rb_live_ is Live; a mismatch means
   the other mode's secret is set: answer 500), GET
   https://pay.railbed.io/v1/payments/<data.payment.id> (a 404 for a Test event whose
   data.payment.metadata.sample is "true" is a dashboard sample: answer 200), find the order by payment_id
   (none: answer 200), check livemode, reference, amount and currency match (a mismatch: log it, answer 200,
   don't fulfil), and call railbed_apply_payment. Answer 200 within 10 seconds; if the re-read or the
   database fails, answer 500 so Railbed retries.
4. A Pay button that calls supabase.functions.invoke('railbed-checkout', { body: { product } }) and sends
   the browser to the returned url, and a /checkout/success page that shows the order's status (refreshing
   every 3 seconds while awaiting_payment) and never marks anything paid.
5. Ask me for RAILBED_SECRET_KEY, RAILBED_WEBHOOK_SECRET and APP_URL through the secret form, never in chat.
6. Write CHECKOUTS.md at the project root as described at checkouts.md/spec.md.
```

## 3. What gets built

Exactly the [Supabase recipe](https://checkouts.md/recipes/supabase.md): the migration with `railbed_apply_payment()`, `supabase/functions/_shared/railbed.ts`, `railbed-checkout`, `railbed-webhook`, `supabase/config.toml`, `PayButton` and the `/checkout/success` page. In Lovable:

- Import the client as `import { supabase } from '@/integrations/supabase/client';` in `PayButton.tsx` and `CheckoutSuccess.tsx`.
- Add the `/checkout/success` route to the app's router (usually `src/App.tsx`).
- Lovable deploys Edge Functions when it changes them. Check that `supabase/config.toml` has `verify_jwt = false` for `railbed-webhook`; without it every delivery fails with `401`.
- Set the secret `APP_URL` to the app's address (the preview address while testing, the published `https://….lovable.app` or custom domain for Live).

## 4. The webhook endpoint

Lovable can't run `npx checkouts.md webhook`. With the key from step 1:

- **If your agent can make the API call and store the secret without showing it** (as in step 1), create the endpoint with `POST https://pay.railbed.io/v1/webhook_endpoints`, `{ "url": "https://<project-ref>.supabase.co/functions/v1/railbed-webhook" }`, and save the returned `secret` as `RAILBED_WEBHOOK_SECRET`.
- **Otherwise**, ask the person to add it in Railbed: Developers → **Add endpoint**, the address above, then **Reveal** the signing secret and paste it into the secret form as `RAILBED_WEBHOOK_SECRET`.

The project ref is in the Supabase URL the app already uses (`https://<project-ref>.supabase.co`). The function is already public, so no tunnel is needed.

## 5. Test it

1. In the app's preview, click the Pay button. Railbed's checkout opens.
2. Enter any email, choose Pay, and on the test provider page choose **Simulate successful payment**.
3. Check: the success page says the order is confirmed; in Supabase (Lovable Cloud's database view, or Supabase's table editor) the order is `paid`; the `railbed-webhook` function's logs show `fulfilled`; in Railbed's dashboard (Test mode) the payment's timeline shows **Webhook delivered**.
4. Try **Simulate an underpayment** with a new order: the order shows `held` and nothing is granted.
5. Record the payment id under Evidence in `CHECKOUTS.md`.

## 6. Go live

1. The business adds a payout wallet in Railbed's Settings.
2. Get a Live key the way the Test key came in step 1: the HTTP flow again with `"modes": ["test", "live"]` (the person approves Test and Live), saving `keys.live.secret`; or, in Railbed's Live mode, the person creates one (Developers → Create key, purpose Checkout API, Full access). Create a Live endpoint for the same function address the way step 4 created the Test one, with the Live key. Then replace both secrets (`RAILBED_SECRET_KEY` with `rb_live_…`, `RAILBED_WEBHOOK_SECRET` with the Live endpoint's secret), never through the chat. Edge Function secrets are one set per project, so after this the project takes real payments; keep a separate Supabase project for testing if the app needs both.
3. Set `APP_URL` to the published address and publish.
4. Set `mode: live` in `CHECKOUTS.md`. The first real payment is the Live evidence.

## 7. CHECKOUTS.md

As in the [Supabase recipe](https://checkouts.md/recipes/supabase.md#7-checkoutsmd), with "Secrets: Edge Function secrets, entered through Lovable's secret form" under Wiring.

## Platform notes

These describe Lovable as it works at the time of writing; if Lovable differs, follow its documentation and keep the rules.

- Secrets entered through Lovable's form are stored as the Supabase project's Edge Function secrets and read with `Deno.env.get('RAILBED_SECRET_KEY')`.
- Never put a Railbed key in a `VITE_` variable or anywhere in `src/`: that is the browser bundle.
- If Lovable shows a secret's value in the chat or in a file, remove it and ask the person to revoke that key in Railbed's Developers page, then create a new one.
