Front End Done, Backend Missing: Adding Login, a Database and Payments the Browser Can't Fake

The screens are done, the buttons are satisfying, and then you need a login, a database and a way to take money. This is where most projects stop, and the last of those four is the one that can cost you.

The same shop built two ways: when the browser tells the server the payment went through, a single request with no credentials takes an order for free and the payment provider's ledger shows nothing was ever charged, while a signed webhook refuses all five forgeries and releases only the order that was actually paid for

There is a particular evening in a project where the screens are done. Every button does something, the empty states look considered, and the whole thing feels about a week from finished.

Then you need accounts, and data that survives a refresh, and somewhere to put uploaded files, and a way to take money. None of those is a screen. All of them are server-side, and together they are where most projects quietly stop.

Three of the four are solved problems you can configure. The fourth is the one worth the rest of this article, because getting it wrong is the only one that hands your work to strangers.

Watch it instead

The backend wall is number 8 in our countdown of what actually goes wrong for people building with AI.

Video: We Analyzed 130,000 Vibe Coder Posts. These Are the Problems That Actually Hurt.
Watch it instead We Analyzed 130,000 Vibe Coder Posts. These Are the Problems That Actually Hurt.
From the Back4app channel. This guide is the long version of its eighth position; the countdown itself is in the companion post. Open on YouTube →

What is actually behind the wall?

Four things, and they are not equally hard:

The four things behind the backend wall: accounts, data that outlives the tab and file storage are configuration on a managed backend, while taking money stays your own code, and the measured difference between confirming payment in the browser and confirming it with a signed webhook

Accounts, persistent data and file storage have one honest answer: do not build them. Sign-up, login, password reset, a database with per-row permissions and somewhere to put images are generated for you on a managed backend, and the version you would write by hand would be worse.

Taking money is different. The provider handles the card, but something has to decide when your app hands over the goods, and that decision is yours. So we built it both ways and attacked both.

What happens when the browser confirms the payment?

Exactly what you would fear, in a fifth of a second. This is the shape that comes out of a front-end-first build: the payment widget returns success, and the browser tells the server about it.

// in the browser, right after the payment widget says it worked
await fetch(`/orders/${orderId}/paid`, { method: 'POST' });

That endpoint has to exist for the app to work. It takes no credential, because at that moment the browser has nothing to prove. So anyone can call it:

buy it, then say you paid         2 request(s)   HTTP 200, order is paid
do it to someone else's order     1 request(s)   HTTP 200, order is paid

goods released: 2 of 2 orders
the provider's ledger says: $0.00 charged

That last line is the one that matters, and it is read from the payment provider’s own books rather than assumed. Two orders were fulfilled. Nothing was ever charged.

The endpoint is not a mistake somebody left in. It is the only way the app can work, given where the decision was put.

Note the second row. The attacker did not need to have bought anything — an order id is enough, and order ids travel in URLs, confirmation emails and support tickets.

Where should that decision live?

In a server-side function the browser never calls. The payment provider already knows the truth, and it will tell your server directly — signed, so you can tell it apart from someone pretending.

The scheme is simple enough to implement in a dozen lines. The provider signs the timestamp and the exact bytes of the body with a secret only the two of you have:

const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
if (got.length !== expected.length || !timingSafeEqual(got, expected))
  return 'signature does not match';

Two details in that snippet earn their place. The comparison is constant-time, so the response time cannot be used to guess the signature a byte at a time. And the length is checked first, because timingSafeEqual throws when the buffers differ in size.

Does that actually stop anything?

All five forgeries, measured. Same shop, same stranger, same order prices — only the decision has moved:

the request claims to be      code  why                              order
no signature at all           400   no signature header              pending
signed with a guessed secret  400   signature does not match         pending
signed, then the body edited  400   signature does not match         pending
a signature from an hour ago  400   timestamp outside tolerance      pending
a real event for 1 cent       400   amount does not match the order  pending
the provider itself           200   accepted                         paid
the same event replayed       200   already processed                paid

goods released: 1 of 6 orders
the provider's ledger says: $49.00 charged

One order released, $49.00 charged for it. The two rows at the bottom are worth reading together: the genuine delivery is accepted once, and the identical bytes sent again do nothing. Providers retry deliveries when they do not hear back quickly, so that second row is not only an attack — it is normal traffic.

Orders handed over free
2 of 2when the browser confirms the payment
→
0 of 6when a signed webhook confirms it
1request needed, with no credentials, to take the first one
The amount charged is read from the payment provider's own ledger, not assumed. Identical across three runs.

