cd /news/developer-tools/flutter-app-with-a-laravel-backend-2… · home › topics › developer-tools › article
[ARTICLE · art-165361] src=ainexusdaily.vercel.app ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Flutter app with a Laravel backend (2026 guide)

A 2026 developer guide recommends pairing Flutter mobile apps with a self-hosted Laravel backend using Laravel Sanctum bearer tokens instead of Firebase, arguing that users, data and billing otherwise sit in a third-party console. The guide specifies roughly a dozen JSON API routes under /api — including register, verify, login, social sign-in, password reset, me, logout, account deletion, backup storage and retrieval, and a version check — with public auth routes behind a rate limiter and tokens stored in flutter_secure_storage. It keeps Firebase Cloud Messaging for push delivery and recommends 6-digit email verification codes valid for 10–15 minutes, with sign-up split into a register call that returns 202 and creates no user row and a verify call that creates the user and returns a token.

read10 min views2 publishedNov 6, 2026
Flutter app with a Laravel backend (2026 guide)
Image: Ainexusdaily (auto-discovered)

Short answer: install Laravel Sanctum, give the app a handful of JSON routes under /api, and have Flutter send Authorization: Bearer <token> on every call after sign-in. Keep the token in flutter_secure_storage. Firebase stays only where Apple and Google make you use it: push delivery goes through F

CM either way. Most Flutter tutorials assume Firebase for everything. That is quick to start, but your users, their data and your bill end up in someone else's console. A small Laravel app gives you the same building blocks on a server you control, plus an admin panel.

This guide covers the six things a typical app needs from it, in the order you usually build them, and then how to set the project up so a coding agent can keep building it with you. Firebase Your own Laravel backend Accounts Firebase Auth Sanctum tokens, your own users table Data Firestore rules Routes and policies in PHP Admin view of your users Firebase console An admin panel you build (Filament) Pricing Per read, write and sign-in One server, from a few dollars a month Push to phones FCM Still FCM, sent from your server Leaving later Export and rewrite It is already yours Pick Firebase if you want zero servers. Pick Laravel if you already know PHP, want one place to see and edit your users, or need data to stay on infrastructure you choose. A complete first version is about a dozen routes: Route Auth What it does POST /api/auth/register public Sends a 6-digit code, creates nothing yet POST /api/auth/verify public Code + password → account + token POST /api/auth/login public Email + password → token POST /api/auth/social public Google or Apple id token → token POST /api/auth/password/forgot public Sends a reset code GET /api/auth/me token The signed-in user POST /api/auth/logout token Revokes this token DELETE /api/auth/me token Deletes the account and its data PUT /api/backup token Stores a snapshot of the app's data GET /api/backup token Latest snapshot, for restore GET /api/version public Latest and minimum app version Put the public auth routes behind a rate limiter (throttle:) so nobody can use them to send thousands of emails.

In a new Laravel app: php artisan install:api That installs Sanctum and creates routes/api.php. Add the HasApiTokens trait to User, then return a token from every route that signs someone in: public function login(Request $req) { $data = $req->validate(['email' => 'required|email', 'password' => 'required|string']); $user = User::where('email', $data['email'])->first(); if (! $user || ! Hash::check($data['password'], $user->password)) { throw ValidationException::withMessages(['email' => ['Invalid credentials.']]); } return [ 'token' => $user->createToken('mobile')->plainTextToken, 'user' => ['id' => $user->id, 'name' => $user->name, 'email' => $user->email], ]; } Everything private goes in one group: Route::middleware('auth:sanctum')->group(function () { Route::get('auth/me', [AuthController::class, 'me']); Route::post('auth/logout', [AuthController::class, 'logout']); }); Sanctum tokens do not expire unless you set expiration in config/sanctum.php.

For a phone app that is usually what you want: sign-out deletes the token with $req->user()->currentAccessToken()->delete(). A code typed into the app works better on a phone than a link in an email, which opens the browser instead of the app. Split sign-up into two calls: register validates name, email and password, stores a hashed password next to a 6-digit code, emails the code and answers 202. No user row exists yet.

