Headless Web Architecture: Connecting a Flat-PHP Frontend to a Flutter Mobile App Backend
Maintaining separate backend logic for web and mobile applications drains developer resources and introduces inconsistencies. A unified backend, serving multiple frontends, streamlines development, reduces costs, and ensures a consistent user experience across platforms. This approach decouples presentation from data, allowing specialized teams to focus on their respective domains while leveraging shared business logic.
📁 Table of Contents
- 👉 The Strategic Advantage of a Unified PHP API Backend
- 👉 Designing the Headless PHP API
- 👉 Example PHP API Endpoint
- 👉 FAQ: Secure Server-to-Server Authentication inside Flat-PHP
- 👉 FAQ: CORS and Preflight Handling in Hybrid Ecosystems
- 👉 FAQ: Concurrent State Updates and Conflict Resolution
- 👉 FAQ: Optimized JSON Schema Design for Diverse Frontends
- 👉 FAQ: Background Queues and Non-Blocking Webhook/FCM Delivery
- 👉 FAQ: Stateless Token Renewal and Interceptors in Flutter
- 👉 FAQ: Cursor-Based Pagination for Infinite Scrolling in Flat-PHP
- 👉 FAQ: Secure File Uploads and Preventing Remote Code Execution
- 👉 FAQ: Real-Time Stream Communication with Server-Sent Events
- 👉 FAQ: Offline Data Sync and Timestamp Sync Protocols in Flutter
The Strategic Advantage of a Unified PHP API Backend
Businesses often face the dilemma of building a web presence and a mobile application, only to find their development efforts fractured across two distinct backend systems. This redundancy is inefficient. A single, well-designed PHP API backend can serve as the central data hub for both a traditional server-rendered PHP web frontend and a modern Flutter mobile application, offering significant strategic advantages, especially for Indian businesses operating in diverse digital environments.
PHP, often underestimated in modern discussions, remains a powerhouse for web development. It powers over 77% of all websites whose server-side programming language is known, demonstrating its stability, vast community, and extensive ecosystem. For startups and established businesses in India, particularly those in Tier-2 and Tier-3 cities like Jaipur's burgeoning e-commerce sector or Coimbatore's textile manufacturers, PHP offers a compelling blend of cost-effectiveness, readily available developer talent, and robust performance. Its maturity translates into predictable development cycles and lower long-term maintenance costs compared to newer, less established technologies.
The primary benefit of a unified API is consistency. When both your web and mobile applications draw data from the same source, using the same business logic and data structures, you eliminate discrepancies in user experience and data presentation. Imagine a logistics company in Mumbai managing its fleet and deliveries: if the web dashboard shows one status for a shipment and the driver's mobile app shows another, operational chaos ensues. A unified API ensures that all platforms reflect the single source of truth.
Furthermore, a shared API dramatically accelerates development. Instead of building and maintaining two separate sets of APIs (one for web, one for mobile), teams can focus on enhancing a single, performant API. This means new features, security updates, and bug fixes can be deployed once and instantly benefit all connected frontends. This efficiency translates directly into faster time-to-market for new services and significant cost savings. For instance, a recent study indicated that companies adopting a unified API strategy can reduce backend development time by up to 30%, freeing up resources for frontend innovation or other critical business areas. This reduction in overhead directly impacts the bottom line, making it a critical consideration for cost-sensitive Indian businesses.
A unified API also simplifies scaling. As your user base grows, you scale a single backend system rather than two. This simplifies infrastructure management, monitoring, and troubleshooting. PHP frameworks like Laravel or Symfony provide excellent tools for building scalable APIs, handling requests efficiently, and integrating with caching mechanisms and load balancers. This scalability is crucial for businesses experiencing rapid growth, such as online education platforms based in Pune or travel aggregators in Delhi, where user demand can fluctuate dramatically.
Designing the Headless PHP API
The foundation of a successful headless architecture lies in a meticulously designed API. This API acts as the central brain, providing data and services to all connected clients without dictating how that data is presented. For our PHP backend, adhering to RESTful principles is paramount for creating a clean, predictable, and maintainable interface.
RESTful Principles and Resource Modeling
A RESTful API organizes data into resources, each identified by a unique URL. Operations on these resources are performed using standard HTTP methods:
-
GET: Retrieve a resource or a collection of resources. -
POST: Create a new resource. -
PUT/PATCH: Update an existing resource. -
DELETE: Remove a resource.
Consider an e-commerce platform in Bengaluru selling handicrafts. We might define resources like /products, /orders, and /customers.
-
GET /products: Retrieves a list of all products. -
GET /products/123: Retrieves details for product ID 123. -
POST /orders: Creates a new order. -
PUT /products/123: Updates product ID 123.
The API should be stateless, meaning each request from a client to the server must contain all the information needed to understand the request. The server should not store any client context between requests. This improves scalability and reliability.
Authentication and Authorization
Securing your API is non-negotiable. For headless architectures, token-based authentication is the standard. JSON Web Tokens (JWT) are a popular choice because they are self-contained and stateless.
/api/login endpoint.Authorization header (e.g., Authorization: Bearer ).For simpler internal APIs or specific services, API keys can also be used, though JWT offers more granular control and better security for public-facing or multi-user applications. For authorization, roles and permissions should be implemented. For example, an admin user might have access to DELETE /products, while a regular customer can only GET /products and POST /orders.
Data Serialization with JSON
JSON (JavaScript Object Notation) is the de facto standard for data exchange in web APIs due to its lightweight nature and human-readability. Your PHP API should always return data in JSON format.
A typical API response for /products/123 might look like this:
{
"id": 123,
"name": "Hand-Painted Silk Scarf",
"description": "Exquisite scarf, hand-painted by artisans in Mysore.",
"price": 1250.00,
"currency": "INR",
"category": "Accessories",
"stock": 50,
"images": [
"https://example.com/images/scarf1.jpg",
"https://example.com/images/scarf2.jpg"
]
}
PHP's json_encode() function makes converting PHP arrays or objects to JSON straightforward. For incoming data (e.g., POST requests), json_decode() can parse JSON strings into PHP arrays or objects.
Example PHP API Endpoint
Here’s a simplified example of a PHP file (api.php) that might handle product requests, demonstrating basic routing and JSON output. In a real-world scenario, you'd use a framework like Laravel or Lumen for robust routing, middleware, and database interactions.
<?php
// api.php - A very basic PHP API example
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
// Simulate a database (in a real app, this would be a DB query)
$products = [
123 => [
"id" => 123,
"name" => "Hand-Painted Silk Scarf",
"description" => "Exquisite scarf, hand-painted by artisans in Mysore.",
"price" => 1250.00,
"currency" => "INR",
"category" => "Accessories",
"stock" => 50,
"images" => [
"https://example.com/images/scarf1.jpg",
"https://example.com/images/scarf2.jpg"
]
]
];
$requestUri = $_SERVER['REQUEST_URI'];
if (preg_match('/\/api\/products\/(\d+)/', $requestUri, $matches)) {
$productId = intval($matches[1]);
if (isset($products[$productId])) {
echo json_encode($products[$productId]);
} else {
http_response_code(404);
echo json_encode(["error" => "Product not found"]);
}
} else {
http_response_code(400);
echo json_encode(["error" => "Invalid endpoint"]);
}
Frequently Asked Questions: Headless PHP APIs & Flutter
To help you successfully execute a headless architecture connecting your traditional flat-PHP web app and your Flutter mobile application, we have answered the most common, high-intent technical challenges engineers encounter during production setups.
How does a flat-PHP frontend securely authenticate against a headless PHP JWT API without a browser session?
In a traditional monolith system, PHP handles state management seamlessly using local session cookies (specifically the PHPSESSID cookie mapped to a server-side storage session). However, in a modern, decoupled headless architecture, the API backend is completely stateless and expects a JSON Web Token (JWT) within the HTTP request header for every single authenticated interaction.
When connecting a flat-PHP frontend to a headless API, the flat-PHP frontend behaves as a secure, server-side client proxy on behalf of the end user's browser. This provides an exceptional layer of protection against client-side exploitation. The workflow operates as follows:
- The user inputs their username and password into a login form hosted on the flat-PHP frontend.
- The flat-PHP server processes the POST submission and fires a secure server-to-server HTTP POST request (via cURL) to the API's authentication endpoint (e.g.,
https://api.bkbtechies.com/v1/auth/login). - The headless PHP API validates the credentials and returns a secure JSON payload containing an
access_token(JWT) and arefresh_token. - Instead of passing the JWT directly to the browser (where storing it in
localStorageexposes it to high-risk Cross-Site Scripting (XSS) attacks), the flat-PHP server writes the JWT into its server-side$_SESSIONstorage. Alternatively, it can set a secure,HttpOnly,Secure, andSameSite=Strictcookie pointing back strictly to the frontend web domain. - For every subsequent page request, the flat-PHP server retrieves the token from the session or decrypts the secure cookie, and passes it as an
Authorization: Bearer [TOKEN]header inside the cURL requests to the API.
Here is an expert-level implementation of an API Client wrapper in flat PHP that automatically handles authorization headers and triggers token refreshes upon expiration:
<?php
class ApiClient {
private $apiUrl = 'https://api.bkbtechies.com/v1';
public function request($endpoint, $method = 'GET', $data = null) {
$ch = curl_init("{$this->apiUrl}{$endpoint}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$headers = [
'Content-Type: application/json',
'Accept: application/json'
];
// Append JWT from the server session if it exists
if (isset($_SESSION['api_jwt'])) {
$headers[] = 'Authorization: Bearer ' . $_SESSION['api_jwt'];
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
} elseif (in_array($method, ['PUT', 'DELETE'])) {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Handle token expiration (HTTP 401 Unauthorized)
if ($statusCode === 401 && isset($_SESSION['api_refresh_token'])) {
if ($this->refreshAccessToken()) {
// Retry the original request once with the new token
return $this->request($endpoint, $method, $data);
}
}
return json_decode($response, true);
}
private function refreshAccessToken() {
$ch = curl_init("{$this->apiUrl}/auth/refresh");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'refresh_token' => $_SESSION['api_refresh_token']
]));
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
if (isset($response['jwt'])) {
$_SESSION['api_jwt'] = $response['jwt'];
return true;
}
return false;
}
}
?>
What are the best practices for handling CORS and preflight requests when a Flutter app and a flat-PHP frontend call the same PHP API?
Cross-Origin Resource Sharing (CORS) security restrictions apply exclusively within browser runtimes. In a unified architecture, you will experience differing request contexts:
- Flutter Native Mobile (iOS/Android): Communicates outside browser engines and does not trigger CORS checks.
- Flutter Web & Browsers: Enforces CORS policies strictly, verifying safe interaction across differing origins via an HTTP
OPTIONSpreflight check. - Flat-PHP Backend (Server-side): Uses PHP
cURL, which completely bypasses browser-side security restrictions.
For smooth interoperability, the shared flat-PHP API must dynamically process OPTIONS requests and serve standard access headers without using insecure wildcards (like Access-Control-Allow-Origin: *) when credential exchanges are required. Implement a robust CORS filtering header check at the entry point of your PHP script:
<?php
// Define whitelist of domain origins
$allowedOrigins = [
'https://bkbtechies.com',
'https://app.bkbtechies.com',
'http://localhost:5000' // Flutter web local port
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: " . $origin);
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Max-Age: 86400"); // Cache preflight response for 24 hours
}
// Handle HTTP OPTIONS preflight request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
exit(0); // Exit immediately for OPTIONS queries
}
?>
How do you handle concurrent state updates and race conditions when both a server-rendered PHP frontend and an offline-first Flutter mobile app modify the same resource?
In high-demand enterprise environments, synchronizing simultaneous writes is critical. A user browsing the flat-PHP e-commerce frontend might perform updates, while a logistics driver on the offline-first Flutter application submits modifications stored during a network outage. Sticking to a standard "Last-Write-Wins" methodology silently destroys intermediate updates, resulting in massive data integrity loss.
The standard solution is Optimistic Concurrency Control (OCC). You implement OCC by declaring a version column (integer format) inside your MySQL tables. When a client requests a resource, they retrieve the current version tag. Upon submitting modifications, the client must submit the original version integer back. The update query explicitly relies on this tag:
UPDATE products
SET stock_count = :stock, version = version + 1
WHERE id = :id AND version = :expected_version;
If the database records reflect that another client has successfully updated the version from 5 to 6 in the interim, the update statement targets 0 rows. The PHP API catches this anomaly, rolls back any active transactions, and returns an HTTP 409 Conflict JSON response. This triggers clean error handling or merge prompts on client frontends.
Let's review the architectural tradeoffs of conflict mitigation strategies in multi-channel ecosystems:
| Concurrence Strategy | Implementation Overhead | Performance Impact | Ideal Applications |
|---|---|---|---|
| Optimistic Locking (OCC) | Low to Moderate (Requires version fields) | Minimal (Zero row locks outside atomic updates) | Collaborative tools, inventory adjustments, SaaS entities |
| Pessimistic Locking | High (Requires stateful DB cursors and transactions) | High (Locks table records, risks bottlenecks) | High-value ticketing systems, booking platforms, banking ledger entries |
| Last-Write-Wins (LWW) | None (Standard default action) | None | Simple telemetry logging, basic preferences updates |
How do you structure API responses to accommodate both server-side PHP HTML rendering (SEO-heavy) and Flutter's widget-based rendering without bloating the payload?
A decoupled flat-PHP frontend relies heavily on deep relational data hierarchies (like nested comments, author profiles, and detailed metadata structures) to render fully optimized, SEO-friendly HTML markups in a single request pass. In contrast, Flutter mobile devices operate in erratic networking environments and require compact, bare-bones JSON payloads to limit cellular consumption, memory utilization, and JSON parsing lag.
To avoid splitting your engineering efforts into two separate APIs, establish a "GraphQL-lite" design pattern using clean REST parameters (fields and embed) on your shared PHP backend. These parameters allow clients to dynamically filter database outputs on the server:
<?php
class ProductController {
public function getProduct($id) {
$db = Database::getConnection();
$fields = isset($_GET['fields']) ? explode(',', $_GET['fields']) : null;
$embed = isset($_GET['embed']) ? explode(',', $_GET['embed']) : [];
$stmt = $db->prepare("SELECT * FROM products WHERE id = :id");
$stmt->execute(['id' => $id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
return ["error" => "Product not found"];
}
// Dynamically include relationships only when explicitly asked
if (in_array('merchant', $embed)) {
$mStmt = $db->prepare("SELECT name, rating FROM merchants WHERE id = :mid");
$mStmt->execute(['mid' => $product['merchant_id']]);
$product['merchant'] = $mStmt->fetch(PDO::FETCH_ASSOC);
}
// Prune the final object attributes based on target parameter fields
if ($fields) {
$filtered = [];
foreach ($fields as $f) {
$f = trim($f);
if (array_key_exists($f, $product)) {
$filtered[$f] = $product[$f];
}
}
if (isset($product['merchant']) && in_array('merchant', $embed)) {
$filtered['merchant'] = $product['merchant'];
}
return $filtered;
}
return $product;
}
}
?>
Using this approach, the flat-PHP SEO renderer calls /api/products/123?embed=merchant to retrieve everything it needs to populate metadata and schema blocks. Meanwhile, the Flutter app fetches only what is displayed on a quick search card: /api/products/123?fields=id,price,stock_count, reducing payload sizes by up to 90%.
In Flutter, deserialize JSON payloads with secure, nullable properties using safe cast conversions:
class Product {
final int id;
final double? price;
final int? stockCount;
final Map<String, dynamic>? merchant;
Product({
required this.id,
this.price,
this.stockCount,
this.merchant,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
price: (json['price'] as num?)?.toDouble(),
stockCount: json['stock_count'] as int?,
merchant: json['merchant'] as Map<String, dynamic>?,
);
}
}
How can you implement secure, non-blocking webhook and push notification delivery (like Firebase Cloud Messaging) in a flat-PHP API backend?
Multi-channel platforms require instant messaging alerts. When an event occurs—such as a booking confirmation processed on the flat-PHP frontend—the backend is expected to trigger external notifications. This includes mobile push alerts via Firebase Cloud Messaging (FCM) and remote shipping hooks.
Executing these external requests synchronously inside the active HTTP request-response thread degrades API performance. Establishing SSL handshake connections and resolving external domains (e.g., Google's FCM APIs) introduces latency ranging from 500ms to 3,000ms. Clients will experience significant delays and sluggish screens.
Since flat-PHP lacks a built-in async runtime loop (unlike Node.js or Go), you must build a database-backed job queue table. This table decouples the notification trigger from the API request-response cycle:
CREATE TABLE job_queue (
id INT AUTO_INCREMENT PRIMARY KEY,
job_type VARCHAR(50) NOT NULL,
payload TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'pending', -- pending, processing, completed, failed
attempts INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
When an API endpoint is hit, the flat-PHP server inserts the notification task into the database table, taking less than 2 milliseconds, and immediately responds with a successful status code to the user's mobile app or browser. An independent, background daemon worker script processes these rows continuously in the background:
<?php
// worker.php - Scheduled or running continuously via systemd CLI daemon
while (true) {
$db = Database::getConnection();
// Select the oldest pending task and lock it to prevent race conditions
$db->beginTransaction();
$stmt = $db->prepare("SELECT * FROM job_queue WHERE status = 'pending' ORDER BY id ASC LIMIT 1 FOR UPDATE");
$stmt->execute();
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
$db->rollBack();
sleep(2); // Rest for 2 seconds to avoid CPU spikes
continue;
}
// Mark as processing immediately
$db->prepare("UPDATE job_queue SET status = 'processing', attempts = attempts + 1 WHERE id = :id")
->execute(['id' => $job['id']]);
$db->commit();
$success = false;
if ($job['job_type'] === 'fcm_push') {
$success = sendFcmNotification(json_decode($job['payload'], true));
}
// Finalize job status based on execution success
if ($success) {
$db->prepare("UPDATE job_queue SET status = 'completed' WHERE id = :id")
->execute(['id' => $job['id']]);
} else {
$nextStatus = ($job['attempts'] >= 5) ? 'failed' : 'pending';
$db->prepare("UPDATE job_queue SET status = :status WHERE id = :id")
->execute(['status' => $nextStatus, 'id' => $job['id']]);
}
}
function sendFcmNotification($payload) {
// Standard implementation of curl request to Google FCM API goes here
return true;
}
?>
By shifting FCM notifications to an asynchronous queue, your user experience remains lightning-fast, and transient network errors on external servers won't cause transaction failures in your application.
How do you implement secure, stateless token renewal and session management in Flutter when interfacing with a PHP API?
Interfacing a mobile Flutter frontend with a stateless flat-PHP JWT API requires robust architecture to handle token expiration gracefully. In mobile apps, saving credentials in memory or forcing the user to log in repeatedly when the JWT expires is unacceptable. Instead, implement a Silent Token Refresh mechanism using Dio Interceptors in Dart.
Store the access_token and refresh_token securely using flutter_secure_storage (which utilizes iOS Keychain and Android Keystore/EncryptedSharedPreferences). When a request returns an HTTP 401 Unauthorized status, the Interceptor intercepts the failed request, fires a silent POST request to the PHP backend’s /api/auth/refresh endpoint using the refresh_token, updates the local secure storage, and retries the original failed request with the new access_token. This entire flow is transparent to the user.
Let's look at the implementation using Flutter's dio package:
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class ApiInterceptor extends Interceptor {
final Dio _dio;
final _storage = const FlutterSecureStorage();
ApiInterceptor(this._dio);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await _storage.read(key: 'access_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
final refreshToken = await _storage.read(key: 'refresh_token');
if (refreshToken != null) {
try {
// Lock dio request queue to prevent multiple simultaneous refreshes
_dio.lock(); // In Dio 5.x, use custom queuing or custom lock flags
final refreshResponse = await Dio().post(
'https://api.bkbtechies.com/v1/auth/refresh',
data: {'refresh_token': refreshToken},
);
if (refreshResponse.statusCode == 200) {
final newAccessToken = refreshResponse.data['access_token'];
final newRefreshToken = refreshResponse.data['refresh_token'];
await _storage.write(key: 'access_token', value: newAccessToken);
await _storage.write(key: 'refresh_token', value: newRefreshToken);
// Unlock and retry
_dio.unlock();
final options = err.requestOptions;
options.headers['Authorization'] = 'Bearer $newAccessToken';
final cloneReq = await _dio.fetch(options);
return handler.resolve(cloneReq);
}
} catch (e) {
_dio.unlock();
// Token renewal failed, wipe storage and redirect to login
await _storage.deleteAll();
// Trigger global logout event
}
}
}
return handler.next(err);
}
}
On the PHP side, the /api/auth/refresh endpoint decodes and verifies the signature of the cryptographic refresh_token, checks that the token ID has not been blacklisted in your MySQL DB or Redis cache, and returns a fresh short-lived JWT access token along with a rotated refresh token to prevent replay attacks. This ensures the user's mobile session remains unbroken and cryptographically secure.
How can you optimize database queries in flat-PHP to handle bulk pagination and infinite scrolling in Flutter without causing server CPU spikes?
Mobile clients loading lists of feeds, catalogs, or logs frequently use infinite scrolling. A common mistake when implementing this in flat-PHP is utilizing standard SQL offset-based pagination: SELECT * FROM products ORDER BY id DESC LIMIT 20 OFFSET 10000;. As the offset value grows, MySQL must scan and discard all preceding rows up to that offset before returning the 20 requested records. In heavy-traffic systems—such as an Indian wholesale market dashboard with millions of SKUs—high offset values trigger severe disk I/O bottlenecks and massive CPU spikes.
To eliminate this bottleneck, implement Keyset-based (Cursor) Pagination. Instead of offsets, the Flutter app passes a cursor parameter (typically the unique ID or timestamp of the last item loaded, e.g., ?limit=20&cursor=4023). The flat-PHP backend queries only the rows immediately following this cursor using a fast index:
<?php
// products.php - Optimized Cursor Pagination
header('Content-Type: application/json');
require_once 'db.php';
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : 20;
$cursor = isset($_GET['cursor']) ? intval($_GET['cursor']) : null;
$db = Database::getConnection();
if ($cursor) {
// Highly efficient indexing query using WHERE id < :cursor instead of OFFSET
$stmt = $db->prepare("SELECT id, name, price, image_url FROM products WHERE id < :cursor ORDER BY id DESC LIMIT :limit");
$stmt->bindValue(':cursor', $cursor, PDO::PARAM_INT);
} else {
$stmt = $db->prepare("SELECT id, name, price, image_url FROM products ORDER BY id DESC LIMIT :limit");
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
$nextCursor = null;
if (count($products) === $limit) {
// The cursor for the next page is the ID of the last element in the returned array
$nextCursor = end($products)['id'];
}
echo json_encode([
"data" => $products,
"paging" => [
"limit" => $limit,
"next_cursor" => $nextCursor,
"has_more" => $nextCursor !== null
]
]);
?>
In the Flutter mobile client, parsing this pagination schema is simple. When the scroll controller reaches the bottom of the list, it fires a request using the next_cursor retrieved from the previous payload, appending the records. Because the query uses a direct primary key range scan (WHERE id < :cursor), response times remain under 15ms even if the database scale scales up to millions of records, reducing cloud computing costs substantially.
What is the safest way to implement secure file uploads from a Flutter mobile app to a flat-PHP API, and how do you prevent remote code execution?
Allowing users to upload documents, images, or receipts from a Flutter mobile app poses serious security risks. Malicious actors can attempt to upload PHP shell scripts (e.g., malicious.php) and execute them by accessing the upload directory directly, resulting in complete Remote Code Execution (RCE) and server compromise. To prevent this vulnerability in a flat-PHP API, you must implement multi-layered verification:
- Verify MIME Type and Magic Bytes: Do not rely on the
$_FILES['file']['type']header sent by the client, which can be easily spoofed. Instead, use PHP'sfinfoextension to analyze the file's binary signature (magic bytes). - Re-encode and Rename Files: Sanitize the file name using cryptographically secure hashes (e.g.,
bin2hex(random_bytes(16))) and append a strictly whitelisted extension. - Store Outside the Web Root: Place uploaded files in a directory that is not accessible via HTTP requests (e.g.,
/var/www/uploads/instead of/var/www/html/uploads/). Serve files securely via a routing script or stream them to a cloud object storage bucket like Amazon S3. - Disable Script Execution via Web Server: If storing files on the local filesystem, configure Apache (
.htaccess) or Nginx to disable script execution in the uploads directory.
Here is a secure PHP upload handler implementation:
<?php
// upload.php - Secure File Upload Endpoint
header('Content-Type: application/json');
require_once 'auth.php'; // Ensure user is authenticated
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit(json_encode(["error" => "Method not allowed"]));
}
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit(json_encode(["error" => "Invalid file upload request"]));
}
$tmpPath = $_FILES['avatar']['tmp_name'];
// Validate file size (e.g., max 2MB)
if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) {
http_response_code(400);
exit(json_encode(["error" => "File size exceeds 2MB limit"]));
}
// Inspect actual file bytes using Fileinfo
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($tmpPath);
$allowedMimeTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'application/pdf' => 'pdf'
];
if (!array_key_exists($mimeType, $allowedMimeTypes)) {
http_response_code(400);
exit(json_encode(["error" => "Unsupported file type"]));
}
$extension = $allowedMimeTypes[$mimeType];
$safeFileName = bin2hex(random_bytes(16)) . '.' . $extension;
// Define storage directory outside public root
$uploadDirectory = '/var/www/secure_uploads/';
if (!is_dir($uploadDirectory)) {
mkdir($uploadDirectory, 0755, true);
}
$destinationPath = $uploadDirectory . $safeFileName;
if (move_uploaded_file($tmpPath, $destinationPath)) {
// In production, sync with S3 here or return path to secure streaming script
echo json_encode([
"success" => true,
"file_identifier" => $safeFileName
]);
} else {
http_response_code(500);
echo json_encode(["error" => "Failed to save uploaded file"]);
}
?>
In addition to backend checks, configure your Nginx server block to block PHP script execution in the uploads directory, ensuring complete isolation from malicious code injections:
location /uploads/ {
location ~ \.php$ {
deny all;
}
}
This multi-layered approach ensures your API handles high-volume uploads from Dart's MultipartFile safely, protecting your servers from malicious exploits.
How do you establish a secure real-time communication channel between a flat-PHP API backend and a Flutter mobile app without polling?
For applications requiring real-time updates—such as dispatch tracking for delivery drivers in Mumbai or live stock updates in retail apps—developers often fall back on continuous HTTP polling. This anti-pattern forces the Flutter app to request updates every 5 seconds, causing massive database loads, high battery consumption, and unnecessary network traffic. While full WebSockets are ideal, setting them up in traditional flat-PHP requires complex infrastructure (like running a persistent Swoole server or ReactPHP process). For a simpler, highly efficient alternative, implement Server-Sent Events (SSE).
SSE operates as a persistent, unidirectional HTTP connection using standard PHP. The server keeps the HTTP response stream open and pushes updates to the Flutter client in real-time using text/event-stream. Flutter natively handles SSE using EventSource or http.Client streams.
<?php
// stream.php - Server-Sent Events Endpoint
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no'); // Disable buffering for Nginx
// Ensure session is closed immediately to prevent session locking across pages
session_write_close();
$db = Database::getConnection();
$lastEventId = isset($_SERVER['HTTP_LAST_EVENT_ID']) ? intval($_SERVER['HTTP_LAST_EVENT_ID']) : 0;
// Keep stream active for 60 seconds
$timeout = 60;
$startTime = time();
while (true) {
if (connection_aborted()) {
break;
}
// Check database for new events since last loaded event
$stmt = $db->prepare("SELECT id, event_type, payload FROM live_events WHERE id > :last_id ORDER BY id ASC");
$stmt->execute(['last_id' => $lastEventId]);
$events = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($events)) {
foreach ($events as $event) {
echo "id: " . $event['id'] . "\n";
echo "event: " . $event['event_type'] . "\n";
echo "data: " . json_encode($event['payload']) . "\n\n";
$lastEventId = $event['id'];
}
ob_flush();
flush();
} else {
// Send a keep-alive comment line every 5 seconds to prevent gateway timeouts
echo ": keepalive\n\n";
ob_flush();
flush();
}
if ((time() - $startTime) > $timeout) {
break; // Gracefully terminate script, letting client reconnect
}
sleep(1); // Poll the local DB every 1 second
}
?>
In Flutter, you consume this real-time stream using the http package, maintaining a persistent stream listener:
import 'dart:convert';
import 'package:http/http.dart' as http;
void listenToLiveUpdates() async {
final client = http.Client();
final request = http.Request("GET", Uri.parse("https://api.bkbtechies.com/v1/stream.php"));
request.headers['Accept'] = 'text/event-stream';
final response = await client.send(request);
response.stream.transform(utf8.decoder).transform(const LineSplitter()).listen((line) {
if (line.startsWith("data: ")) {
final String rawJson = line.substring(6);
final data = jsonDecode(rawJson);
// Update UI or trigger local notifications in Flutter
}
});
}
SSE reduces network overhead and keeps the mobile app synchronized in real-time, leveraging standard Apache/Nginx web servers without requiring dedicated WebSocket daemons.
How can you implement offline data synchronization and sync tracking in a Flutter mobile app that relies on a flat-PHP API?
Mobile users often travel through regions with poor connectivity, such as rural highways or warehouse cellars. To provide a seamless user experience, a hybrid Flutter application must function offline-first, queueing changes locally and synchronizing with the flat-PHP backend once connectivity is restored. To design a robust sync engine between Flutter and flat-PHP, avoid complex full-database replication. Instead, use a Last-Synced Timestamp Protocol:
- Local Storage in Flutter: Use Isar or SQLite for secure offline storage. Every local record must contain a
is_dirtyboolean flag and alast_modifiedtimestamp. - Server-Side Tracking: In the MySQL database, add
updated_at(timestamp) andis_deleted(tinyint) columns to track soft deletes. - Synchronization Loop:
- Upload Phase: Flutter queries all local records where
is_dirty = true, packages them as a JSON list, and POSTs them to/api/sync/upload. The flat-PHP API processes these items using atomic database operations, resolves write conflicts using Optimistic Concurrency Control, updates database rows, and responds with a list of successful IDs. Flutter clears theis_dirtyflag for these records. - Download Phase: Flutter GETs
/api/sync/download?last_sync_time=2026-05-30T10:00:00. The flat-PHP backend queries all records whereupdated_at > last_sync_time, including soft-deleted entries. The app merges these items into the local database and updates its locallast_sync_timeparameter to the current server time.
- Upload Phase: Flutter queries all local records where
Here is the flat-PHP download endpoint implementation:
<?php
// download_sync.php - Sync Down to Flutter
header('Content-Type: application/json');
require_once 'db.php';
$lastSyncTime = isset($_GET['last_sync_time']) ? $_GET['last_sync_time'] : '1970-01-01 00:00:00';
$db = Database::getConnection();
// Query all additions, edits, and deletions (soft deletes) since last sync
$stmt = $db->prepare("SELECT id, name, price, stock, is_deleted, updated_at FROM products WHERE updated_at > :last_sync ORDER BY updated_at ASC");
$stmt->execute(['last_sync' => $lastSyncTime]);
$changes = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Return server current time for client's subsequent tracking
$serverTime = date('Y-m-d H:i:s');
echo json_encode([
"server_time" => $serverTime,
"changes" => $changes
]);
?>
By packaging sync updates in unified batches, the mobile app reduces cellular requests, handles spotty connections gracefully, and maintains data integrity across the entire application ecosystem.
Build a Secure, Decoupled Architecture Today
Connecting legacy systems, flat-PHP frontends, and dynamic cross-platform Flutter mobile applications doesn't have to be complex. BKB Techies has a proven track record of designing highly scalable, high-performance, and GSC-optimized headless architectures that power growth for businesses worldwide.
Consult Our Cloud Architects