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 LabA 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:
{
"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:
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.
UNTRUSTED TRUSTED ENFORCEMENT
mobile client ---- request ----> API identity + authorization
local state business rules
embedded config transactional databaseTLS 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:
{
"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:
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.
Where this appears in my projects
A circular puzzle game that rethinks falling-block gameplay across a radial 2D board and a fully rotatable 3D globe, with handcrafted levels, progression, online services, and a cosmetic economy.
Wordle-style web game for developers where players identify a hidden technology using multi-attribute feedback and directional year hints.
Related Tutorials
Interleave two transactions and see lost updates, snapshots, row locks, deadlocks, and PostgreSQL serialization failures as they happen.
Retries are normal. Idempotency makes important API operations safe to retry without pretending distributed systems can guarantee magic exactly-once execution.
Validating every input at the edge, rate limiting without a database, and using signed state so a client can hold data it cannot forge.
Follow fetch through DNS, connection security, edge infrastructure, authentication, cache, database, serialization, retries, and the final UI update.