verify takes the email, the code and the same password, creates the user with email_verified_at set and answers with a token. Two details matter. Answer register the same way whether or not the address already has an account, and email the existing owner a note instead of a code, so the route cannot be used to find out who is signed up. And keep codes short-lived (10–15 minutes) with a limit on attempts.

One small client handles the base URL, the headers and the token: class Api { Api(this.baseUrl); final String baseUrl; final _storage = const FlutterSecureStorage(); Future<Map<String, String>> _headers() async { final token = await _storage.read(key: 'api_token'); return { 'Accept': 'application/json', 'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }; } Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body) async { final res = await http .post(Uri.parse('$baseUrl/api/$path'), headers: await _headers(), body: jsonEncode(body)) .timeout(const Duration(seconds: 30)); final json = res.body.isEmpty ? <String, dynamic>{} : jsonDecode(res.body) as Map<String, dynamic>; if (res.statusCode == 401) await _storage.delete(key: 'api_token'); if (res.statusCode >= 400) throw ApiException(json['message'] as String? ?? 'Error ${res.statusCode}'); return json; } Future<void> signIn(String email, String password) async { final json = await post('auth/login', {'email': email, 'password': password}); await _storage.write(key: 'api_token', value: json['token'] as String); } } Pass the server address at build time instead of hard-coding it: flutter run --dart-define=API_BASE_URL=https://api.example.com and read it with const String.fromEnvironment('API_BASE_URL').

When it is empty, the app can fall back to local-only mode, which keeps development and screenshots independent of a server. The Accept: application/json header is not optional: without it, Laravel answers a validation error with a redirect instead of JSON. The phone does the provider part and gets an id token. Send that token to POST /api/auth/social with the provider name.

The server checks the signature against Google's or Apple's public keys, checks aud against your client ids, iss and exp, and then finds or creates the user. Match users in this order: By provider and subject (sub), stored on the user row. This never changes for a person. By email, but only if the provider says the email is verified and your own row is verified too.

Otherwise anyone could create a password account with someone else's address and be let into it when the real owner later signs in with Google. Otherwise create a new user. Answer with the same { token, user } as a password login, so the rest of the app does not care how the person signed in. The phone side, including Apple's App Store rule and the Android SHA-1 keys, is in our guide to Google and Apple sign-in in Flutter.

For most apps "sync" really means "restore my data on a new phone". That is much simpler than real-time sync: the app exports its local data as one JSON document and uploads it. Route::middleware('auth:sanctum')->group(function () { Route::put('backup', [BackupController::class, 'store']); Route::get('backup', [BackupController::class, 'latest']); }); In the model, cast the payload with 'payload' => 'encrypted' so it is stored encrypted with your APP_KEY. Three rules keep it safe: Version every upload and keep the last few, so a bad backup does not replace the only good one. Limit the size and answer 413 above it. Keep a copy of the app key. A new APP_KEY makes every stored backup unreadable. Answer that case with its own status (409) so the app can say "back up again" instead of "nothing found".

Update prompts. A public GET /api/version returns: { "latest": "1.0.1", "minimum": "1.0.0", "message": "A few fixes and a faster start.", "android_url": "https://play.google.com/store/apps/details?id=…", "ios_url": "https://apps.apple.com/app/id…" } On launch the app compares its own version (package_info_plus) with both numbers. Below minimum, block the app with a button to the store; below latest, show a dismissible prompt. Keep the numbers in a table the admin panel can edit, so releasing a forced update does not need a deploy.

Push. Phones only receive push through FCM (and APNs behind it on iOS), so Firebase stays for this one job. Your server sends through the FCM HTTP v1 API with a service-account access token: Http::withToken($accessToken)->post( "https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send", ['message' => ['topic' => 'all', 'notification' => ['title' => $title, 'body' => $body]]], ); Subscribe every install to a topic such as all for announcements, and store device tokens on the user when you need to reach one person. Log every send, so the admin panel can show a history.

Most of the screens you add after this will probably be written with Claude Code, Cursor, Codex or Copilot. A Flutter app plus a Laravel app is two projects in two languages, and an agent that has to guess how they fit together guesses wrong. Two kinds of file fix most of that. Project notes.

Put an AGENTS.md at the root of the project. Agents read it before they change anything, so keep it short and factual: what the app is, where things live, how the two sides talk, and the commands that must pass. - Flutter app in app/, Laravel API and Filament panel in backend/. - The app talks to the API only through lib/data/api.dart; every call sends Accept: application/json and the Sanctum token.

  • A new endpoint: route in backend/routes/api.php, a feature test in backend/tests/Feature/, then the call in api.dart. - Done means: flutter analyze clean, flutter test green, cd backend && composer test green. Claude Code reads CLAUDE.md instead; a one-line CLAUDE.md containing @AGENTS.md imports the same file, so there is only one to keep up to date. Skills for the jobs you repeat.

A skill is a folder with a SKILL.md: a name, one sentence saying when to use it, then the exact steps. --- name: add-api-endpoint description: Add a Laravel API route and the Flutter call that uses it. Use when a screen needs new data from the server. --- 1.

Add the route inside the auth:sanctum group in backend/routes/api.php. 2. Write the controller method; validate input with $req->validate(). 3. Add a feature test that calls the route with and without a token. 4. Add the method to lib/data/api.dart and use it from the controller. 5.

Run the three commands from AGENTS.md. Keep skills in a skills/ folder and list them in AGENTS.md, so every agent finds them; Claude Code also picks them up from .claude/skills/. Good first skills: add an endpoint, add a screen, remove a feature you do not need, and ship a release (bump the version, then update minimum and latest from step 6). The tests are what make this work.

With a test suite on both sides, "the commands pass" is a definition of done the agent can check by itself, instead of you reading every diff. Filament turns your models into an admin panel with very little code: a users list, the stored backups, a form that sends a push, and the version table from step 6. Restrict it to an admin role, and keep it on the same Laravel app so it shares the models and the database. [ ] HTTPS on the API domain, and APP_DEBUG=false [ ] APP_KEY stored somewhere outside the server [ ] Rate limits on every public auth route [ ] Accept: application/json sent by the app [ ] Token deleted on sign-out and on 401 [ ] Google and Apple id tokens verified on the server, never trusted from the phone [ ] A Delete account route that removes the user and their data (App Store guideline 5.1.1(v)) [ ] minimum version set before you ship a breaking API change [ ] AGENTS.md names the commands that must pass on both sides Can Laravel replace Firebase for a Flutter app?

Should I use Sanctum or Passport for a Flutter app? Why does my Laravel API return HTML or a redirect to Flutter? Accept: application/json header, so Laravel treats it as a browser request. Send that header on every call, or add a middleware that forces JSON responses for /api.

Where should a Flutter app store the Laravel token? flutter_secure_storage, which uses the Keychain on iOS and encrypted storage on Android. Not in shared_preferences. How do I get Claude Code or Cursor to work well on a Flutter and Laravel project?

AGENTS.md that explains where the app and the API live, how they talk and which test commands must pass, and a skill for each job you repeat, such as adding an endpoint. Then ask for changes in terms of those skills. Does the app have to be online to work? If you would rather start with all of this already wired up, Skelora is a Flutter app with this Laravel API and a Filament admin panel in the same download: email-code accounts, Google and Apple sign-in, encrypted backups, update prompts and push.

It ships with an AGENTS.md and one skill per feature, so your coding agent can take out what you do not need. Originally published at coderqh.com.

Key Takeaways #

  • •Most Flutter tutorials assume Firebase for everything.
  • •This story was reported by Dev.to , covering developments in thedev space.
  • •AI advancements continue to reshape industries — read the full article on Dev.to for complete coverage.

📖 Continue reading the full article:

Read Full Article on Dev.to →

── more in #developer-tools 4 stories · sorted by recency
── more on @flutter 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/flutter-app-with-a-l…] indexed:0 read:10min 2026-11-06 · —