Published September 5, 2026
7 min read
Advanced
Backend & CloudInteractive visualizer

Why You Should Never Trust the Client: Server-Authoritative App Architecture

Use trust boundaries, authorization, validation, idempotency, transactions, and server-owned rules to keep mobile games and APIs correct under tampering and retries.

Trust Boundary Simulator

Open in Engineering Lab
Loading visualizer...

A mobile app runs on a device its owner controls. They can inspect network traffic, alter local storage, replay an old request, call an endpoint without using your UI, or run a modified build. The backend must treat every client value as a claim, not a fact.

Games make the principle obvious because coins and rewards are easy to imagine, but the same boundary protects subscription entitlements, coupon redemption, inventory, permissions, votes, account ownership, and financial state.

This tutorial explains defensive architecture. The examples are intentionally local and illustrative; they are not instructions for attacking a real service.

The dangerous payload

Suppose a client sends:

JSON
{
  "levelCompleted": 100,
  "reward": 999999
}

Schema validation can prove that both values are numbers in an allowed range. It cannot prove that the user unlocked level 100, completed it, or deserves that reward. Zod, JSON Schema, and TypeScript validate representation; trusted business state validates truth.

An insecure handler effectively does this:

TypeScript
const claim = ClaimSchema.parse(await request.json());
await db.players.update(userId, {
  coins: current.coins + claim.reward,
  completedLevel: claim.levelCompleted,
});

The request is well-typed and still completely unsafe. The server has delegated its authority to the caller.

Draw the trust boundary first

Everything shipped in a mobile binary belongs on the untrusted side: UI logic, local save data, cached configuration, API URLs, and values named “secret.” Obfuscation may increase reverse-engineering effort, but it does not turn a distributed credential into a server secret.

TEXT
UNTRUSTED                         TRUSTED ENFORCEMENT
mobile client  ---- request ----> API identity + authorization
local state                         business rules
embedded config                     transactional database

TLS protects the request from network observers and modification in transit. Request signing can authenticate a device or client installation under a defined key model. Neither makes a compromised client honest about game state. Decide exactly what each credential proves.

Send intent, not the answer

The client should send the minimum information needed to request a transition:

JSON
{
  "levelId": 12,
  "idempotencyKey": "01J..."
}

The server derives the user from the authenticated session, loads authoritative progress, checks that level 12 is currently playable, applies server-owned completion rules, calculates the configured reward, and commits the result.

This does not require making every game simulation fully server-hosted. Authority is a spectrum. A single-player mobile game may simulate moment-to-moment play locally and ask the server to validate economically important transitions. A competitive real-time game may require authoritative input processing, time rules, and state reconciliation. Choose the boundary from the threat and product model.

Authentication is not authorization

Authentication establishes an identity: this token corresponds to user 42. Authorization checks whether that identity may perform this operation on this resource. An authenticated request containing userId: 17 must not gain access merely because the JSON is valid.

Derive ownership from the credential where possible:

TypeScript
const session = await authenticate(request);
const input = CompleteLevelSchema.parse(await request.json());

const player = await db.players.findByUserId(session.userId);
authorizeLevelAttempt(player, input.levelId);

In Supabase, Row Level Security can enforce row access close to data. Service-role code can bypass those policies, so a Cloudflare Worker using privileged credentials inherits the responsibility to authenticate, authorize, validate, and limit every operation.

Client validation is still useful

Client-side validation provides immediate feedback, prevents accidental bad requests, and makes the UI pleasant. It is UX, not security. Server validation is still mandatory because callers can bypass the client.

Validate layers separately:

  • Shape: required fields, types, lengths, and accepted enum values.
  • Identity: the credential is valid and maps to an active subject.
  • Authorization: the subject may operate on this resource.
  • State transition: prerequisites and invariants permit this change now.
  • Outcome: rewards and resulting state are calculated from trusted rules.

Replay and duplicate delivery

A captured valid request may be sent again. Honest clients also create duplicates through retry, double taps, background resumption, and lost responses. A timestamp alone does not reliably solve this: clocks differ, validity windows permit replay, and strict windows reject legitimate delayed traffic.

Use a unique claim identity or idempotency key tied to the authenticated user and operation. Enforce uniqueness durably and commit the deduplication record in the same transaction as the reward. A module-level Set inside a Cloudflare Worker is not a durable guarantee across isolates or restarts.

The idempotency tutorial covers request fingerprints, concurrent duplicates, stored responses, and expiry policy.

Race conditions require atomic enforcement

Two requests can both read “unclaimed,” both calculate a reward, and both write it. A read followed by a later write is not safe merely because each query succeeds.

Use database constraints, conditional updates, locking, or transactions that preserve the invariant under concurrency. For example, a unique constraint on (user_id, reward_id) can make only one claim record win. The transaction then applies coins and records the claim together.

The exact mechanism depends on the data model and isolation behavior. Write the invariant—“one reward per user per level”—before choosing the primitive.

Rate limiting is a guardrail, not authority

Rate limits bound abuse, protect dependencies, and reduce accidental storms. They do not prove a request is valid. A patient attacker can remain below a limit; a shared IP limit can punish legitimate users.

Combine identity-aware and broader limits where appropriate, store counters in a coordination system that matches the required consistency, and return useful retry information. The Cloudflare Workers security guide compares platform rate limiting, KV, and Durable Object tradeoffs.

Signed requests and signed state

A server signature can let a client carry data it cannot forge. The server signs canonical state with a key that never enters the app and verifies it when the state returns. This can reduce storage for some workflows, but it does not prevent replay unless the signed claims include and enforce expiry, nonce, version, audience, and one-time-use semantics where required.

A client-held signing secret is different: once extracted, it may allow arbitrary signatures. Platform attestation can raise confidence about app/device integrity, but availability, bypass resistance, privacy, and false positives vary. Treat it as one signal, not the only authorization rule.

Failure and observability

Return stable error categories without exposing sensitive internals: unauthenticated, forbidden, invalid transition, duplicate, rate limited, or temporarily unavailable. Log a correlation ID, authenticated subject, rule decision, and outcome without storing tokens or excessive personal data.

Security events need product behavior too. A rejected duplicate may return the original successful result. An expired session may refresh and retry. An impossible transition may require telemetry or moderation, but the client should receive a safe response.

Where this appears in shipped apps

In Spheriz, economically meaningful operations cross from a React Native/Expo client through a Cloudflare Worker to backend state; the client presents gameplay, while trusted services enforce economy rules. DevGuess uses signed progress state and validated edge endpoints for a different point on the authority spectrum.

The surrounding network path is explained in What Happens When Your App Makes an HTTP Request?. Together, the two tutorials show why a protected connection is only the start: the server must still decide which claims deserve trust.

Key takeaways

  • A client-controlled device cannot be the authority for valuable state.
  • Schema validation proves shape, not whether a business claim is true.
  • Authenticate identity, authorize the action, then validate the state transition.
  • Derive identity, rewards, prices, and permissions from trusted sources.
  • Make duplicates and concurrent requests safe with durable idempotency and transactions.
  • Secrets embedded in a mobile app should be treated as public.
  • Rate limiting and signatures supplement server-owned rules; they do not replace them.