A Paystack checkout can show Payment successful while your application still has no safe reason to unlock the product, mark an invoice paid or issue value.
That is because payment UX and payment state are different things.
Paystack’s own documentation says a callback URL being visited does not prove that the transaction succeeded. It recommends verifying the transaction server-side, and it recommends webhooks over callbacks or polling for reliable status updates.
That makes the right architecture for a Lovable + Supabase app fairly clear:
Browser starts checkout → trusted server initializes/validates → Paystack processes payment → webhook/verification confirms state → database changes idempotently → UI reads the verified state.
I have spent a lot of product-building time thinking about payments as state transitions rather than green buttons. That distinction matters even more in AI-assisted development because it is easy to prompt a convincing checkout flow before defining what “paid” is allowed to change in the system.
1. Keep the Paystack secret key off the client
Paystack’s Transaction API requires a secret-key bearer token for privileged requests. Lovable’s current FAQ also warns not to place sensitive API keys directly in the client/prompt output and recommends server-side secrets with Lovable Cloud or Supabase Edge Functions.
So do not put your Paystack secret key in browser JavaScript.
For a Supabase-backed Lovable app, a sensible pattern is a Supabase Edge Function or another trusted server endpoint that:
- Authenticates the user where appropriate.
- Looks up the product/order/obligation being paid.
- Calculates or verifies the expected amount server-side.
- Generates or reserves a unique transaction reference.
- Calls Paystack with the secret key.
- Returns only the safe checkout information the browser needs.
The browser should not be the authority for the amount simply because a form field contains it.
2. Create a payment record before or during initialization
Your database needs something stable to reconcile against Paystack.
At minimum, a payment attempt can track:
- internal payment ID;
- Paystack reference, unique;
- user/customer/order/obligation ID;
- expected amount;
- currency;
- status such as pending/success/failed/reversed where your model needs it;
- provider transaction ID if stored;
- verified/paid timestamp;
- fulfilment state where separate.
The important part is that reference and business context exist before a success screen tries to invent meaning.
3. Do not fulfil from the callback alone
Paystack’s Accept Payments documentation is explicit: after redirect, retrieve the reference and call the Verify endpoint before delivering value.
The callback is useful for user experience. It can tell your frontend which transaction to check.
It should not be the only source of truth because the customer’s browser can close, lose network, revisit a URL or fail before your app updates.
4. Use webhooks for reliable server-to-server updates
Paystack recommends webhooks because Paystack controls the server-to-server delivery rather than depending on the customer’s device.
Your webhook should:
- Receive the raw/request payload correctly.
- Validate that the event came from Paystack.
- Find the payment by provider reference.
- Confirm the event matches the payment you expected.
- Update state idempotently.
- Return
200 OKpromptly.
Paystack’s current Webhook documentation says events that are not acknowledged with 200 OK are retried. That means duplicate delivery is normal behavior you must design for, not an exceptional bug.
5. Verify the webhook signature
Paystack sends an x-paystack-signature header. Its documentation says the value is an HMAC SHA512 signature of the event payload using your secret key.
Verify that signature before trusting the event.
This check belongs server-side because the secret key used to calculate the expected signature must remain secret.
Do not tell Lovable “if the payload says success, mark it paid.” Tell it the security rule too.
6. Make success idempotent
Suppose Paystack sends the same successful event twice because your first acknowledgement timed out.
Your application should not:
- issue two receipts;
- add wallet credit twice;
- create two subscriptions;
- settle the same invoice twice;
- send two fulfilment jobs that both mutate stock.
A strong model makes the provider reference unique and makes the transition from pending → successful safe to repeat.
The second copy of the same event should effectively say, this transaction is already applied.
Paystack’s Verify Payments documentation also warns against double fulfilment when verification and webhooks are both used.
7. Verify amount, currency and reference—not only status
A success status is only one part of the business rule.
Before fulfilment, compare the verified transaction with what your server expected:
- reference;
- amount;
- currency;
- intended customer/order context;
- current internal state.
If your app expected ₦50,000, a successful but unrelated ₦500 transaction should not satisfy the obligation just because the provider returned success.
The exact fields you enforce depend on your product, but the rule is general: verify the transaction against your own expectation.
8. Decide what the database transition means
Do not let “payment successful” trigger ten unrelated writes scattered through frontend code.
Model the consequence.
For example:
- payment attempt becomes confirmed;
- invoice balance is reduced;
- allocation is recorded;
- receipt becomes issuable;
- access entitlement is granted;
- fulfilment job becomes eligible.
Where several writes must stay consistent, use a transaction or a server-side operation designed to preserve the invariant.
This is one of the reasons I treat AI-generated interfaces as only one layer of product engineering. The important question is often not “can it call Paystack?” but what must never become inconsistent when Paystack calls us twice?
9. Test failure paths before going live
Test more than the happy path:
- customer closes checkout;
- transaction remains pending;
- callback never returns;
- webhook arrives twice;
- webhook arrives before the browser callback;
- callback arrives before webhook;
- amount does not match;
- signature is invalid;
- your webhook handler temporarily fails;
- the same reference is submitted again.
If your model survives those, the green success screen becomes the easy part.
A simple architecture
| Layer | Responsibility |
|---|---|
| Lovable UI | Start checkout, show pending/success state from backend |
| Supabase/Auth | Identify user and protect records |
| Edge Function/server | Hold secret, initialize and verify Paystack |
| Paystack | Process payment and send signed events |
| Webhook handler | Validate event and apply idempotent state change |
| Database | Preserve payment/reference/business invariants |
This architecture is useful beyond Nigeria. The same principles apply to Stripe, Flutterwave and other payment systems even though their APIs and signatures differ.
Why this is a good AI-builder problem
A prompt can generate a Paystack button quickly. The production work is defining the security and state rules around that button.
That is where product thinking and engineering judgment still matter.
If you are building a Lovable/Supabase product that needs Paystack and you want the payment model reviewed before real users depend on it, contact me.
Related: Lovable + Supabase troubleshooting and the AI-built app production checklist.



