- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
One API, Three Apps: Keeping a Mobile App and a Web Portal in Sync
About Post
When you deploy a web app, every user gets the new version within minutes. When you release a mobile app, users get it whenever they feel like updating. Which, for some of them, is never.
That one difference shapes everything about building an API that serves both. Kate PMS, the property management system I lead at Kate, has three clients talking to the same backend: a web portal for staff, a staff app, and a tenant app built with React Native. Contracts, payments and maintenance requests flow through all of them.
Keeping them in sync isn't one clever trick. It's a handful of rules that sound obvious and are surprisingly easy to break on a busy day. Here they are.
The real problem: you never have just one version in production
The web portal is easy. It ships with the backend, so the front end and the API always match.
The mobile apps are different. A release goes through app store review. Users have automatic updates turned off, or they're on an older phone, or they simply haven't opened the app in weeks. At any moment, several versions of each app are out there calling your API, and each one expects the API to behave the way it did when that version was built.
So the mental shift is this: the API isn't for the current app. It's for every version still installed.
Rule 1: Change the API by adding, not by editing
The safest change is an additive one. A new field, a new endpoint, a new optional parameter: older apps simply ignore what they don't know about.
The dangerous changes are the ones that look like tidying up:
- Renaming a field from
tenant_nametotenant. - Changing a value's type, like an amount from a number to a string.
- Making an optional request field required.
- Changing what a status value means.
Every one of these works perfectly with the newest app and breaks the older ones, often in ways that only show up as a blank screen for a user you'll never hear from.
In Laravel, API Resources make this discipline easier, because the response shape is defined in one place instead of leaking straight from the model:
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'status' => $this->status->value, // older apps read this
'status_label' => $this->status->label(), // added later, ignored by them
'due_date' => $this->due_date->toDateString(),
];
}
Adding a column to the database doesn't change the API by accident, and renaming one doesn't break it by accident. The resource is the contract.
Rule 2: Version when you truly have to break
Sometimes a change can't be additive: a flow is redesigned, or a response needs a different structure. That's what versioning is for. A prefix like /api/v1 and /api/v2, with separate route groups and resources, lets the new app use the new shape while older apps keep working on the old one.
The cost is that you now maintain two versions. So version rarely, deliberately, and with a plan for retiring the old one. If every small change becomes a new version, you'll end up maintaining a museum.
Rule 3: Know which versions are actually out there
You can't retire an old version safely if you don't know who's still using it. Have every app send its version with each request, in a header such as X-App-Version, and log it.
Now "can we remove the old field?" stops being a guess. You can see whether anyone is still on the version that reads it.
Rule 4: Keep a forced update as a safety valve
Occasionally, you need old versions gone: a serious bug, a payment flow that must change, a legal requirement. For that, the API needs a way to say "this version is no longer supported, please update".
A simplified version of the idea, as Laravel middleware:
public function handle(Request $request, Closure $next): Response
{
$version = $request->header('X-App-Version');
$minimum = config('mobile.min_supported_version');
if ($version && version_compare($version, $minimum, '<')) {
return response()->json([
'message' => 'A new version of the app is required.',
'update_required' => true,
], 426); // 426 Upgrade Required
}
return $next($request);
}
The app treats that response as a signal to show a friendly full-screen "please update" message with a link to the store, rather than a cryptic error.
The key is that this logic has to exist before you need it. A forced-update check you add in version 3 can't help you with users stuck on version 1. Build it into the very first release.
It's also worth having a softer level: a "recommended" version that shows a dismissible banner, so you can nudge people without blocking them.
The rule I'd tattoo on every mobile API: ship the forced-update check in version 1.0, and hope you rarely use it. It's the one feature you can never add retroactively.
Rule 5: The server owns validation and business rules
With three clients, it's tempting to validate in each one. Then the rules drift: the web portal accepts a date the tenant app rejects, and the staff app allows a value the server later chokes on.
The fix is to make the server the single source of truth:
- Form Requests define the rules once. Every client gets the same 422 response with the same field errors, and each app displays them.
- Client-side checks are for user experience only. "This field is required" can be instant in the app, but the server still decides.
- Send lists from the server. Things like maintenance categories or document types come from an endpoint, not hard-coded in the app. Add a new category on the server and every app version shows it, with no store release.
That last point is underrated. Every piece of data you hard-code into a mobile app becomes something you can only change by shipping a new version.
What about over-the-air updates?
With Expo, JavaScript-only changes can ship as over-the-air updates without going through the store, which closes the gap a lot. But changes to native code still need a store release, and users still have to open the app to receive the update. It's a helpful tool, not a replacement for a backwards-compatible API.
Test the contract, not just the code
Feature tests that assert the response shape catch accidental breaking changes before they ship:
$this->getJson('/api/v1/contracts/'.$contract->id)
->assertOk()
->assertJsonStructure(['data' => ['id', 'status', 'due_date']]);
When someone "tidies up" a field name, this test fails in CI, during pull-request review, instead of on someone's phone.
The checklist
- Design the API for every installed version, not just the latest.
- Add fields; don't rename or remove them in place.
- Version only for real breaking changes, and plan the retirement.
- Send the app version with every request and log it.
- Ship a forced-update mechanism from day one.
- Keep validation and lists on the server.
- Test response shapes so breaking changes fail in CI.
If you maintain an API for mobile apps, how do you handle old versions: forced updates, long-lived API versions, or something else entirely?

Be first to comment it...