What is a backend?

Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: The half of a webapp that the user never sees directly — code that runs on a server you control, owns the database, holds the secrets, and decides what the browser is allowed to do.


In plain English

When you visit a webapp, two completely different things are running simultaneously:

  • The frontend — code that runs INSIDE your browser. HTML, CSS, JavaScript. Anyone can read it just by viewing source. Everything in the browser can be tampered with, faked, or replayed by a curious user.

  • The backend — code that runs on a computer somewhere else, that the user never directly touches. Database queries happen there. API keys live there. Business rules (“only admins can delete users”) are enforced there. The user can ASK the backend to do things, but the backend decides whether to.

This split exists for one fundamental reason: the browser cannot be trusted. Anything you put in the browser — a password, a permission check, an API key, a price calculation — can be inspected, modified, or replayed by anyone with developer tools. The browser is the user’s tool, not yours.

The backend is the part you actually own. It runs on your servers (or your hosting provider’s servers, which you’ve configured). It can hold secrets. It can enforce rules without the user being able to override them. It owns the database. It is the source of truth.

In a modern Next.js / Supabase webapp, the line between “frontend” and “backend” is blurry — server components, server actions, and edge functions all live within the same project as the UI code. But the conceptual split is the same: code that runs on the user’s machine vs code that runs on yours.


Why it matters

Three things go wrong when developers don’t internalize this split:

  1. Security disasters. A “secret” API key embedded in a JavaScript file is publicly readable. A permission check (if (user.isAdmin)) written in the browser can be bypassed by changing one line of JavaScript in DevTools. Real security only exists on the backend.

  2. Data corruption. If the browser is allowed to write directly to the database, a user can write any value — including impossible ones. The backend is the chokepoint that validates “this user is allowed to do this action, with these values, right now.”

  3. Architectural confusion. New developers often treat the browser like a place that “runs the app” and the server like a “data warehouse.” That model breaks down fast. The right model is: the backend IS the app; the browser is a view onto it.

The most important question to ask when designing any feature is: “What happens if the user sends arbitrary data here?” If the answer is “we trust them,” you’ve made the wrong choice. The backend’s whole job is to assume nobody can be trusted and verify everything.


The two-host model

Picture a typical webapp running:

┌─────────────────────────────┐         ┌──────────────────────────────┐
│  USER'S MACHINE             │         │  YOUR SERVERS (Vercel etc.)  │
│                             │         │                              │
│  Browser:                   │         │  Backend code:               │
│  - HTML/CSS rendering       │   HTTP  │  - Server components         │
│  - JavaScript               │  ◀──▶   │  - API routes / Server       │
│  - User interactions        │         │     actions / Functions      │
│  - Anything you'd find via  │         │  - Connects to database      │
│    "View source"            │         │  - Holds secrets             │
│                             │         │  - Enforces auth + rules     │
└─────────────────────────────┘         └──────────────────────────────┘
       NOT TRUSTED                                  TRUSTED

Every interaction between user and webapp passes through HTTP (HTTPS, really). The browser sends a request; the backend processes it; the backend sends back a response. The user can see and modify everything on their side. They cannot see what happens on yours.

This is why secrets, business rules, and database access all live on the backend. It’s also why the backend has to validate everything that comes from the browser — because what arrives is whatever the user (or an attacker) decided to send.


What “the backend” actually consists of

A modern webapp’s backend is rarely one monolithic thing. It’s a collection of services:

ComponentWhat it does
Application serverWhere your custom code runs. In Next.js: server components, server actions, route handlers.
DatabasePersistent data storage. Postgres, MySQL, etc.
Authentication serviceUser sign-up, login, sessions, password reset. Supabase Auth, Clerk, Auth0.
File storageImages, documents, large blobs. Supabase Storage, S3, R2.
Cache layerRedis, Memcached, KV stores. Speeds up frequent reads.
Background workersLong-running jobs that shouldn’t block requests. Email sending, image processing.
External APIsStripe for payments, SendGrid for email, Anthropic for AI.

