FSD Frontend System Design

Part II β€” The Shield Β· Lesson 3.5

Client-side Security

In one lineThe client is fully controlled by the user β€” it can enforce nothing and keep no secret.

Where you've seen itREACT_APP_API_KEY in a .env file, inlined into main.js at build time, readable by anyone who opens the Sources tab.

  • Time15 min
  • DiagramWhat the user can change vs what the server must re-check
  • Chapter3 Β· Security

Diagram

diagram
WHAT THE USER CONTROLS β€” all of it

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Their machine ──────────────────────────┐
  β”‚  your JS bundle      readable, editable, breakpointable          β”‚
  β”‚  network requests    replayable in curl, headers rewritable      β”‚
  β”‚  localStorage        open in DevTools                            β”‚
  β”‚  React state         editable via extensions                     β”‚
  β”‚  "disabled" button   removed with one DOM edit                   β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό  every request is attacker-shaped
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  YOUR SERVER β€” the only place a rule is actually enforced        β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
diagram
CLIENT CHECK vs SERVER CHECK β€” both, for different reasons

  Client validation   = user experience   (instant feedback)
  Server validation   = security          (the actual gate)

  Hiding an admin button      βœ“ tidy UI
  Blocking /api/admin         βœ“ the real control
diagram
TOKEN STORAGE β€” pick your poison

                      XSS-readable   CSRF-prone   effort
  localStorage             YES           no        low
  memory (JS variable)     YES           no        medium   lost on reload
  HttpOnly cookie          NO            YES       medium   SameSite fixes CSRF

  Best default: HttpOnly + Secure + SameSite=Lax cookie,
                short-lived access token, refresh rotated server-side.

How it works

  1. Assume the bundle is public. Anything in it β€” keys, endpoints, feature flags, business logic β€” is disclosed.
  2. Keep real secrets on the server. If the browser must call a third-party API with a secret, proxy it through your backend.
  3. Publishable keys are fine (Stripe publishable key, Maps key) β€” but restrict them by referrer, origin, and scope in the provider's console.
  4. Re-check every authorisation on the server. Hiding UI is presentation; the endpoint is the boundary.
  5. Don't rely on obfuscation or minification. It slows a reader by minutes, not attackers.

Trade-offs

Frontend canFrontend cannot
Improve UX with fast validationEnforce authorisation
Reduce blast radius (CSP, HttpOnly)Keep a secret
Rate-limit gently (debounce)Prevent request replay
Hide UI for unauthorised rolesPrevent access to the API

Interview angle

"Where do you store the JWT?"

  • Not localStorage if you can avoid it: any XSS reads it instantly.
  • Prefer an HttpOnly; Secure; SameSite=Lax cookie, which XSS cannot read, and handle CSRF separately.
  • Keep access tokens short-lived with server-side refresh rotation, so a stolen token has a small window.

Recap

  • Every client-side check is a hint; every server-side check is the rule.
  • Nothing shipped to the browser is secret β€” proxy anything that must stay private.
  • Token storage is a trade between XSS exposure and CSRF exposure; HttpOnly + SameSite handles both.