laravel-core-platform maintained by m0hammadgh92
Laravel 11 Core Platform
A production-grade, business-agnostic, multi-tenant Core platform for Laravel 11, designed to be extracted into a Composer package.
🎯 Overview
This Core platform provides a complete foundation for building multi-tenant SaaS applications with complex business logic. It handles:
- Multi-tenancy with domain-based resolution
- Customer & User separation (authentication ≠ commercial entity)
- Order management with complete lifecycle
- Billing & Invoicing with immutable financial records
- Payment processing with gateway abstraction
- Customer wallet system
- Queue-driven architecture for all side-effects
- Event-driven design for extensibility
- Scoped settings with inheritance
- Activity logging for audit trails
- File management with polymorphic attachments
✨ Key Principles
1. Queue-First Architecture
ALL external operations, slow I/O, emails, notifications, PDFs, payment processing happen via queued jobs ONLY.
2. Event-Driven Core
Business operations emit domain events. Listeners (all queued) respond to events for side-effects.
3. Strict Financial Discipline
- Money stored as INTEGER minor units (cents) - NO FLOATS
- Currency always stored alongside amounts
- Immutable invoice numbers via transactional sequences
- Wallet ledger with balance snapshots
4. Multi-Tenancy by Design
- Every tenant-owned table has
tenant_id - Tenant resolved by request domain
- Global Eloquent scopes prevent cross-tenant data leak
- All queries automatically scoped
5. Customer ≠ User
- Users: Authentication, activity causers
- Customers: Commercial entities, own orders/invoices/payments/wallet
- Orders, invoices, payments NEVER reference users directly
6. Package-Ready from Day One
- No hardcoded business logic
- No direct
env()calls in logic - Everything configurable
- Clear service contracts
📁 Structure
app/
├── Core/
│ ├── Tenancy/ # Multi-tenant infrastructure
│ ├── Identity/ # Users (in app/Models/User.php)
│ ├── CRM/ # Customer management
│ ├── Catalog/ # Products & Variants
│ ├── Sales/ # Orders
│ ├── Billing/ # Invoices, Payments, Wallet, Sequences
│ ├── Pricing/ # Tax Rates, Coupons
│ ├── Settings/ # Scoped settings
│ ├── Notifications/ # Templates & Outbound Messages
│ ├── Files/ # File management
│ └── Audit/ # (uses spatie/activitylog)
├── Support/
│ ├── Money/ # Money value object
│ ├── States/ # (To be created)
│ └── SettingsResolver/ # Settings inheritance resolver
├── Events/ # Domain events (to be created)
├── Listeners/ # Queued event listeners (to be created)
├── Jobs/ # Async jobs (to be created)
└── Policies/ # Authorization (to be created)
modules/ # Future vertical modules (empty)
🚀 Getting Started
Installation
This is already installed! The platform is ready to use.
Environment Setup
-
Configure Database (already using SQLite for development)
-
Configure Redis for queues (production):
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
- Configure Mail (for notifications):
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
Running the Application
# Run migrations (already done)
php artisan migrate
# Start Horizon (queue worker)
php artisan horizon
# Start development server
php artisan serve
# Access Filament admin panel
open http://localhost:8000/admin
🏢 Multi-Tenancy
How It Works
- Request arrives with domain (e.g.,
acme.example.com) ResolveTenantMiddlewareextracts the hostTenantContextqueriestenant_domainstable- Tenant bound to container as
'tenant' - All models with
BelongsToTenanttrait auto-scope to this tenant
Adding a Tenant
php artisan tinker
// Create tenant
$tenant = \App\Core\Tenancy\Tenant::create([
'name' => 'Acme Corporation',
'slug' => 'acme',
'status' => 'active',
'metadata' => [],
]);
// Add domain
$tenant->domains()->create([
'domain' => 'acme.test',
'is_primary' => true,
]);
// Create default brand (optional)
$tenant->brands()->create([
'name' => 'Acme Brand',
'slug' => 'default',
'is_default' => true,
]);
// Create admin user for this tenant
$user = \App\Models\User::create([
'tenant_id' => $tenant->id,
'name' => 'Admin User',
'email' => 'admin@acme.test',
'password' => bcrypt('password'),
'role' => 'admin',
'is_active' => true,
]);
Testing Multi-Tenancy Locally
Add to /etc/hosts:
127.0.0.1 acme.test
127.0.0.1 demo.test
Then access: http://acme.test:8000
💰 Money Handling
All monetary values use the Money value object:
use App\Support\Money\Money;
// From minor units (cents)
$price = Money::fromMinor(1999, 'USD'); // $19.99
// From major units (dollars)
$price = Money::fromMajor(19.99, 'USD'); // $19.99
// Operations
$total = $price->multiply(3); // $59.97
$discount = $price->percentage(2000); // 20% = $3.998 → $4.00
$final = $total->subtract($discount); // $55.97
// Display
echo $price->format(); // "$19.99"
📋 Order Lifecycle
draft
↓ (OrderSubmitted event)
pending_payment
↓ (PaymentCaptured event → OrderPaid event)
paid
↓ (items fulfilled)
closed
OR
canceled (from draft/pending_payment)
refunded (from paid)
Creating an Order
use App\Core\Sales\Order;
use App\Core\Billing\Sequence;
$order = Order::create([
'tenant_id' => $tenant->id,
'customer_id' => $customer->id,
'order_number' => Sequence::nextFor($tenant->id, 'order', 'ORD-'),
'status' => 'draft',
'currency' => 'USD',
// Amounts calculated by OrderCalculator service
]);
// Add items
$order->items()->create([
'tenant_id' => $tenant->id,
'product_variant_id' => $variant->id,
'product_name' => $variant->product->name,
'variant_name' => $variant->name,
'quantity' => 1,
'unit_price_minor' => $variant->price_minor,
'currency' => 'USD',
// Calculate totals
]);
📄 Invoice System
Invoices are immutable financial documents with:
- Unique sequential numbers (thread-safe generation via
Sequence) - Customer snapshot (for legal record)
- Cannot be deleted (only voided)
Creating an Invoice
use App\Core\Billing\Invoice;
use App\Core\Billing\Sequence;
$invoice = Invoice::create([
'tenant_id' => $tenant->id,
'customer_id' => $customer->id,
'order_id' => $order->id,
'invoice_number' => Sequence::nextFor($tenant->id, 'invoice', 'INV-'),
'status' => 'draft',
'currency' => 'USD',
'customer_snapshot' => $customer->toArray(), // Immutable record
]);
// Generate PDF via queued job
GenerateInvoicePdfJob::dispatch($invoice);
👛 Wallet System
Customers can have wallet balances (one per currency):
$wallet = $customer->walletAccounts()->firstOrCreate([
'currency' => 'USD',
], [
'balance_minor' => 0,
]);
// Credit wallet (via WalletService for transactional safety)
app(\App\Core\Billing\Services\WalletService::class)
->credit($wallet, Money::fromMajor(50.00, 'USD'), 'Initial credit');
// Debit wallet
app(\App\Core\Billing\Services\WalletService::class)
->debit($wallet, Money::fromMajor(10.00, 'USD'), 'Payment');
⚙️ Settings System
Settings follow this precedence: user → customer → brand → tenant → global
use App\Support\SettingsResolver\SettingsResolver;
$resolver = app(SettingsResolver::class);
// Get setting with context
$value = $resolver->get(
key: 'theme.color',
default: 'blue',
userId: $user->id,
customerId: $customer->id
);
// Set setting
$resolver->set(
key: 'theme.color',
value: 'red',
scopeType: 'customer',
scopeId: $customer->id
);
🔔 Notifications
All notifications are queued jobs that create OutboundMessage records:
// Listen to event
class OrderConfirmationListener
{
public function handle(OrderSubmitted $event)
{
SendOrderConfirmationJob::dispatch($event->order);
}
}
📦 Queue Lanes
Three priority lanes:
critical: Payment callbacks, critical operations (3 workers, timeout 300s)default: Notifications, emails (5 workers, timeout 180s)low: PDFs, exports, background tasks (2 workers, timeout 600s)
Dispatch to specific queue:
GenerateInvoicePdfJob::dispatch($invoice)->onQueue('low');
PaymentCallbackJob::dispatch($payment)->onQueue('critical');
🧩 Extending with Modules
Future vertical modules for your specific business needs should:
- Live in
modules/ModuleName/ - Have own Models, Services, Events, Jobs
- Can extend Core models (Order, Customer, Product)
- Listen to Core events for integration
- Never depend on other modules
Example:
// modules/YourModule/Models/CustomEntity.php
class CustomEntity extends Model
{
public function order()
{
return $this->belongsTo(\App\Core\Sales\Order::class);
}
}
// modules/YourModule/Listeners/HandleOrderPaid.php
class HandleOrderPaid
{
public function handle(OrderPaid $event)
{
if ($event->order->hasProductType('your_product_type')) {
YourCustomJob::dispatch($event->order);
}
}
}
🔐 Security
Policies (To Be Created)
All Filament resources should use policies:
- Tenant scoping enforced
- Role-based permissions
- Super admin bypass for tenant management
Authentication
Users access Filament admin panel if:
is_active = truerole IN ('super_admin', 'admin')
🧪 Testing
# Run all tests
php artisan test
# Run specific test suite
php artisan test --testsuite=Feature
# Run with coverage
php artisan test --coverage
Critical test areas:
- Tenant isolation (no cross-tenant data leakage)
- Money calculations (precision)
- Sequence generation (thread-safety)
- Wallet transactions (balance integrity)
- Event/listener flow
📊 Admin Panel (Filament)
Access at /admin with admin credentials.
Resources to create:
- Tenants (super_admin only)
- Users
- Customers
- Products & Variants
- Orders
- Invoices
- Payments
- Wallet Accounts
- Coupons
- Tax Rates
- Settings
- Files
- Outbound Messages
- Activity Log
🔄 Development Workflow
Creating a New Domain Feature
- Create migration in
database/migrations/ - Create model in
app/Core/{Domain}/ - Create event(s) in
app/Events/ - Create listener(s) in
app/Listeners/(queued!) - Create job(s) in
app/Jobs/ - Create service/action in
app/Core/{Domain}/Actions/ - Create Filament resource in
app/Filament/Resources/ - Write tests in
tests/Feature/{Domain}/
Example: Adding Refund Support
// 1. Update migrations (already has refunded_minor)
// 2. Create event
class RefundCreated
{
public function __construct(public Payment $payment) {}
}
// 3. Create listener
class UpdateInvoiceAfterRefund
{
public function handle(RefundCreated $event)
{
$invoice = $event->payment->invoice;
$invoice->increment('refunded_minor', $event->payment->amount_minor);
$invoice->save();
}
}
// 4. Create job
class ProcessRefundJob implements ShouldQueue
{
public $queue = 'critical';
public function handle()
{
// Process refund via gateway
// Fire RefundCreated event
}
}
🎯 Roadmap
Completed ✅
- Database schema (all 24 migrations)
- Multi-tenancy infrastructure
- Core models (Tenant, Customer, User, Product, Order, Invoice, Payment, Wallet)
- Money value object
- Settings resolver
- Filament & Horizon integration
In Progress 🔨
- Domain events
- Queued listeners
- Background jobs
- Filament resources
- Policies
Planned 📋
- API layer (optional)
- Webhook system
- PDF generation service
- Payment gateway drivers
- Fulfillment system abstraction
- Test suite
- Package extraction
📚 Resources
📄 License
Proprietary. Not for redistribution.
🙏 Credits
Built with:
- Laravel 11
- Filament 3
- Laravel Horizon
- Spatie Laravel Activity Log
Built for production. Designed for extensibility. Ready for extraction.