By • Published • Updated

Laravel as a Backend for Mobile Apps (2026): API Design That Survives App Store Releases

Laravel is a strong backend for iOS, Android, Flutter, and React Native apps when the API is designed around one fact: you cannot force users to update. Version every route, return one predictable JSON shape, issue revocable per-device tokens, paginate with cursors, and ship a minimum-supported-build check from the very first release.

A web frontend and its API deploy together, so a breaking change is fixed in one release. A mobile client is different: once a build is approved and installed, it keeps calling your API exactly as it was compiled to, possibly for years. Every decision below follows from that constraint. This guide covers the design side; for hardening the same API, pair it with the Laravel API security checklist.

Is Laravel a good backend for a mobile app?

Yes, for most product apps. Laravel gives a mobile team authentication, queues, scheduled jobs, notifications, file storage, validation, and an ORM out of the box, so a small team can ship a complete API without assembling a framework. It is a weaker fit for workloads dominated by long-lived connections or heavy computation.

Where it fits well:

  • Transactional product apps — accounts, orders, bookings, content feeds, subscriptions, admin workflows. This is the bulk of what mobile apps need from a server.
  • Small teams who want one conventional codebase that a new hire can read on day one.
  • Apps with background work — sending push notifications, processing uploads, syncing with third-party systems — because queues and the scheduler are first-party.

Where I would look elsewhere, or add a separate service:

  • Real-time systems where WebSocket fan-out is the core product rather than a feature.
  • CPU-heavy pipelines such as media transcoding or model inference, which belong in dedicated workers.
  • Teams whose entire skill set is in another ecosystem; the best backend is one your team can operate.

How should a Laravel API shape JSON for mobile clients?

Return every resource through an API Resource class and every error through one envelope. Mobile decoders are strict: Swift’s Codable and Kotlin serializers fail on unexpected types, so a field that is sometimes a string and sometimes null, or an error that is sometimes HTML, becomes a crash or a blank screen in production.

Laravel API Resources decouple the response from the database schema, which is exactly what an API serving old app builds needs. Rename a column and the resource keeps emitting the old key.

class OrderResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->public_id,          // never the auto-increment key
            'status' => $this->status->value,  // backed enum, stable strings
            'total' => [
                'amount' => $this->total_minor, // integer minor units, not floats
                'currency' => $this->currency,
            ],
            'updated_at' => $this->updated_at->toIso8601String(),
        ];
    }
}

Rules that keep decoders happy across releases:

  • Additive changes only within a version. Adding a field is safe; removing, renaming, or changing a type is a new version.
  • Money as integers with a currency code, and dates as ISO 8601 with an offset.
  • Enums as strings, and clients that map unknown values to a fallback case instead of failing the whole decode.
  • JSON for every error, including 404, 419, 429, and 500. Make sure API routes never fall through to an HTML error page.

A single error shape means one decoder and one error-handling path in every client:

{
  "error": {
    "code": "order_not_cancellable",
    "message": "This order has already shipped.",
    "fields": {}
  }
}

The code is a stable, machine-readable string the app can branch on. The message is display text that you can change server-side without shipping a build.

Which authentication should a mobile app use with Laravel?

For a first-party app, use Laravel Sanctum API tokens: one token per device, named after the device, scoped with abilities, and revocable by deleting a row. Reach for Passport only when you need an OAuth2 server for third-party clients. Self-contained JWTs make revocation harder than it needs to be.

Sanctum vs Passport vs JWT for mobile apps

FactorSanctum tokensPassport (OAuth2)JWT package
Token typeOpaque, stored hashed in the databaseOAuth2 access and refresh tokensSigned, self-contained token
Revoke one deviceDelete the token rowRevoke the token and its refresh tokenNeeds a denylist until expiry
Refresh flowNot built in; use expiry plus re-login or a custom rotate endpointBuilt inProvided by the package
Third-party clientsNot designed for itYes, including authorization code with PKCENot designed for it
Setup and upkeepLow, first-partyHigher: clients, grants, keysModerate, community-maintained
Best fitYour own iOS, Android, Flutter, or React Native appPlatforms where partners need delegated accessStateless verification across several services

Issue the token from a login endpoint that takes a device_name, so users can see and sign out individual devices, and so a lost phone can be revoked without logging out everything else:

$token = $user->createToken(
    $request->input('device_name'),    // e.g. "iPhone 16 - iOS"
    ['orders:read', 'orders:write']    // abilities checked per route
);

return ['token' => $token->plainTextToken];

