The Challenge
Mobile API backends have a fundamentally different security profile than server-to-server APIs. The client is untrusted, runs on hardware you don't control, can be decompiled by adversaries, and communicates over networks that may be intercepted. Every API endpoint you expose is potentially accessible to an attacker who has reverse-engineered the app binary — meaning your backend must validate everything, trust nothing from the client, and assume that any client-side security control can be bypassed.
Authentication in mobile contexts is complicated by the need to balance security with user experience. Users who are forced to re-authenticate too frequently abandon apps; users whose sessions never expire have their accounts compromised by stolen devices. Token rotation, background refresh, and graceful re-authentication flows are engineering problems that have security consequences if implemented incorrectly — a leaked refresh token that works indefinitely is functionally equivalent to a leaked password.
Scalability and security are frequently in tension in API design. Rate limiting at the right granularity (per-user, per-IP, per-endpoint) requires Redis or a similar shared state layer and adds latency. Structured logging that enables incident response requires more code than unstructured print statements. Input validation that prevents injection attacks requires schema libraries and error handling. We make these engineering investments upfront, because retrofitting them into an existing API with active users is significantly more expensive and risky than building them in from the start.
Signs You Need This
- You're building a mobile app that handles user accounts, transactions, or any personal data
- Your current API returns raw database errors or stack traces in HTTP responses
- You have no structured logging — incidents would require reading through raw application logs
- Authentication tokens don't expire or are shared across all environments with the same signing key
- Input validation is done only in the mobile app, with no server-side schema validation
- There's no rate limiting on authentication endpoints — a bot could try millions of passwords undetected
How We Approach It
Threat Model & Architecture Design
Every backend we build starts with a formal threat model documented using the STRIDE framework (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege). We enumerate every API endpoint, the data it processes, and the trust boundaries it crosses. For mobile backends, this always surfaces the same core questions: how are devices authenticated vs. users, what happens if a JWT signing key is compromised, how do we detect and respond to credential stuffing, and what data can be inferred from API response timing even without direct data access. Threat model decisions are documented and become the basis for the security requirements that guide implementation.
Authentication Architecture & JWT Implementation
We implement asymmetric JWT (RS256 or ES256) with separate signing keys per environment so a compromised staging key doesn't affect production. Access tokens have a 15-minute expiry; refresh tokens have a configurable expiry (typically 30 days) with sliding window renewal and one-time use — each refresh issues a new refresh token and invalidates the old one, so token theft is detectable. Refresh token families are stored server-side so logout from one device can invalidate all sessions for a user. We implement the OAuth 2.0 device authorization flow for TV and desktop companions if needed, and PKCE for any web OAuth flows to prevent authorization code interception.
Input Validation & API Security Layer
Every API endpoint validates its request payload against a strict schema using Zod (TypeScript) or Pydantic (Python) — both libraries that do runtime type checking, not just compile-time inference. Unknown fields are stripped from requests by default rather than passed through to the database layer, preventing mass assignment attacks. SQL queries use parameterized statements via Prisma ORM or SQLAlchemy — raw string interpolation in queries is a build-time lint error. File upload endpoints validate MIME type, extension, and file size on the server side and store uploads in S3 with a non-public bucket and pre-signed URL access rather than a publicly accessible URL.
import { z } from 'zod';
// Strict schema — unknown fields are stripped automatically
export const CreateUserSchema = z.object({
email: z.string().email().max(255).toLowerCase(),
password: z
.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[0-9]/, 'Must contain a digit')
.regex(/[^a-zA-Z0-9]/, 'Must contain a special character'),
displayName: z.string().min(1).max(100).trim(),
// role intentionally excluded — assigned server-side, never from client
}).strict(); // .strict() rejects any extra fields (prevents mass assignment)
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
// In the route handler:
app.post('/v1/users', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.flatten().fieldErrors,
// Never return the raw Zod error — it may leak schema details
});
}
const validatedInput = result.data; // Type-safe, stripped unknown fields
// ... create user with validatedInput
});
Rate Limiting & Abuse Prevention
Rate limits are implemented using Redis with a sliding window algorithm — more accurate than fixed window limits and harder to game by timing requests at window boundaries. We configure limits at multiple granularities: global per-IP limits to prevent DDoS, per-user limits on sensitive operations, and per-endpoint limits calibrated to the legitimate usage pattern of each endpoint. Authentication endpoints have strict limits with exponential backoff and automatic temporary blocks after repeated failures. We implement a lightweight anomaly detection layer that flags accounts with unusual patterns (login from new country after long inactivity, sudden burst of API calls, access to resources that don't belong to the user) for review rather than automatic blocking.
Structured Logging & Observability
Every API request generates a structured log entry with: request ID, user ID (if authenticated), endpoint, HTTP method, status code, latency, request size, response size, and client device/version information. Sensitive fields (passwords, tokens, credit card numbers, SSNs) are never logged — we maintain a blocklist of field names that are automatically redacted before log output. Logs are shipped to CloudWatch Logs in JSON format for queryability. We configure CloudWatch Insights queries for the most important incident response scenarios: all requests by a specific user, all 4xx errors in a time window, endpoints with latency above a threshold, and failed authentication attempts by IP. Alerting is configured for error rate spikes, authentication failure bursts, and latency degradation.
Redis Sliding Window Rate Limiter
Fixed-window rate limits are bypassed by timing requests at window boundaries. We use a sliding window implemented with a Redis sorted set — accurate, atomic, and horizontally scalable:
import { redis } from '../lib/redis';
interface RateLimitOptions {
windowMs: number; // sliding window duration in ms
max: number; // max requests in the window
keyPrefix: string;
}
export async function slidingWindowRateLimit(
identifier: string, // user ID or IP — never trust client X-Forwarded-For
opts: RateLimitOptions
): Promise<{ allowed: boolean; remaining: number; resetIn: number }> {
const now = Date.now();
const windowStart = now - opts.windowMs;
const key = `${opts.keyPrefix}:${identifier}`;
const pipeline = redis.pipeline();
// Remove requests outside the sliding window
pipeline.zremrangebyscore(key, 0, windowStart);
// Count requests in current window
pipeline.zcard(key);
// Add current request with timestamp as score
pipeline.zadd(key, now, `${now}-${Math.random()}`);
// Set TTL to window duration (auto-cleanup)
pipeline.pexpire(key, opts.windowMs);
const results = await pipeline.exec();
const currentCount = (results[1][1] as number);
if (currentCount >= opts.max) {
return { allowed: false, remaining: 0, resetIn: opts.windowMs };
}
return { allowed: true, remaining: opts.max - currentCount - 1, resetIn: opts.windowMs };
}
// Usage on login endpoint — strict 5 attempts per 15 minutes per user identity
app.post('/v1/auth/login', async (req, res) => {
const { email } = req.body;
const limit = await slidingWindowRateLimit(email, {
windowMs: 15 * 60 * 1000,
max: 5,
keyPrefix: 'login'
});
if (!limit.allowed) {
return res.status(429).json({ error: 'Too many login attempts. Try again later.' });
}
// ... proceed with authentication
});
Tools We Use
Our stack choices prioritize type safety, security-by-default library choices, and operational observability.
Common Mistakes We Prevent
- Returning raw database errors or ORM exceptions in API responses — error messages that include table names, column names, or stack traces are a free reconnaissance gift to attackers mapping your data model
- Using the same JWT signing key across all environments — a key leaked from a staging or CI environment is also the production signing key, enabling token forgery in production
- Implementing rate limiting at the application layer without Redis shared state — each instance of a horizontally scaled API maintains its own in-memory counter, so a limit of "100 requests per minute" with 10 instances is effectively 1,000 requests per minute
- Logging request payloads wholesale without a sensitive field redaction layer — a single log line containing a user's password or payment token is a compliance violation and a security incident waiting to happen
Key principle: Security in an API backend is not a feature you add at the end — it's a set of architectural decisions that become increasingly expensive to retrofit. Input validation schemas, JWT key rotation strategies, and audit logging are 10x cheaper to build correctly on day one than to add correctly to an existing system with live users and running traffic.
What You Get
- Production-ready API backend with documented security architecture and threat model
- Asymmetric JWT authentication with per-environment signing keys, rotation procedures, and refresh token families
- Schema-validated request handling with sensitive field stripping and parameterized database queries
- Redis-backed rate limiting with sliding window algorithm and per-endpoint configuration
- Structured JSON logging with sensitive field redaction and CloudWatch Insights query library
- AWS API Gateway integration with WAF rules and throttling configuration
- Security test report and handover documentation including operational runbook
Timeline & What to Expect
After handover, your team owns the full codebase with comprehensive documentation of every security decision and why it was made. We provide a 30-day support window for questions, and we're available for a follow-up security review after 6 months as the API has grown with real usage patterns that may surface new requirements.
Frequently Asked Questions
Which tech stack do you use?
We work with Node.js/TypeScript and Python/FastAPI as our primary backends — both have mature security libraries and strong typing support that makes security-correct code easier to write and review. We default to TypeScript + Node.js for most mobile backends because of the ecosystem familiarity and the quality of JWT and validation libraries. If your team has a strong existing preference or expertise in a different stack, we adapt — the security patterns we implement are language-agnostic even if the specific libraries differ.
How do you handle breaking API changes for mobile clients?
We implement API versioning (URL path versioning: /v1/, /v2/) from the start so breaking changes can be introduced in a new version while the old version remains available for older app versions still in the wild. We also implement a minimum supported version header check — the API can return a 426 Upgrade Required for app versions below your support threshold, prompting users to update. This is defined in the app version manifest, not hardcoded, so you can change the minimum version without a deployment.
Can this scale to millions of users?
The architecture is designed to scale horizontally — stateless application servers, Redis for shared rate limit state, PostgreSQL with read replicas for read-heavy workloads, and AWS API Gateway which scales automatically with traffic. We implement database connection pooling (PgBouncer or RDS Proxy) to handle connection limits at high concurrency. We load test the rate limiting layer specifically during the engagement to verify it holds under burst traffic without false-positives for legitimate users.
When This Is the Right Fit
This engagement is right for you if you're building a new mobile app backend from scratch and want to avoid the expensive security retrofit that typically happens 6–12 months post-launch, or if you have an existing backend that has grown organically and has known security gaps (missing input validation, no rate limiting, unstructured logs) that you want to address systematically before a major user growth phase or compliance audit.
This is not the right fit if you need a fully managed backend service (Firebase, Supabase, AWS Amplify) rather than a custom API. Managed services have their own security properties and limitations — we can advise on configuration and security review of those platforms, but building on top of a managed service is a different engagement than building a custom backend from scratch.