Multi-tenant scheduling and CRM platform built with Laravel and Vue.
Nexa allows organizations to create branded booking pages, manage team availability, capture booking attribution data, and track leads through a CRM pipeline. The project explores tenant isolation, scheduling automation, webhook integrations, custom domains, and offline-first client workflows.
Nexa is a fully functional, production-ready workspace scheduling and sales CRM application. Core capabilities include:
- Multi-Tenant Workspace Architecture: Isolated branding, schedules, settings, and users per organization.
- Premium Interactive Date Picker: A polished, custom-designed inline monthly calendar grid that replaces default HTML input pickers.
- Team & Collective Scheduling: Dynamic unions of provider schedules to show unified booking availability slots.
- Workload-Balanced Round-Robin Routing: Distributes appointments dynamically to the staff member with the lowest scheduled meetings count.
- CRM Pipeline & UTM Attribution: Automatically creates company, contact, and opportunity records from bookings while preserving marketing source parameters (
utm_*). - Google Calendar Synchronization: Encrypted OAuth credentials connection to automatically sync scheduled meetings.
- HMAC-Signed Outbound Webhooks: Secure webhook subscription pipeline utilizing SHA256 HMAC headers to send real-time events to external developer targets.
- Domain & Branding Wizard: Manage custom domains and dynamically trigger mocked Let's Encrypt SSL edge certificates verification checks.
- Offline Resiliency Sync Queue: Client-side Axios interceptors queue and serialize mutations during connection losses, flushing them sequentially when online.
- Autofill Typewriter Simulations: Built-in interactive simulations showing autofill typing flows on login and schedule views for premium demonstrations.
Nexa uses a single-database multi-tenant architecture where application data is scoped by tenant identifiers. Bookings, CRM records, webhook subscriptions, and user accounts are isolated at the application and database layers.
The scheduling engine supports availability checks, provider assignment, and workload-balanced routing. Booking events can trigger outbound webhooks and CRM updates while preserving attribution metadata captured during the booking process.
This repository is currently an active development project and serves as both a learning platform and a foundation for future SaaS experimentation.
Most scheduling applications stop at appointment booking. Nexa explores what happens after a booking is created:
- How should appointments be distributed across a team?
- How can attribution data be preserved?
- How can bookings automatically flow into CRM workflows?
- How can organizations manage branding and domains in a multi-tenant environment?
The project serves as a practical exploration of scheduling systems, CRM workflows, webhook infrastructure, and SaaS architecture using Laravel and Vue.
Nexa is actively maintained and designed to support enterprise capabilities. Our roadmap includes:
- Multi-Tenant Database Isolation: Migrate from a single-database scoping paradigm to dedicated, isolated databases per tenant (e.g., using
spatie/laravel-multitenancy) to meet strict compliance and data protection guidelines. - Stripe Billing & Subscriptions Checkout: Integrate subscription cycles and plan configurations to charge tenants based on staff seat counts, webhook quotas, and booking volumes.
- Real-Time Live WebSockets Feed: Implement Laravel Reverb channels to stream real-time updates directly to the administrator dashboard, providing instant updates for lead stage changes on the CRM Kanban board and timeline feeds.
- Busy Day Tooltips & Availability Indicators: Add micro-tooltips (e.g., "3 slots left") and crossed-out dates directly to the calendar days in the date picker to guide visitors to high-availability dates.
Nexa is designed to run locally on your development system to avoid unnecessary hosting costs.
To explore the scheduling and CRM pipelines:
- Authentication (Demo Auto-Login):
- Access the local
/demoroute. The system will automatically generate a mock demo workspace (demoslug), seed initial analytics, and log you in as a system administrator.
- Access the local
- Branding & Custom Domain:
- Go to Settings -> Branding & Logo and modify the organization name, brand color, and custom domain (e.g.
book.acme.com). The system dynamically updates the layout variables (--primary-color) across all client-facing scheduling interfaces.
- Go to Settings -> Branding & Logo and modify the organization name, brand color, and custom domain (e.g.
- Automated SSL Lifecycle:
- Navigate to the new Settings -> Domains & SSL widget. Observe CNAME record instructions, and click Verify DNS & Activate SSL to trigger a simulated edge Let's Encrypt validation routine.
- Outbound Developer Webhooks:
- Navigate to the new Settings -> Webhooks panel. Configure an endpoint (e.g., mock target URL) and a signing secret. Use the Test Payload button to queue a simulated event, and check the delivery logs list.
- Offline Resiliency Syncing:
- To test offline resiliency: disconnect your network connection (or toggle offline mode in browser devtools). Update a deal stage on the CRM Pipeline board. A floating red warning alert will indicate offline mode. Reconnect your network to trigger automatic client-side synchronization and see the green success banner.
- Simulate a Booking (Round-Robin):
- Visit the public booking link for the workspace (
/booking/team) and append UTM parameters to the URL:/booking/team?utm_source=google&utm_medium=cpc&utm_campaign=summer_promo. - Select the Collective Team Booking option. Book an available slot. The round-robin algorithm will automatically resolve active providers and route the slot to the available provider with the lowest daily appointment count.
- Visit the public booking link for the workspace (
- Inspect the CRM Kanban & Attribution:
- Navigate back to the CRM Pipeline tab. Verify that the newly created deal has the captured UTM attribution parameters (
utm_source=google, etc.) and the deal is associated with the selected provider.
- Navigate back to the CRM Pipeline tab. Verify that the newly created deal has the captured UTM attribution parameters (
- Local Demo Walkthrough Guide
- Future Roadmap
- System Architecture
- Core Technical Specifications
- Directory Workspace Layout
- Deployment & Architectural Considerations
- Prerequisites & Development Setup
- Testing & Verification
Nexa manages tenancy, resolves custom domains, handles collective team availability, executes workload-based round-robin routing, and runs outbound webhook sync hooks:
flowchart TD
subgraph ClientLayer["π Public Booking & HUD HUD"]
A[Visitor to acme.localhost:8000]
U[URL Query Params: utm_source, utm_medium...]
O[Offline Interceptor Queue: LocalStorage]
end
subgraph AppBoundary["βοΈ Nexa Multi-Tenant App Boundary"]
B[TenantResolutionMiddleware]
subgraph SchedulingEngine["π
Scheduling & Routing Core"]
AE[Availability Engine: Conflict Checker]
RR[Round-Robin Workload Allocator]
end
subgraph CRM["πΌ CRM Pipeline & Attribution"]
C[Auto-created CRM Company]
D[Auto-created CRM Contact]
E[CRM Deal + UTM Attribution]
end
db[("πΎ SQLite Database <br> [Scoped tenant_id Index]")]
subgraph DeveloperWebhooks["π‘ Developer Webhook Pipeline"]
WS[Webhook Service]
QJ[Queue: DispatchWebhookJob]
WL[Webhook Delivery Logs]
end
end
subgraph External["π Integrations & Edge Protocols"]
G[Google Calendar OAuth API]
LE[Mock Let's Encrypt Certificate Edge Validations]
R[Outbound Developer Endpoint]
end
%% Routing
A -->|1. Resolve Subdomain| B
B -->|2. Scopes Session| db
%% Booking wizard
A -->|3. Check Slots| AE
AE -->|4. Query Calendar Syncs| G
A -->|5. Book Slot| RR
U -->|6. Append Attribution| RR
%% Round-Robin workload selection
RR -->|Lowest Workload Provider| db
%% CRM and Attribution
RR -->|7. Auto-Sync Profile| C
RR -->|8. Link Contact| D
RR -->|9. Write Deal with UTM| E
%% Offline Syncing
O -->|11. Sequenced Flush when Online| B
%% Outbound Webhooks
db -->|12. Event Trigger| WS
WS -->|13. HMAC SHA256 Signature| QJ
QJ -->|14. HTTP POST + Retry Backoff| R
QJ -->|15. Log Latency| WL
%% SSL Provisioning
B -->|16. Dynamic SSL verification| LE
To protect external endpoints against payload injection attacks, Nexa computes an HMAC SHA256 signature using the webhook's configured secret and attaches it to the X-Nexa-Signature header:
// From /app/Jobs/DispatchWebhookJob.php
$payloadJson = json_encode($this->payload);
$signature = hash_hmac('sha256', $payloadJson, $subscription->secret);
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'X-Nexa-Event' => $this->event,
'X-Nexa-Signature' => $signature,
])->timeout(10)->post($subscription->url, $this->payload);Collective booking slots route dynamically to the staff member with the lowest appointment count for the selected day. If counts are equal, the system routes to the first available member (lowest User ID):
// From /app/Http/Controllers/PublicBookingController.php
foreach ($providers as $p) {
$check = $availabilityService->checkAvailability($p, $start, $end);
if ($check['available']) {
$count = Appointment::where('staff_id', $p->id)
->whereBetween('start_time', [$startOfDay, $endOfDay])
->count();
$eligibleProviders[] = ['provider' => $p, 'count' => $count];
}
}
usort($eligibleProviders, function($a, $b) {
if ($a['count'] === $b['count']) {
return $a['provider']->id <=> $b['provider']->id;
}
return $a['count'] <=> $b['count'];
});
$provider = $eligibleProviders[0]['provider'];Nexa intercepts data mutations (POST, PUT, DELETE, PATCH) on network failure or offline states, serializes them to localStorage, and queues them for sequential FIFO syncing when connection returns:
// From /resources/js/utils/offlineQueue.js
export function registerOfflineInterceptor(onOfflineTriggered, onSyncCompleted) {
axios.interceptors.request.use((config) => {
if (isMutation(config) && !navigator.onLine) {
addToQueue(config);
if (onOfflineTriggered) onOfflineTriggered(config);
return Promise.reject({ isOfflineQueue: true, config });
}
return config;
});
}To support tenant data scoping without risking duplicate guest emails colliding across organizations, Nexa drops the global database-level index and enforces a composite database-level index:
// From /database/migrations/2026_06_08_000000_fix_users_unique_constraint.php
Schema::table('users', function (Blueprint $table) {
$table->dropUnique(['email']);
$table->unique(['email', 'tenant_id']);
});Nexa/
βββ app/
β βββ Http/
β β βββ Controllers/
β β β βββ Admin/ # Admin Dashboard, CRM, AI, Webhook & OAuth Controllers
β β βββ Middleware/ # Tenant Resolution & Subscription Limit Checkers
β βββ Jobs/ # Outbound Webhooks (DispatchWebhookJob) & Calendar Sync
β βββ Models/ # Multi-tenant Eloquent models (WebhookSubscription, WebhookDelivery)
β βββ Services/ # Availability, WebhookService, Google & Outlook calendar APIs
β βββ Traits/ # BelongsToTenant global scoping traits
βββ database/
β βββ migrations/ # Webhooks schema & CRM UTM column migrations
βββ resources/
β βββ js/ # Vue 3 SPA Client Workspace
β β βββ pages/ # Calendar, Settings (Webhook/SSL panel), and Auth views
β β βββ utils/ # Offline queue helper (offlineQueue.js)
β βββ views/ # Server-side Blade layouts (app.blade.php)
βββ routes/
βββ web.php # Web, public bookings, and protected admin API routing
Nexa is optimized for local environments and cost-free execution. To avoid cloud hosting fees, the production platform has purposefully not been deployed to a live server. Below is an architectural overview of why serverless platforms like Vercel are unsuitable for Nexa, followed by budget-friendly alternatives.
- Stateless Serverless Runtimes (SQLite Conflict): Nexa uses a local SQLite database (
database.sqlite) for storing bookings and CRM data. Vercel's serverless functions are ephemeralβthey spin down and reset, which would completely erase your database contents and cause data desynchronization across concurrent client requests. - Persistent Background Queues: Developer webhook dispatches and email schedules run on Laravel's queue worker (
php artisan queue:work). Vercel does not support long-running, persistent processes to process queue jobs. - No Native PHP Support: Vercel is designed for Node.js/frontend runtimes. Running Laravel on Vercel requires community-built runtimes (like
vercel-php) and complex routing layers (vercel.json) that are fragile and harder to maintain.
If you decide to deploy Nexa in the future without incurring high costs, consider these alternatives:
- Fly.io / Railway / Render (Zero-to-Low Cost PaaS): These platforms offer a git-push workflow like Vercel but run standard persistent containers. You can attach a persistent volume for the SQLite database or host a free tier PostgreSQL database alongside the app.
- VPS + Laravel Forge / Ploi (~$4/mo): You can rent a low-cost VPS from Hetzner, OVH, or DigitalOcean, and manage it with Laravel Forge or Ploi. This automatically sets up Nginx, PHP, SSL (Let's Encrypt), queues, and scheduled cron jobs.
- PHP >= 8.2 (with PDO SQLite extension enabled)
- Composer
- Node.js >= 18
- SQLite 3
-
Clone & Configure Environment:
git clone https://github.com/Hubrisdog/nexa.git cd nexa cp .env.example .env -
Install Dependencies:
composer install npm install
-
Database Initialization:
php artisan key:generate php artisan migrate --seed
-
Run Vite Development Server:
npm run dev
-
Start PHP Server:
php artisan serve
Open
http://localhost:8000in your browser.
Run the full PHPUnit verification suite, including our webhooks and round-robin features:
php artisan testThe test suite runs on an isolated in-memory database, verifying:
- Webhook Subscriptions CRUD and HMAC signatures.
- Outbound event delivery and request telemetry logging.
- Team scheduling union calculations.
- Lowest-workload workload round-robin allocation logic.