On the device and server side of the lifecycle:

  • Store tokens in the platform keystore: the iOS Keychain, and Android Keystore-backed storage. Never in UserDefaults, plain shared preferences, or AsyncStorage.
  • Set an expiry. Sanctum tokens do not expire unless you configure it. A rolling expiry, renewed on use, keeps active users signed in while dormant tokens die.
  • Revoke on risk events: password change, account recovery, or a user signing out “all devices” should delete the relevant token rows immediately.
  • Treat a 401 as a state, not an error. The client should clear the stored token and return to sign-in cleanly rather than retrying in a loop.

For the client side of the same problem, the mobile banking security checklist covers token storage, session handling, and the controls a security review expects in the app.

How do you version a Laravel API so old app builds keep working?

Put the version in the URL, starting at /api/v1 on day one, and only make additive changes inside a version. Ship a public configuration endpoint that returns the minimum supported build, so you can prompt or force an update before you remove anything an old build depends on.

// routes/api.php
Route::prefix('v1')->group(function () {
    Route::get('/app-config', AppConfigController::class);   // public, cacheable
    Route::post('/auth/token', [AuthController::class, 'store'])
        ->middleware('throttle:auth');

    Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
        Route::get('/orders', [OrderController::class, 'index']);
        Route::delete('/auth/token', [AuthController::class, 'destroy']);
    });
});

The client sends its platform and build number on every request and checks the configuration endpoint at launch. A Swift version using URLSession:

struct AppConfig: Decodable {
    let minimumSupportedBuild: Int
    let latestBuild: Int
    let updateUrl: URL
}

func fetchAppConfig() async throws -> AppConfig {
    var request = URLRequest(url: URL(string: "https://api.example.com/api/v1/app-config")!)
    request.setValue("ios", forHTTPHeaderField: "X-Client-Platform")
    request.setValue(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String,
                     forHTTPHeaderField: "X-Client-Build")

    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }

    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    return try decoder.decode(AppConfig.self, from: data)
}

A deprecation policy that holds up:

  • Log the client build on every request. You cannot retire a version without knowing which builds still call it.
  • Run versions side by side. v2 controllers can reuse the same services and models; only the resources and request validation differ.
  • Soft prompt, then hard block. Raise a “recommended update” first, and only raise minimum_supported_build once usage of the old build is negligible.
  • Return a specific error code, such as client_update_required, from retired endpoints so even an old build can show something sensible.

How should pagination and sync work for offline-first apps?

Use cursor pagination for lists and an updated_since parameter for sync. Offset pagination skips or duplicates rows when data changes between page requests, which happens constantly on a phone with a flaky connection. Cursors are stable, and delta sync means the app downloads only what changed.

$orders = $request->user()->orders()
    ->withTrashed()   // include soft-deleted rows as tombstones
    ->when($request->filled('updated_since'), fn ($query) =>
        $query->where('updated_at', '>', $request->date('updated_since')))
    ->orderBy('updated_at')
    ->orderBy('id')
    ->cursorPaginate(100);

return OrderResource::collection($orders);

Details that decide whether sync is reliable:

  • Deletions must sync too. Soft deletes with a deleted_at in the resource let the client remove local rows it would otherwise keep forever.
  • The client stores the server’s timestamp, not the device clock, as its next updated_since value. Device clocks drift.
  • Writes are idempotent. Accept a client-generated idempotency key or UUID on create requests so a retry after a dropped response does not create a duplicate.
  • Conflicts have a rule, decided up front: last write wins, server wins, or reject with a conflict error the app can resolve. Undefined behaviour here becomes data loss.

How do you send push notifications from Laravel?

Send them from queued jobs, never inside the request. Store each device’s push token against the user and platform, dispatch a queued notification, and deliver through APNs for iOS and Firebase Cloud Messaging for Android, using a maintained notification channel package or the provider SDKs directly.

  • Register tokens on every launch with an upsert endpoint. Push tokens change, and the app is the only party that knows when.
  • Prune invalid tokens when APNs or FCM report them as unregistered, otherwise the queue spends its time on devices that no longer exist.
  • Keep payloads small and non-sensitive. Send an identifier and let the app fetch the detail over the authenticated API; lock screens are not private.
  • Separate queues by priority so a bulk marketing send cannot delay a time-sensitive transactional alert.

How should mobile apps upload files to a Laravel backend?

Let the app upload directly to object storage with a short-lived presigned URL, then tell the API the upload finished. Streaming large photos or videos through PHP ties up workers on slow mobile connections. Laravel can generate presigned S3 upload URLs, so the API only authorizes and records the file.

['url' => $url, 'headers' => $headers] = Storage::disk('s3')
    ->temporaryUploadUrl(
        'uploads/'.$request->user()->id.'/'.Str::uuid().'.jpg',
        now()->addMinutes(5)
    );

