# 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.

URL: https://www.back4app.com/blog/add-backend-to-frontend-login-database-payments
Published: 2026-11-02 · Tested: 2026-10-27
Tested on: 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
Publisher: Back4app Engineering

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.

## 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](/blog/blog-assets/past-the-backend-wall/the-wall.png)

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.

```js
// 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:

```text
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.

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:

```js
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:

```text
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.

## 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](https://github.com/templates-back4app/payment-wall) — one command, two shapes of the same shop, and the switch that shows you what each check is actually holding up.