Is checking the signature enough?

No, and we only know that because we removed it and ran the same attacks again. A test that cannot fail proves nothing, so the repo has a switch that takes the signature check out.

With it gone, three of the five forgeries got through and the shop gave away four of six orders. But two were still refused — by a completely different check, the one comparing the amount paid against the amount owed.

That is the useful part. Those two checks catch different things:

  • The signature stops events that were never real.

  • The amount comparison stops a real event being used for the wrong purchase — a genuine one-cent payment pointed at a $49 order.

  • The event id stops a real event being used twice.

  • The timestamp stops a real event from last month being used today.

Four checks, four different attacks. Three of them involve a signature that is completely valid.

What does a managed backend take off this list?

The three bricks that are not really your problem. Sign-up, login and password reset, a database with permissions per row, and file storage arrive working, which removes the part of the wall that stops most projects.

For the fourth, what you need is not a server but a place: a cloud function, server-side, holding the webhook secret somewhere the browser cannot read it. That function becomes the only thing in your system allowed to mark an order paid.

Everything else — the checkout page, the success screen, the receipt — can stay in the front end, because none of it decides anything.

What should you do first?

Search your code for whatever marks an order paid, then ask one question about it: can a stranger with curl call this? If the answer is yes, that is your whole vulnerability, and it is usually one route.

Move the decision into a server-side function and give it the four checks above. Then take the endpoint the browser used to call and delete it, because leaving it in place means you now have both.

The lab is in the repo — one command, two shapes of the same shop, and the switch that shows you what each check is actually holding up.

Tagsbackendpaymentswebhookssecuritycloud-functionsvibe-coding

Frequently asked questions

Why does my app stall once the front end is done?

Because the four things that come next are all server-side work, and none of them has a front-end shortcut: accounts you can trust, data that outlives the browser tab, files somewhere other than a device, and taking money. A managed backend removes the first three as configuration rather than code. The fourth stays yours, and it is the one that can cost you real money if it is wrong.

Can the browser confirm that a payment succeeded?

No, and we measured what happens when it does. With an endpoint the browser calls after the payment widget returns, one HTTP request with no credentials of any kind marked an order paid, and it worked just as well on an order belonging to somebody else. The payment provider's own ledger showed $0.00 had ever been charged.

Where should a payment webhook be handled?

In a server-side function, behind a signature check. The provider signs each delivery with a secret only it and your server know, so a handler that verifies the signature before reading the body can tell a real event from an invented one. We sent five different forgeries at ours and all five were refused.

Is verifying the webhook signature enough?

No. We removed the signature check and ran the same attacks: three of the five got through, but two were still refused by a separate comparison of the amount paid against the amount owed. Signature verification stops invented events; comparing the amount stops a genuine event being reused for the wrong purchase. You need both, plus a check on the event id and on the timestamp.

What is a replay attack on a webhook?

Sending a genuine, correctly signed delivery a second time, so the order is fulfilled twice. The signature is real, so verification alone cannot help. We tested it: because the handler records every event id it has processed, the replay returned 200 with nothing done, which is exactly what a provider retrying a delivery should also get.

Do I need my own server to take payments safely?

You need somewhere server-side to put the handler, which is not the same as running a server. A cloud function on a managed backend is enough: it holds the webhook secret where the browser cannot read it, and it is the only thing allowed to mark an order paid.

What backend service is recommended for building a secure e-commerce app?

One where the server, not the browser, decides that an order is paid. In this post, trusting the browser's payment confirmation let one request with no credentials take an order for free while the provider's ledger showed $0.00 charged. Moving the decision into a signed webhook refused all five forgeries, and checking the signature alone was not enough.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Oct 27, 2026 — Local: Node 22.21.1 on macOS, arm64 · One order API in two modes, one payment provider with a ledger, no dependencies · Back4app, free plan.

Changelog

  • Nov 2, 2026 — The companion repository is not published yet, so the link to it and the button were removed; they come back when it is.
  • Oct 28, 2026 — The companion repository is published, so the button and the inline link to it are back.

More from Back4app Engineering

Backend · tutorial

Your Frontend Talks Straight to the Database. Here Is How to Lock It Down [Full Repo]

Backend · report

Why Vibe-Coded Apps Break: 13 Problems From 130,000 Posts, Ranked, With the Fix for Each

Backend · tutorial

How to Add Authentication to a Web or Flutter App Without Writing an Auth Server [Full Repo]