return ['upload_url' => $url, 'headers' => $headers];

After the app confirms the upload, validate the object server-side — size, content type, and ownership of the key — and push any processing such as thumbnails onto a queue. Never trust the file type the client claims.

What rate limits does a mobile API need?

At minimum, a general per-user limit for authenticated routes and much stricter limits on login, one-time codes, and password reset. Key limits by user ID when authenticated and by IP otherwise. Mobile carriers place many users behind shared IP addresses, so IP-only limits punish legitimate users.

Laravel’s named rate limiters express this in a few lines. The numbers below are placeholders; tune them to your own traffic:

// AppServiceProvider::boot()
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(120)->by($request->user()?->id ?: $request->ip());
});

RateLimiter::for('auth', function (Request $request) {
    return [
        Limit::perMinute(5)->by((string) $request->input('email')),
        Limit::perMinute(30)->by($request->ip()),
    ];
});

Return 429 in your standard error envelope with a Retry-After header, and make the client respect it with backoff. A client that retries instantly on failure turns a brief incident into a self-inflicted outage. The OWASP API Security Top 10 lists unrestricted resource consumption and broken authorization among the most common API risks, and both apply directly to mobile backends.

What should you monitor on a mobile backend?

Monitor error rates and latency per endpoint, broken down by app version and platform. A backend change that looks healthy overall can break one older build entirely, and you will only see it if requests carry the client build and your dashboards can filter by it.

  • Request context: client platform, build, and a request ID returned in a response header so support can match an app report to a server log.
  • Error tracking on both sides, with the same request ID visible in app crash reports and server exceptions.
  • Queue health: backlog depth, failed jobs, and job duration. Laravel Pulse or a hosted APM covers the basics; Horizon adds detail if you run Redis queues.
  • Version adoption: requests per build over time, which is the number you need before retiring an API version.

Where does Next.js fit next to a Laravel mobile backend?

Next.js fits beside Laravel, not in front of the mobile app. It works well for an admin dashboard, marketing site, or web version of the product, consuming the same Laravel API or a separate web backend-for-frontend. The mobile app should call Laravel directly through its versioned API.

Adding a Next.js layer between the app and Laravel means two services that must both stay compatible with every installed build. Keep the mobile contract in one place. If you are planning the stack as a whole, my Laravel API backend development service covers the API, admin tooling, and infrastructure together.

What should you build first?

Before the first app release, get the decisions that are expensive to reverse in place: the /api/v1 prefix, the response and error envelope, per-device Sanctum tokens, the app-config endpoint with a minimum build, and client build logging. Everything else in this guide can be added later without breaking installed apps.

If you are still scoping the product, the mobile MVP playbook covers what to include in a first release, and mobile app development covers the client side. When you want the backend designed and built alongside the app, I offer Laravel backend and API development for mobile apps.

Frequently Asked Questions

Should a mobile app use Laravel Sanctum or Passport?

Use Sanctum when your own first-party app is the only client. It issues revocable per-device tokens with very little setup. Choose Passport only when you need a real OAuth2 server, for example when third-party developers or partner apps must obtain delegated access to your users' data.

How long should a Laravel API keep supporting an old version?

Long enough that almost no active installs still depend on it. Set the window from your own analytics on build adoption rather than the calendar, announce it in-app before removal, and use a minimum-supported-build check so the last stragglers see an update prompt instead of broken screens.

Can Laravel handle real-time features for a mobile app?

Yes. Laravel broadcasting with Laravel Reverb, or a hosted WebSocket service, covers live updates while the app is in the foreground. Push notifications through APNs and FCM still handle the background case, because mobile operating systems suspend open sockets when the app is not active.

Should a mobile app call Laravel directly or go through Next.js?

Call Laravel directly. A mobile client gains little from an extra hop, and every layer is another thing that has to stay backward compatible with old app builds. Next.js earns its place for the admin panel, marketing site, or a web client, not as a proxy in front of the mobile API.

Puneetpal Arneja, Senior iOS Developer & Mobile Architect

Written by

Staff Mobile Engineer at Albertsons · 13+ years building mobile apps

I've shipped 18+ mobile applications across banking, healthcare, and consumer products — including work for Citibank, Apple, PNC, and Albertsons. Everything here comes out of production work, not theory.

Building a Laravel Backend for Your App?

I design and build Laravel APIs for iOS, Android, Flutter, and React Native apps, with versioning, authentication, sync, and push set up so old app builds keep working as the product evolves.

Related Services & Further Reading