mucan54 / laravel-remote-eloquent
Execute Eloquent queries on remote Laravel backend with automatic Row Level Security
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/mucan54/laravel-remote-eloquent
Requires
- php: ^8.1|^8.2|^8.3
- illuminate/database: ^10.0|^11.0
- illuminate/http: ^10.0|^11.0
- illuminate/support: ^10.0|^11.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2025-11-11 09:16:58 UTC
README
One package. Two modes. Same codebase.
Execute Eloquent queries on a remote Laravel backend with automatic Row Level Security. Perfect for mobile apps and client applications.
Installation
composer require mucan54/laravel-remote-eloquent
Publish configuration:
php artisan vendor:publish --tag=remote-eloquent-config
Configuration
Client Mode (Mobile App / Client Application)
.env:
REMOTE_ELOQUENT_MODE=client REMOTE_ELOQUENT_API_URL=https://api.yourapp.com
After login, store the token:
cache()->put('remote_eloquent_token', $token);
Server Mode (Backend API)
Install Laravel Sanctum (recommended for mobile apps):
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\ServiceProvider"
php artisan migrate
.env:
REMOTE_ELOQUENT_MODE=server REMOTE_ELOQUENT_AUTH_MIDDLEWARE=auth:sanctum
config/remote-eloquent.php:
// Authentication (defaults to Sanctum) 'auth_middleware' => env('REMOTE_ELOQUENT_AUTH_MIDDLEWARE', 'auth:sanctum'), // Model whitelist 'allowed_models' => [ 'Post', 'Comment', 'Product', ],
Alternative authentication:
// Laravel Passport 'auth_middleware' => 'auth:api', // JWT Auth 'auth_middleware' => 'jwt.auth', // Multiple middleware 'auth_middleware' => ['auth:sanctum', 'verified'], // Disable authentication (NOT recommended) 'auth_middleware' => null,
Usage
Same Model, Both Environments
<?php namespace App\Models; use RemoteEloquent\RemoteModel; use Illuminate\Database\Eloquent\Builder; class Post extends RemoteModel { protected $fillable = ['user_id', 'title', 'content', 'status']; /** * Global Scopes (Server Mode Only) * Automatic Row Level Security! */ protected static function booted() { static::addGlobalScope('user', function (Builder $builder) { if (auth()->check()) { $builder->where('user_id', auth()->id()); } }); } public function user() { return $this->belongsTo(User::class); } }
Query Anywhere
// Works in both client and server modes! $posts = Post::where('status', 'published') ->with('user') ->latest() ->paginate(20); // Client mode: Sends API request to backend // Server mode: Executes on local database with Global Scopes
How It Works
┌─────────────────────────────────┐
│ Mobile App / Client │
│ REMOTE_ELOQUENT_MODE=client │
│ │
│ Post::where('status', 1)->get()│
│ ↓ │
│ Sends JSON AST to API │
└────────────┬────────────────────┘
│ HTTPS + Token
│
┌────────────▼────────────────────┐
│ Laravel Backend API │
│ REMOTE_ELOQUENT_MODE=server │
│ │
│ 1. Validate model whitelist │
│ 2. Validate method whitelist │
│ 3. Execute query locally │
│ 4. Global Scopes apply! ✅ │
└────────────┬────────────────────┘
│
┌────────────▼────────────────────┐
│ PostgreSQL / MySQL │
│ SELECT * FROM posts │
│ WHERE user_id = 123 (Auto! ✅) │
│ AND status = 1 │
└─────────────────────────────────┘
Key Features
✅ One Package, Two Modes
- Client mode: Sends queries to API
- Server mode: Executes locally
- Same models work everywhere
✅ Global Scopes = Row Level Security
// Backend model static::addGlobalScope('user', function (Builder $builder) { if (auth()->check()) { $builder->where('user_id', auth()->id()); } }); // Mobile app just calls Post::all(); // SQL executed: WHERE user_id = 123 (Automatic!)
✅ Full Eloquent Support
- Relationships:
with(),has(),whereHas() - Queries:
where(),orderBy(),groupBy() - Aggregates:
count(),sum(),avg() - Pagination:
paginate(),simplePaginate()
✅ Secure by Default
- Authentication required (Laravel Sanctum)
- Model whitelist
- Method whitelist
- No SQL injection
- No code execution
Examples
Complex Queries
$posts = Post::with(['user', 'comments' => function($query) { $query->where('approved', true) ->orderBy('created_at', 'desc') ->limit(5); }]) ->where('status', 'published') ->whereHas('comments', function($query) { $query->where('rating', '>', 3); }) ->latest() ->paginate(20);
Multi-Tenancy
// Backend model protected static function booted() { // User isolation static::addGlobalScope('user', function (Builder $builder) { if (auth()->check()) { $builder->where('user_id', auth()->id()); } }); // Tenant isolation static::addGlobalScope('tenant', function (Builder $builder) { if (auth()->check() && auth()->user()->tenant_id) { $builder->where('tenant_id', auth()->user()->tenant_id); } }); }
Livewire Component
use App\Models\Post; use Livewire\Component; use Livewire\WithPagination; class PostList extends Component { use WithPagination; public string $search = ''; public function render() { $posts = Post::query() ->with('user') ->when($this->search, fn($q) => $q->where('title', 'like', "%{$this->search}%")) ->latest() ->paginate(20); return view('livewire.post-list', ['posts' => $posts]); } }
Batch Queries (Performance!)
Execute multiple queries in a single request. Works in BOTH modes!
use RemoteEloquent\Client\BatchQuery; // Client mode: 1 HTTP request instead of 4! // Server mode: Executes locally, same code! $results = BatchQuery::run([ 'posts' => Post::where('status', 'published')->limit(10), 'recentComments' => Comment::latest()->limit(5), 'postCount' => Post::where('status', 'published')->count(), 'userData' => User::with('profile')->find(auth()->id()), ]); // Access results (works everywhere!) $posts = $results['posts']; // Collection $recentComments = $results['recentComments']; // Collection $postCount = $results['postCount']; // int $userData = $results['userData']; // object // Advanced: Custom method/parameters $results = BatchQuery::run([ 'published' => [ 'query' => Post::where('status', 'published'), 'method' => 'paginate', 'parameters' => [20] ], 'drafts' => [ 'query' => Post::where('status', 'draft'), 'method' => 'count', 'parameters' => [] ], ]);
Benefits:
- ✅ Reduce HTTP requests (10 queries = 1 request instead of 10!)
- ✅ Better mobile app performance
- ✅ Lower latency
- ✅ Automatic error handling per query
- ✅ Works in both modes - same code everywhere!
Remote Services (Server-Side Logic!)
Execute service methods remotely when they need server-side credentials or resources.
Use Cases:
- Payment processing (Stripe keys only on server)
- Email sending (SMTP credentials only on server)
- External API calls (API keys only on server)
- AWS/Cloud operations (credentials only on server)
Usage:
<?php namespace App\Services; use RemoteEloquent\Client\RemoteService; class PaymentService { use RemoteService; /** * Methods in this array will execute on SERVER * (has access to STRIPE_SECRET_KEY in server .env) */ protected array $remoteMethods = [ 'processPayment', 'refundPayment', 'createCustomer', ]; /** * This runs on SERVER (has Stripe secret key) */ public function processPayment(int $amount, string $token) { \Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY')); $charge = \Stripe\Charge::create([ 'amount' => $amount, 'currency' => 'usd', 'source' => $token, ]); return $charge->id; } /** * This runs LOCALLY (not in $remoteMethods) */ public function calculateFee(int $amount) { return $amount * 0.029 + 30; // Local calculation } }
In Mobile App:
$paymentService = new PaymentService(); // Executes on SERVER (secure!) $chargeId = $paymentService->processPayment(1000, $token); // Executes LOCALLY $fee = $paymentService->calculateFee(1000);
Server Configuration:
// config/remote-eloquent.php 'allowed_services' => [ 'App\Services\PaymentService', 'App\Services\EmailService', 'App\Services\*', // All services in App\Services ],
Real-World Example (Email Service):
class EmailService { use RemoteService; protected array $remoteMethods = ['sendWelcomeEmail', 'sendInvoice']; public function sendWelcomeEmail(int $userId) { $user = User::find($userId); // Server has MAIL_* credentials Mail::to($user->email)->send(new WelcomeEmail($user)); return true; } } // Mobile app $emailService = new EmailService(); $emailService->sendWelcomeEmail(auth()->id()); // Executes on server
Benefits:
- ✅ Keep secrets on server only
- ✅ Same code works in both environments
- ✅ Automatic serialization/deserialization
- ✅ Type-safe method calls
Batch Services with Pipeline Pattern 🚀
Execute multiple service methods with an elegant fluent interface. Works in BOTH modes!
Quick Example
use RemoteEloquent\Client\BatchService; $paymentService = new PaymentService(); $emailService = new EmailService(); $smsService = new SmsService(); // Fluent pipeline - clean and readable! $results = BatchService::pipeline() ->step('payment', [$paymentService, 'charge', [1000, $token]]) ->stopOnFailure() ->step('email', [$emailService, 'send', fn($prev) => [$prev['payment']['orderId']]]) ->skipOnFailure() ->step('sms', [$smsService, 'send', fn($prev) => [$prev['payment']['orderId']]]) ->skipOnFailure() ->execute(); // Check results if (!isset($results['payment']['error'])) { echo "Payment successful! Order: {$results['payment']['orderId']}"; }
What just happened?
- ✅ Payment processes first
- ✅ If payment succeeds, email sent with order ID from payment result
- ✅ If payment succeeds, SMS sent with order ID from payment result
- ✅ If payment fails, email and SMS are automatically skipped
- ✅ All in ONE HTTP request!
Pipeline API
Adding Steps:
->step('key', [$service, 'method', $args]) // or ->step('key', $service, 'method', $args)
Failure Strategies:
->stopOnFailure()- Stop entire pipeline if this step fails (default)->skipOnFailure()- Skip dependent steps if this fails->continueOnFailure()- Continue even if this fails
Explicit Dependencies:
->step('order', [$orderService, 'create', fn($p) => [...]]) ->dependsOn('user', 'payment') // Explicit dependencies
Closure Arguments:
// Access previous results via closure ->step('email', [$emailService, 'send', fn($prev) => [ $userId, $prev['payment']['orderId'], $prev['inventory']['productName'] ]])
Real-World Example: Complete Checkout
$results = BatchService::pipeline() // Step 1: Check inventory ->step('inventory', [$inventoryService, 'check', [$productId, $quantity]]) ->stopOnFailure() // Step 2: Process payment (uses inventory price) ->step('payment', $paymentService, 'charge', fn($p) => [ $p['inventory']['price'], $token ]) ->stopOnFailure() // Step 3: Update inventory (uses payment order ID) ->step('inventory_update', $inventoryService, 'decrement', fn($p) => [ $productId, $quantity, $p['payment']['orderId'] ]) ->stopOnFailure() // Step 4: Send receipt email ->step('email', $emailService, 'sendReceipt', fn($p) => [ $userId, $p['payment']['orderId'] ]) ->skipOnFailure() // Don't fail checkout if email fails // Step 5: Send SMS confirmation ->step('sms', $smsService, 'sendConfirmation', fn($p) => [ $phone, $p['payment']['orderId'] ]) ->skipOnFailure() // Step 6: Track analytics (always run) ->step('analytics', $analyticsService, 'track', fn($p) => [ 'checkout_completed', $p['payment']['orderId'] ?? null ]) ->continueOnFailure() ->execute(); // Handle results if (!isset($results['payment']['error'])) { echo "Order {$results['payment']['orderId']} created!"; echo $results['email'] ? "Receipt sent!" : "Email failed"; echo $results['sms'] ? "SMS sent!" : "SMS failed"; } else { echo "Checkout failed: {$results['payment']['error']}"; }
User Registration Workflow
$results = BatchService::pipeline() // Create user account ->step('user', [$userService, 'create', [$email, $password]]) ->stopOnFailure() // Create user profile ->step('profile', $profileService, 'create', fn($p) => [ $p['user']['id'], $name, $avatar ]) ->stopOnFailure() // Send welcome email ->step('welcome_email', $emailService, 'sendWelcome', fn($p) => [ $p['user']['email'], $p['user']['name'] ]) ->skipOnFailure() // Create default settings ->step('settings', $settingsService, 'createDefaults', fn($p) => [ $p['user']['id'] ]) ->skipOnFailure() // Track registration ->step('analytics', $analyticsService, 'trackRegistration', fn($p) => [ $p['user']['id'], 'source' => 'mobile_app' ]) ->continueOnFailure() ->execute();
Alternative: Array-Based API (Advanced)
For advanced use cases, you can use the array-based API with explicit configuration:
$results = BatchService::run([ 'payment' => [ 'service' => $paymentService, 'method' => 'charge', 'args' => [1000, $token], 'on_failure' => 'stop', ], 'email' => [ 'service' => $emailService, 'method' => 'send', 'args' => fn($r) => [$r['payment']['orderId']], 'depends_on' => ['payment'], 'on_failure' => 'skip', ], ]);
Or simple format:
$results = BatchService::run([ 'payment' => [$paymentService, 'charge', [1000, $token]], 'email' => [$emailService, 'send', [$userId, $orderId]], ]);
Error Handling
$results = BatchService::pipeline() ->step('payment', [$paymentService, 'charge', [1000]]) ->step('email', [$emailService, 'send', fn($p) => [$p['payment']]]) ->execute(); // Check for errors if (isset($results['payment']['error'])) { echo "Error: {$results['payment']['error']}"; } // Check if skipped if (isset($results['email']['skipped'])) { echo "Email skipped: {$results['email']['reason']}"; } // Check success if (!isset($results['payment']['error']) && !isset($results['email']['error'])) { echo "All steps completed successfully!"; }
Key Features
✅ Fluent Interface - Clean, readable method chaining
✅ Implicit Dependencies - Steps execute in order, later steps access earlier results
✅ Explicit Dependencies - Use ->dependsOn() when needed
✅ Closure Arguments - Pass data between steps with fn($prev) => [...]
✅ Failure Strategies - stopOnFailure, skipOnFailure, continueOnFailure
✅ Works in BOTH modes - Client mode: 1 HTTP request, Server mode: local execution
✅ Automatic Validation - Detects circular dependencies
✅ Type Safe - Full IDE autocomplete support
Important: Closures work in server mode only. In client mode, use static arguments or pre-computed values
Payload Encryption 🔒
Encrypt all API payloads end-to-end for maximum security. Works in BOTH modes!
Features
✅ AES-256-GCM - Military-grade authenticated encryption ✅ Per-User Encryption - Each user gets unique encryption key (optional) ✅ High Performance - <0.01ms overhead, supports 10,000+ req/s ✅ Transparent - Automatic encryption/decryption, no code changes needed ✅ Tamper-Proof - Authenticated encryption detects payload modification ✅ Man-in-the-Middle Protection - Even HTTPS traffic content is encrypted
Quick Setup
1. Generate Encryption Key:
# Generate secure 256-bit key (SEPARATE from Laravel's APP_KEY!)
openssl rand -base64 32
⚠️ IMPORTANT SECURITY NOTE:
- This is a SEPARATE key from Laravel's
APP_KEY - NEVER use Laravel's
APP_KEYfor this encryption - This key will be shared between client and server (mobile app needs it)
- Laravel's
APP_KEYmust stay on the server only - Generate a dedicated key specifically for Remote Eloquent encryption
2. Configure Environment:
Server (.env):
# Enable encryption REMOTE_ELOQUENT_ENCRYPTION_ENABLED=true # Paste generated key (SEPARATE from APP_KEY!) REMOTE_ELOQUENT_ENCRYPTION_KEY="your-generated-key-here" # Optional: Enable per-user encryption REMOTE_ELOQUENT_ENCRYPTION_PER_USER=false
Client/Mobile (.env):
# Enable encryption REMOTE_ELOQUENT_ENCRYPTION_ENABLED=true # SAME key as server (this is why it must be separate from APP_KEY!) REMOTE_ELOQUENT_ENCRYPTION_KEY="same-key-as-server" # Optional: Enable per-user encryption REMOTE_ELOQUENT_ENCRYPTION_PER_USER=false
3. Done! All API communication is now encrypted automatically.
How It Works
Client Side (Mobile App):
1. Query built: Post::where('status', 'published')->get()
2. AST generated: { model: 'Post', chain: [...], method: 'get' }
3. ✅ ENCRYPTED: Base64 payload with IV + Tag + Ciphertext
4. HTTP POST: { encrypted_payload: "..." }
5. ✅ Response DECRYPTED: Automatic decryption
6. Result returned: Collection of posts
Server Side (Laravel):
1. HTTP POST received: { encrypted_payload: "..." }
2. ✅ DECRYPTED by middleware: { model: 'Post', chain: [...] }
3. Query executed: Post::where('status', 'published')->get()
4. ✅ Response ENCRYPTED: { encrypted: true, payload: "..." }
5. HTTP response sent
Per-User Encryption
Enable unique encryption keys per user for maximum security:
REMOTE_ELOQUENT_ENCRYPTION_PER_USER=true
How it works:
Master Key + User ID → Unique User Key (via HKDF-SHA256)
🔒 SECURITY: User ID Source
- User ID is ALWAYS obtained from
auth()->user()on the server - NEVER from client request (prevents tampering)
- Server uses authenticated session to derive per-user key
- Client cannot fake another user's encryption key
Benefits:
- ✅ User A cannot decrypt User B's data (even with master key)
- ✅ Prevents cross-user data leaks
- ✅ Compartmentalized security
- ✅ Keys cached for performance
- ✅ Tamper-proof - User ID from authentication, not client
Example:
// User #1 authenticated via Sanctum // Server uses auth()->user()->id (NOT from request!) $posts = Post::where('user_id', 1)->get(); // Encrypted with user 1's key // User #2 authenticated via Sanctum // Server uses auth()->user()->id = 2 $posts = Post::where('user_id', 2)->get(); // Encrypted with user 2's key // User 1 CANNOT decrypt User 2's responses! // Even if User 1 tries to fake user_id in request, server ignores it
Configuration
// config/remote-eloquent.php 'encryption' => [ // Enable/disable encryption 'enabled' => env('REMOTE_ELOQUENT_ENCRYPTION_ENABLED', false), // Master encryption key (REQUIRED when enabled) // IMPORTANT: Use a SEPARATE key from Laravel's APP_KEY! // This key is shared between client and server. 'master_key' => env('REMOTE_ELOQUENT_ENCRYPTION_KEY', ''), // Encrypt responses (optional, default: true) // Set to false to encrypt requests only (useful for debugging or performance) 'encrypt_responses' => env('REMOTE_ELOQUENT_ENCRYPTION_RESPONSES', true), // Per-user encryption (optional) 'per_user' => env('REMOTE_ELOQUENT_ENCRYPTION_PER_USER', false), ],
Optional: Encrypt Requests Only
You can choose to encrypt only requests (client → server) and leave responses unencrypted:
# Encrypt requests but NOT responses REMOTE_ELOQUENT_ENCRYPTION_ENABLED=true REMOTE_ELOQUENT_ENCRYPTION_KEY="your-key-here" REMOTE_ELOQUENT_ENCRYPTION_RESPONSES=false
Use Cases:
- Debugging - Easier to inspect response data during development
- Performance - Slightly faster if responses don't need encryption
- Public Data - If responses contain only public data (posts, products, etc.)
- Sensitive Input Only - Protect user input (passwords, payment info) but not server responses
Example:
// Request: User's password encrypted ✅ POST /api/remote-eloquent/execute { encrypted_payload: "..." } // Response: Posts data unencrypted (easier to debug) { success: true, data: [{ id: 1, title: "Hello" }] }
Security Properties
AES-256-GCM provides:
- Confidentiality - Data cannot be read without the key
- Authentication - Tampering is detected via authentication tag
- Integrity - Modified ciphertext fails to decrypt
Encryption Details:
- Algorithm: AES-256-GCM (Advanced Encryption Standard, Galois/Counter Mode)
- Key Size: 256 bits (32 bytes)
- IV Size: 96 bits (12 bytes) - Random per request
- Tag Size: 128 bits (16 bytes) - Authentication tag
- Key Derivation: HKDF-SHA256 (for per-user keys)
Performance
Benchmarks:
- Encryption: ~0.005ms per operation
- Decryption: ~0.005ms per operation
- Key Derivation: ~0.001ms (cached)
- Total Overhead: <0.01ms per request
- Throughput: 10,000+ requests/second
Key Caching:
// Keys cached in memory (singleton pattern) // First request: 0.01ms (derive + encrypt) // Subsequent: 0.005ms (encrypt only, key cached)
Use Cases
1. Sensitive Data Protection:
// Payment information encrypted end-to-end $payment = Payment::where('user_id', auth()->id())->first();
2. Compliance (HIPAA, GDPR, PCI-DSS):
// Medical records encrypted in transit $records = MedicalRecord::where('patient_id', $id)->get();
3. Multi-Tenant Security:
// Each tenant's data encrypted with unique key // Enable REMOTE_ELOQUENT_ENCRYPTION_PER_USER=true $orders = Order::where('tenant_id', $tenantId)->get();
4. Zero Trust Architecture:
// Even administrators cannot inspect encrypted payloads // Requires decryption key to access data
Debugging
Check if encryption is enabled:
use RemoteEloquent\Security\EncryptionService; if (EncryptionService::isEnabled()) { echo "Encryption is ENABLED"; } if (EncryptionService::isPerUserEnabled()) { echo "Per-user encryption is ENABLED"; }
Test encryption/decryption:
$service = EncryptionService::instance(); // Encrypt $encrypted = $service->encrypt(['foo' => 'bar'], auth()->id()); echo "Encrypted: " . $encrypted; // Decrypt $decrypted = $service->decrypt($encrypted, auth()->id()); // $decrypted = ['foo' => 'bar']
Important Notes
⚠️ Key Management:
- NEVER use Laravel's
APP_KEY- This encryption key is shared with mobile clients! - Laravel's
APP_KEYmust stay on the server only - Generate a separate, dedicated key for Remote Eloquent encryption
- This key will be the same on both client and server (that's why it must be separate!)
- Store
REMOTE_ELOQUENT_ENCRYPTION_KEYsecurely (never commit to Git) - Use different keys for dev/staging/production
- Rotate keys periodically for maximum security
⚠️ Performance:
- Encryption adds ~0.01ms per request (negligible)
- Key caching prevents performance degradation
- No impact on database queries
⚠️ Compatibility:
- Works with all features: queries, batches, services
- Transparent to application code
- No changes needed to existing code
Anti-Replay Attack Protection 🛡️
Prevent replay attacks by validating request timestamps and UUIDs. Even if an attacker captures an encrypted payload, they cannot reuse it.
Features
✅ Timestamp Validation - Reject requests older than configured minutes ✅ UUID/Nonce Validation - Each request can only be sent once ✅ Combined Protection - Requests expire AND can only be used once ✅ Clock Skew Detection - Reject requests from the future ✅ Automatic - Transparent integration with encryption
Quick Setup
1. Enable Anti-Replay Protection:
# Enable timestamp validation (requests expire after X minutes) REMOTE_ELOQUENT_TIMESTAMP_ENABLED=true REMOTE_ELOQUENT_TIMESTAMP_MINS=5 # Enable UUID validation (each request can only be sent once) REMOTE_ELOQUENT_UUID_ENABLED=true
2. Done! All requests are now protected against replay attacks.
How It Works
Timestamp Validation:
1. Client adds timestamp + timezone to payload
2. Server validates timestamp age
3. If older than 5 minutes (configurable): REJECTED
4. If from the future (clock skew): REJECTED
UUID Validation:
1. Client adds unique UUID (v4) to payload
2. Server checks if UUID has been used before (cached)
3. If UUID exists in cache: REJECTED (replay attack!)
4. UUID cached for timestamp duration
Combined Protection:
✅ Timestamp: Payload expires after 5 minutes
✅ UUID: Payload can only be sent once (even within time window)
✅ Result: Maximum protection against replay attacks
Configuration
// config/remote-eloquent.php 'anti_replay' => [ // Enable timestamp validation 'timestamp_enabled' => env('REMOTE_ELOQUENT_TIMESTAMP_ENABLED', false), // Request expiration time in minutes 'timestamp_minutes' => env('REMOTE_ELOQUENT_TIMESTAMP_MINS', 5), // Enable UUID/nonce validation 'uuid_enabled' => env('REMOTE_ELOQUENT_UUID_ENABLED', false), ],
Environment Variables
Server (.env):
# Enable timestamp validation REMOTE_ELOQUENT_TIMESTAMP_ENABLED=true # Reject requests older than 5 minutes REMOTE_ELOQUENT_TIMESTAMP_MINS=5 # Enable UUID validation (one-time use) REMOTE_ELOQUENT_UUID_ENABLED=true
Client/Mobile (.env):
# Same configuration as server REMOTE_ELOQUENT_TIMESTAMP_ENABLED=true REMOTE_ELOQUENT_TIMESTAMP_MINS=5 REMOTE_ELOQUENT_UUID_ENABLED=true
Security Benefits
🔒 Prevent Replay Attacks:
Scenario: Attacker captures encrypted network traffic
WITHOUT anti-replay:
❌ Attacker can replay captured payload indefinitely
❌ Server executes same request multiple times
WITH anti-replay:
✅ Timestamp expired → Request rejected
✅ UUID already used → Request rejected
✅ Payload can only be used once within time window
🔒 Protection Modes:
Timestamp Only:
REMOTE_ELOQUENT_TIMESTAMP_ENABLED=true REMOTE_ELOQUENT_UUID_ENABLED=false
- Requests expire after 5 minutes
- Good for: Basic protection, lower cache usage
- Limitation: Can replay within 5-minute window
UUID Only:
REMOTE_ELOQUENT_TIMESTAMP_ENABLED=false REMOTE_ELOQUENT_UUID_ENABLED=true
- Each UUID can only be used once
- Good for: One-time use enforcement
- Limitation: UUIDs cached for 60 minutes by default
Both (Recommended):
REMOTE_ELOQUENT_TIMESTAMP_ENABLED=true REMOTE_ELOQUENT_UUID_ENABLED=true
- ✅ Requests expire after 5 minutes
- ✅ Each request can only be sent once
- ✅ Maximum security
- UUIDs only cached for timestamp duration (efficient)
Use Cases
1. Financial Transactions:
// Payment request can only be sent once $payment = PaymentService::charge(1000, $token);
2. Sensitive Operations:
// Delete operation cannot be replayed Post::find($id)->delete();
3. Compliance (PCI-DSS, HIPAA):
// Audit trail: each request has unique UUID // Replay attacks logged and prevented
Automatic Integration
Anti-replay works automatically with all features:
Queries:
// Timestamp + UUID added automatically $posts = Post::where('status', 'published')->get();
Batch Queries:
// All queries protected $results = BatchQuery::run([ 'posts' => Post::latest()->limit(10), 'stats' => Post::count(), ]);
Services:
// Service calls protected $chargeId = $paymentService->processPayment(1000);
Batch Services:
// Pipeline protected $results = BatchService::pipeline() ->step('payment', [$paymentService, 'charge', [1000]]) ->execute();
Debugging
Check Configuration:
// Check if timestamp validation is enabled $enabled = config('remote-eloquent.anti_replay.timestamp_enabled'); // Check expiration time $minutes = config('remote-eloquent.anti_replay.timestamp_minutes'); // Check if UUID validation is enabled $uuidEnabled = config('remote-eloquent.anti_replay.uuid_enabled');
Test Payload:
use RemoteEloquent\Security\AntiReplayValidator; // Add security fields to payload $payload = ['model' => 'Post', 'method' => 'get']; $secured = AntiReplayValidator::addSecurityFields($payload); // Result: // [ // 'model' => 'Post', // 'method' => 'get', // '_timestamp' => '2025-11-08T10:30:00+00:00', // '_timezone' => 'UTC', // '_uuid' => '550e8400-e29b-41d4-a716-446655440000' // ]
Error Messages
Timestamp Expired:
Request expired. Maximum age: 5 minutes, actual: 12 minutes.
Future Timestamp (Clock Skew):
Request timestamp is in the future. Possible clock skew or attack.
UUID Replay:
Request UUID already used. Replay attack detected.
Missing Fields:
Request timestamp missing. Possible replay attack.
Request UUID missing. Possible replay attack.
Performance
Overhead:
- Timestamp generation: <0.001ms
- UUID generation: <0.001ms
- Cache lookup: ~0.005ms
- Total: <0.01ms per request
Cache Usage:
Timestamp enabled + UUID enabled:
- Cache duration: 5 minutes (timestamp_minutes)
- Cache key pattern: remote_eloquent_uuid:{uuid}
- Cache backend: Laravel's default cache
UUID only:
- Cache duration: 60 minutes
- More cache memory required
Timestamp only:
- No cache required
- Zero cache overhead
Important Notes
⚠️ Clock Synchronization:
- Ensure client and server clocks are synchronized
- Use NTP (Network Time Protocol) on both sides
- Small clock differences (< 1 minute) are acceptable
- Large clock skew will cause legitimate requests to fail
⚠️ Timezone Handling:
- Client sends timezone with timestamp
- Server validates using client's timezone
- No timezone conversion errors
⚠️ Cache Configuration:
- Ensure Laravel cache is configured and working
- Redis recommended for high-traffic applications
- File cache works for development
⚠️ Encryption Integration:
- Anti-replay works with or without encryption
- When encryption enabled: timestamp/UUID encrypted in payload
- Without encryption: timestamp/UUID sent in plain text (still protected)
Recommendation
Production Setup (Maximum Security):
# Encryption REMOTE_ELOQUENT_ENCRYPTION_ENABLED=true REMOTE_ELOQUENT_ENCRYPTION_KEY="your-key-here" REMOTE_ELOQUENT_ENCRYPTION_PER_USER=true # Anti-Replay REMOTE_ELOQUENT_TIMESTAMP_ENABLED=true REMOTE_ELOQUENT_TIMESTAMP_MINS=5 REMOTE_ELOQUENT_UUID_ENABLED=true
Benefits:
- ✅ Payloads encrypted end-to-end
- ✅ Per-user encryption keys
- ✅ Requests expire after 5 minutes
- ✅ Each request can only be sent once
- ✅ Complete protection against replay attacks
Configuration Reference
// config/remote-eloquent.php return [ // 'client' or 'server' 'mode' => env('REMOTE_ELOQUENT_MODE', 'server'), // Client: API URL 'api_url' => env('REMOTE_ELOQUENT_API_URL'), // Server: Authentication middleware (Sanctum by default) 'auth_middleware' => env('REMOTE_ELOQUENT_AUTH_MIDDLEWARE', 'auth:sanctum'), // Server: Model whitelist 'allowed_models' => [ 'Post', 'Comment', ], // Server: Service whitelist 'allowed_services' => [ 'App\Services\PaymentService', 'App\Services\*', // All in App\Services ], // Allowed methods 'allowed_methods' => [ 'chain' => ['where', 'with', 'orderBy', 'limit', ...], 'terminal' => ['get', 'first', 'find', 'count', 'paginate', ...], ], // Batch queries 'batch' => [ 'enabled' => true, 'max_queries' => 10, ], // Payload encryption 'encryption' => [ 'enabled' => env('REMOTE_ELOQUENT_ENCRYPTION_ENABLED', false), 'master_key' => env('REMOTE_ELOQUENT_ENCRYPTION_KEY', ''), 'encrypt_responses' => env('REMOTE_ELOQUENT_ENCRYPTION_RESPONSES', true), 'per_user' => env('REMOTE_ELOQUENT_ENCRYPTION_PER_USER', false), ], // Anti-replay attack protection 'anti_replay' => [ 'timestamp_enabled' => env('REMOTE_ELOQUENT_TIMESTAMP_ENABLED', false), 'timestamp_minutes' => env('REMOTE_ELOQUENT_TIMESTAMP_MINS', 5), 'uuid_enabled' => env('REMOTE_ELOQUENT_UUID_ENABLED', false), ], ];
Environment Variables
Client (Mobile App)
REMOTE_ELOQUENT_MODE=client REMOTE_ELOQUENT_API_URL=https://api.yourapp.com # Encryption (optional but recommended) REMOTE_ELOQUENT_ENCRYPTION_ENABLED=true REMOTE_ELOQUENT_ENCRYPTION_KEY="your-generated-key-here" REMOTE_ELOQUENT_ENCRYPTION_RESPONSES=true REMOTE_ELOQUENT_ENCRYPTION_PER_USER=false # Anti-Replay Protection (optional but recommended) REMOTE_ELOQUENT_TIMESTAMP_ENABLED=true REMOTE_ELOQUENT_TIMESTAMP_MINS=5 REMOTE_ELOQUENT_UUID_ENABLED=true
Server (Backend)
REMOTE_ELOQUENT_MODE=server REMOTE_ELOQUENT_AUTH_MIDDLEWARE=auth:sanctum # Encryption (optional but recommended) REMOTE_ELOQUENT_ENCRYPTION_ENABLED=true REMOTE_ELOQUENT_ENCRYPTION_KEY="same-key-as-client" REMOTE_ELOQUENT_ENCRYPTION_RESPONSES=true REMOTE_ELOQUENT_ENCRYPTION_PER_USER=false # Anti-Replay Protection (optional but recommended) REMOTE_ELOQUENT_TIMESTAMP_ENABLED=true REMOTE_ELOQUENT_TIMESTAMP_MINS=5 REMOTE_ELOQUENT_UUID_ENABLED=true
Optional:
# Disable authentication (NOT recommended) REMOTE_ELOQUENT_AUTH_MIDDLEWARE= # Use Laravel Passport REMOTE_ELOQUENT_AUTH_MIDDLEWARE=auth:api # Batch settings REMOTE_ELOQUENT_BATCH_ENABLED=true REMOTE_ELOQUENT_BATCH_MAX=10
Security Checklist
- Use
REMOTE_ELOQUENT_MODE=serveron backend - Use
REMOTE_ELOQUENT_MODE=clienton mobile - Install and configure Laravel Sanctum
- Configure
allowed_modelswhitelist - Configure
allowed_serviceswhitelist (if using RemoteService) - Add Global Scopes to all models
- Keep authentication enabled (
auth_middleware=auth:sanctum) - Enable payload encryption (
REMOTE_ELOQUENT_ENCRYPTION_ENABLED=true) - Generate secure encryption key (
openssl rand -base64 32) - Use SEPARATE key from APP_KEY (encryption key is shared with clients)
- Consider per-user encryption for sensitive data
- User IDs from auth()->user() only (never trust client-provided IDs)
- Enable anti-replay protection (timestamp + UUID validation)
- Use HTTPS in production
- Test your Global Scopes
- Only mark necessary methods in
$remoteMethodsarray
API Endpoints
When in server mode, these endpoints are automatically registered:
POST /api/remote-eloquent/execute- Execute single queryPOST /api/remote-eloquent/batch- Execute batch queriesPOST /api/remote-eloquent/service- Execute remote service methodPOST /api/remote-eloquent/batch-service- Execute batch service methodsGET /api/remote-eloquent/health- Health check
Testing
// Test health GET https://api.yourapp.com/api/remote-eloquent/health // Test single query POST https://api.yourapp.com/api/remote-eloquent/execute Authorization: Bearer {token} { "model": "Post", "chain": [ {"method": "where", "parameters": ["status", "published"]}, {"method": "orderBy", "parameters": ["created_at", "desc"]} ], "method": "get", "parameters": [] } // Test batch queries POST https://api.yourapp.com/api/remote-eloquent/batch Authorization: Bearer {token} { "queries": { "posts": { "model": "Post", "chain": [{"method": "where", "parameters": ["status", "published"]}], "method": "get", "parameters": [] }, "postCount": { "model": "Post", "chain": [{"method": "where", "parameters": ["status", "published"]}], "method": "count", "parameters": [] } } } // Test batch service methods POST https://api.yourapp.com/api/remote-eloquent/batch-service Authorization: Bearer {token} { "services": { "payment": { "service": "App\\Services\\PaymentService", "method": "processPayment", "arguments": [1000, "tok_visa"] }, "email": { "service": "App\\Services\\EmailService", "method": "sendReceipt", "arguments": [123, 456] } } }
License
MIT
Author
mucan54
Why This Package?
Problem: Mobile apps need to query remote databases, but traditional APIs are messy:
// ❌ Traditional way $response = Http::post('/api/posts', ['filters' => ...]); $posts = $response->json('data');
Solution: Use Eloquent syntax everywhere:
// ✅ With this package $posts = Post::where('status', 'published')->get();
Same code. Client or server. Less is more.