For solo projects on the Bible Quest-style stack, most of these are managed services you don’t operate yourself. You write the application code; everything else is rented from Supabase/Vercel/etc.


Server-side vs client-side rendering

A specific tension in modern webapps: where does HTML get generated?

  • Client-side rendering (CSR) — the browser downloads a small HTML shell + a big JavaScript bundle. JavaScript runs in the browser, fetches data, and builds the page DOM. Classic single-page apps. Fast for navigation between pages once loaded; slow for the very first page load.

  • Server-side rendering (SSR) — the backend generates the full HTML, sends it to the browser. The browser displays it immediately. JavaScript “hydrates” the page afterward to add interactivity. Faster first load; better for SEO.

  • Static site generation (SSG) — the backend generates HTML at BUILD time and serves the same HTML to every visitor. Even faster; perfect for content that doesn’t change per user.

  • Incremental static regeneration (ISR) — a hybrid: HTML cached, regenerated periodically when data changes.

Next.js supports all four modes per-page. The mode you pick determines how much work the backend does per request.

In all four modes, the backend is still trusted code. CSR doesn’t mean “no backend” — it means “the backend mostly serves JSON instead of HTML.” The user-trust split is the same.


A concrete example: a Bible Quest action

When a user clicks “Mark as complete” on a Bible Quest question, here’s what happens:

1. Browser  → Click triggers a server action call
              POST /bible/lesson/12 with payload { questionId: 47, answer: "abc" }
              (Plus the user's session cookie, set after login)

2. Backend  → Receives the request
            → Reads the session cookie
            → Looks up the user in the database
            → Checks: is this user allowed to answer this question? (e.g. not already answered)
            → Validates the answer payload (shape, length, content)
            → Writes to the database: user_progress table, marks question complete
            → Returns updated progress data

3. Browser  → Receives response
            → Updates the UI to show the question as complete

Everything between step 1 and step 3 — the auth check, the validation, the database write — happens on the backend. The browser doesn’t directly read or write the database. If an attacker tried to POST { questionId: 47, userId: "someone-else" }, the backend would reject it: the session cookie says who the user is, not the payload.

This is why Row-Level Security (RLS) on the database is a defense-in-depth measure rather than the primary mechanism — the backend should already have refused the action before the database ever sees it.


The classic “browser is not trusted” examples

A few canonical lessons:

  • Don’t compute prices in the browser. Send the cart contents to the backend; the backend looks up real prices and computes the total. If the browser is allowed to submit { total: 0.01 }, attackers will.

  • Don’t trust JavaScript form validation. Validate in the browser for UX (fast feedback). Validate AGAIN on the backend, because the browser can be bypassed.

  • Don’t hide admin features by just not showing the menu. The user can still send the underlying request. Enforce permissions on the backend per-endpoint.

  • Don’t trust the Origin or Referer headers. They can be forged. Use proper auth tokens or session cookies.

  • Don’t put real secrets in NEXT_PUBLIC_* env vars. Those go to the browser. See environment variables.

The pattern is always: the browser is a UI, not a gatekeeper.


How backend code gets triggered

Three primary trigger types in modern webapps:

1. HTTP requests from the user’s browser

The most common. User clicks button → browser sends HTTP request → backend handles it → response.

In Next.js this means:

  • Server components rendering on initial page load
  • Server actions invoked when forms or buttons submit
  • Route handlers at /app/api/*/route.ts for explicit APIs

2. Scheduled jobs (cron)

Code that runs on a schedule, no user involved. “Every night at 3am, send a digest email to subscribers.” On Vercel: Cron Jobs. On other platforms: a scheduler + a function.

3. Webhooks from external services

Stripe POSTs to your /api/webhooks/stripe when a payment succeeds. GitHub POSTs to /api/webhooks/github when someone opens a PR. Your backend accepts the POST and acts on it.

These three trigger types cover almost every backend action in a typical webapp.


Common gotchas

  • The browser sees everything you send to it. “Hidden” API keys, “hidden” permission flags, even <input type="hidden"> values — all visible. Anything sensitive must stay on the server.

  • CORS is a browser-enforced rule, not a server security measure. CORS prevents some cross-origin browser requests but does NOT stop a script outside a browser from hitting your API. Real auth is required regardless.

  • A “private” GitHub repo is not a security barrier. Once code is built and deployed, hard-coded secrets in the build output may leak. Always use env vars.

  • Server components in Next.js can leak secrets if you’re not careful. Importing a server-only utility into a client component pulls its code into the client bundle. Use the server-only package to enforce the boundary.

  • API routes have to validate everything from the client. Use a schema validator (Zod, Valibot) at the entry point. Untrusted data + a typed signature is a lie.

  • The same data shows up at multiple layers — keep validation consistent. Browser form validation, server action validation, database constraints, RLS policies. They should all agree on what’s valid.

  • Cold starts hit the backend, not the browser. A serverless function that hasn’t run in 5 minutes takes longer on the first request. Users feel this. Architectures (Edge runtime, keep-warm, Fluid Compute) exist to mitigate it.

  • Backend errors must not leak internals. A stack trace in a 500 response can reveal table names, env var names, library versions. Catch and log; return a generic error to the browser.

  • Idempotency matters. A user might click “submit” twice. A webhook might be retried. Design backend endpoints so that the same call repeated has the same effect — or use idempotency keys to dedupe.

  • The backend’s clock is the authoritative one. Never trust a timestamp from the browser. If you need “when did this happen?”, set it on the backend.

  • Database connections are expensive. Open one per request and you’ll exhaust limits. Use a connection pool (Supabase’s pgbouncer, Hyperdrive, Prisma’s Data Proxy).

  • Logging on the backend is your time machine. When something breaks in production, server logs are often the only diagnostic. Log enough to understand what happened — but never log raw passwords, full tokens, or PII you don’t need to debug.

  • The backend can be a multi-tenant shared environment. On serverless, your function instance may handle requests for different users in sequence. Don’t use module-level globals to cache per-user data — you’ll leak across users.

  • “Free tier” backend hosting has hard limits. Function execution time, memory, request count. Hitting them either fails the request (Vercel) or runs up bills (AWS). Know your limits.

  • The backend is also a target. Brute-force login attempts, DDoS, scraper bots, SQL injection probes. Rate-limit aggressively at the edge (Cloudflare, Vercel WAF, your code).

  • Two backends are harder than one. A “microservices” architecture before you need it adds latency, complexity, and failure modes. Start with one backend; split only when you genuinely need to.

  • Don’t confuse “doing it in TypeScript on the server” with “doing it well.” Node.js can run anything, but some workloads (video transcoding, scientific compute) need different runtimes. Recognise when you’re forcing the wrong tool.


When the backend isn’t on Node.js

Most modern JS-stack webapps run their backend in Node.js or a Node-compatible runtime (Bun, Deno, edge V8 isolates). But the concept of a backend predates Node by decades. Mature backends commonly run on:

  • Python — Django, FastAPI, Flask. Great for data-heavy or AI work.
  • Ruby — Rails. Famous for developer productivity.
  • Java / Kotlin — Spring. Big-enterprise default.
  • Go — performance-critical services, infrastructure tools.
  • Rust — newer; very fast, very strict.
  • PHP — Laravel, WordPress. Powers a vast share of the web.
  • C# — .NET, ASP.NET. Microsoft ecosystem.

For George’s stack (Next.js + Supabase + Vercel), the backend is Node.js (Vercel functions) + Postgres (Supabase). But the principles in this entry apply identically regardless of language.


See also


Sources