A multi-tenant appointment-booking application for local service businesses, built with Laravel, Filament and Livewire. Businesses manage services, staff schedules and bookings; customers book through a tenant-specific public calendar.
This README is the developer/operator entry point. Update it alongside development changes; the maintenance checklist defines what to keep synchronized.
- Public guest booking at
/{tenant}/book, with service, employee and time-slot selection. - Availability calculated from service duration, employee schedules, existing bookings and temporary slot holds.
- Separate global and tenant administration panels, with role-based access and tenant membership checks.
- No-payment, full-upfront and deposit payment policies; Stripe direct accounts and Stripe Connect onboarding/webhooks.
- Business-side cancellation and rescheduling, payment snapshots, and automatic refunds for eligible cancellations.
- Queued booking confirmation, cancellation, rescheduling and reminder notifications through email/SMS preferences.
- Tenant-configurable 24/48-hour reminders and a tenant dashboard with metrics and upcoming appointments.
- English/Spanish translation resources and tenant currency configuration.
Implementation is not a claim of production readiness or successful provider integration in your environment. See requirements versus implementation for the roadmap boundary.
twilio/sdkis locked at 8.12.1, satisfying the declared^8.0requirement. Composer validation and installation were checked in an existing checkout; a clean fresh installation and live SMS delivery have not been verified.
| Component | Repository baseline |
|---|---|
| PHP | ^8.3, Composer, and extensions required by the locked dependencies |
| Backend | Laravel 13.18.1, Filament 5.6.8 in composer.lock; Livewire public UI |
| Frontend | Vite 8.1.3, Tailwind CSS 4.3.2, Laravel Vite plugin 3.1.0 in package-lock.json |
| Node.js | Vite/plugin engine requirement: ^20.19.0 or >=22.12.0; npm |
| Local database | SQLite with PDO SQLite for the recipe below |
| Engine-specific validation | Disposable MySQL/MariaDB database; see the hold-expiry guide |
Versions above describe lockfiles, not necessarily installed packages. MySQL is the database target in the PRD; tracked runtime configuration defaults to SQLite. Do not infer that every configured database driver has been validated.
Use a fresh checkout and a disposable local database only. Never point these commands at production or a shared database. Dependency scripts can boot the application; migrations write schema/data, and workers may call external providers.
After the dependency-lock blocker is resolved:
- Clone the repository and open its root directory.
- Create your local
.envdeliberately. Review its settings and inherited shell environment before running Composer or Artisan. Do not copy production credentials or commit this file. - Select SQLite, clear any
DB_URLoverride, and use only synthetic booking/contact data. A minimal local configuration is shown below; it is not a copy of the environment template.
APP_NAME="Booking Platform"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000
DB_CONNECTION=sqlite
DB_URL=
CACHE_STORE=database
QUEUE_CONNECTION=database
MAIL_MAILER=logLeave DB_DATABASE unset to use database/database.sqlite, or explicitly select your disposable SQLite file. Keep Stripe/Twilio credentials absent until you intentionally test an integration. Use email-only preferences locally; log mail can contain contact data, so do not use real customer details.
- On this fresh local checkout, install dependencies, generate its application key and create the SQLite file:
composer install
php artisan key:generate
php -r "file_exists('database/database.sqlite') || touch('database/database.sqlite');"- Confirm configuration still targets that disposable database, then create its schema and build assets:
php artisan config:clear
php artisan migrate
npm install --ignore-scripts
npm run buildDo not regenerate APP_KEY for an existing installation with encrypted data. Database-backed cache and queues require their migration tables. The default setup does not create business data or panel accounts.
composer run setup runs, in order:
composer install.- Copies
.env.exampleto.envonly when.envis absent. php artisan key:generate.php artisan migrate --force.npm install --ignore-scriptsandnpm run build.
It does not seed data, create the SQLite file explicitly, or pause for database confirmation. Because it generates a key and forces migrations, it is not a safe default for an existing or shared environment. Prefer the explicit local steps above; inspect configuration before using the shortcut.
On an empty, disposable local database only:
php artisan db:seedDatabaseSeeder creates a demo salon (demo-salon), users for all four roles, a haircut service, an employee assignment and sample schedules. Visit /demo-salon/book afterward and review the staff schedules in the tenant panel before relying on demo availability.
The seeder uses fixed fixture accounts and is not an account-provisioning mechanism. Do not expose seeded accounts publicly, reuse their credentials, or run it against real business data. It uses inserts rather than an idempotent import; repeating it may fail on existing records.
These names and defaults come from tracked PHP configuration, not a validated environment-template inventory. Tenant business settings are stored separately in the database.
| Category | Settings and source |
|---|---|
| Application | APP_NAME, APP_ENV, APP_DEBUG, APP_URL, APP_KEY, APP_LOCALE, APP_FALLBACK_LOCALE in config/app.php; timezone is configured as UTC |
| Database | DB_CONNECTION, DB_URL, DB_DATABASE; server connections also use DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD in config/database.php |
| Cache | CACHE_STORE defaults to database; optional DB_CACHE_CONNECTION in config/cache.php |
| Queue | QUEUE_CONNECTION defaults to database; optional DB_QUEUE_CONNECTION and DB_QUEUE in config/queue.php |
MAIL_MAILER defaults to log; SMTP uses MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_SCHEME; sender uses MAIL_FROM_ADDRESS/MAIL_FROM_NAME in config/mail.php |
|
| SMS | TWILIO_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER in config/services.php; SDK 8.12.1 is locked, but live delivery is unverified |
| Platform Stripe | STRIPE_SECRET, STRIPE_PUBLISHABLE_KEY, STRIPE_CONNECT_CLIENT_ID, STRIPE_CONNECT_WEBHOOK_SECRET in config/services.php |
| Booking holds | Default hold TTL is 10 minutes in config/booking.php |
Tenant settings include payment policy, deposit percentage, currency, refund window, reminder lead time and notification settings. Direct Stripe mode resolves credentials/webhook signing secret from the tenant; Connect mode uses platform configuration and the tenant's connected account. Global Stripe variables do not replace tenant direct-account configuration.
Store secrets outside source control. Enable provider integrations deliberately with test/sandbox configuration and synthetic recipients; queue processing, onboarding, payments and refunds can make real external calls.
Start the local development processes:
composer run devThis starts php artisan serve, php artisan queue:listen --tries=1 --timeout=0, Laravel Pail and the Vite development server. It does not start the scheduler. In a separate terminal:
php artisan schedule:workIf you manage processes separately, use php artisan serve, npm run dev, and a queue worker such as php artisan queue:work. Do not start an extra worker just because the Composer development shortcut already includes one.
Scheduled jobs are registered in routes/console.php:
| Command | Frequency | Purpose |
|---|---|---|
booking:clean-holds |
Every minute | Clean expired slot holds |
booking:send-reminders |
Every minute | Queue notifications in the tenant's 24/48-hour reminder window |
booking:auto-refund |
Hourly | Process eligible cancellation refunds |
A queue worker processes webhook jobs and notifications; the scheduler initiates timed work. Running only the web server is insufficient. Reminder selection uses a narrow time window around the configured lead time, so scheduler uptime matters.
Refund eligibility requires a cancelled booking, a paid/partial payment, a Stripe payment intent and valid cancellation audit data. Business cancellations bypass the customer cutoff; customer cancellations must satisfy the tenant refund window. The hourly command is not permission to refund arbitrary bookings.
This is a local-development recipe, not a verified production deployment runbook. Before exposing an installation:
- Serve only the
public/directory over HTTPS; disable debug output and protect secrets, logs and backups. - Provision real accounts securely; never seed demo users into production.
- Keep a supervised queue worker running and invoke
php artisan schedule:runevery minute through your scheduler/cron service. Use one intentional scheduling arrangement rather than overlapping local and server schedulers. - Configure Stripe callback/webhook URLs for the correct environment, signing secrets and account mode; verify delivery with test-mode events before enabling real payments.
- Validate database migrations, engine-specific constraints, backup/restore and payment/notification behavior independently before release.
Consult Laravel's queue, scheduler and deployment documentation for process-management details.
| Route | Purpose/access |
|---|---|
GET / |
Welcome page, not the booking calendar |
GET /{tenant}/book |
Public calendar; {tenant} is the business slug |
/super-admin |
Filament global panel; Super Admin role |
/tenant |
Filament tenant panel; Business Admin and Employee roles, restricted to their tenant |
GET /up |
Laravel health endpoint; not a provider/queue readiness guarantee |
GET /stripe/connect/start |
Start Connect onboarding; authenticated Business Admin with a tenant |
GET /stripe/connect/callback |
Connect callback with tenant/session-state checks |
POST /webhooks/stripe/connect |
Connect webhook, signature/account validation and queued processing |
POST /webhooks/stripe/{tenant} |
Direct-account webhook using tenant slug and signing secret |
Panel login screens are provided by Filament. The tenant panel registers bookings, services, users, schedules and its dashboard; resource policies further restrict actions. A Super Admin is not automatically granted tenant-panel access, and the Client role has no Filament panel access.
See web routes, panel providers and User access contracts for the actual access rules.
Install development dependencies before testing. Use a disposable local checkout with no production database settings or live provider credentials. phpunit.xml sets both environment and server values for a testing environment and in-memory SQLite settings; the shared application test case checks effective configuration before database refresh or migration and rejects unsafe connection shapes. This guard does not protect arbitrary application code or service providers executed during bootstrap. Check inherited environment overrides before running any suite.
composer testThe script clears cached configuration, then runs php artisan test. phpunit.xml configures SQLite :memory:, an empty DB_URL, array cache/mail/session stores and a synchronous queue. Those are test settings, not the runtime defaults. Existing shell variables can affect the effective configuration; do not assume the test filename alone guarantees isolation.
A focused example after confirming that isolation:
php artisan test --filter=AvailabilityServiceTestThe suite covers domain services, bookings, notifications, Stripe/webhooks, policies and Filament resources. This README does not report a test run or guarantee external provider delivery.
SQLite results do not establish MySQL/MariaDB locking or index behavior. Follow the separate booking-hold expiry validation guide only against a disposable database: it includes destructive migrate:fresh instructions and a receipt to fill with observed results, not proof that your environment passed.
The application uses a shared database with tenant-linked models, service-layer booking logic, Filament administration and a Livewire public booking flow.
| Location | Responsibility |
|---|---|
app/Models, app/Enums, app/Policies |
Domain records, role/status definitions and authorization |
app/Services |
Availability, holds/bookings, Stripe account resolution, refund eligibility, notifications and dashboard metrics |
app/Livewire, resources/views |
Public calendar and Blade presentation |
app/Filament, app/Providers/Filament |
Admin resources, widgets and panel registration |
app/Http/Controllers, routes/web.php |
Public entry points, Connect onboarding and webhook reception |
app/Jobs, app/Notifications, app/Channels |
Background processing and email/SMS delivery |
app/Console/Commands, routes/console.php |
Hold cleanup, reminders and refunds |
database/migrations, database/seeders |
Schema evolution and optional local fixtures |
resources/css, resources/js, lang |
Frontend assets, tenant theme and translations |
tests/Unit, tests/Feature |
Automated domain and application checks |
- Product requirements (Spanish): product intent and roadmap, not implementation evidence.
- OpenSpec specifications: detailed contracts; confirm behavior against current code and tests.
- MySQL/MariaDB hold-expiry guide: engine-specific validation procedure and safety constraints.
- Laravel documentation: framework concepts and operational reference.
Do not treat every PRD item as shipped. Customer self-service history and autonomous cancellation/rescheduling links, a ready-to-run Sail/Docker environment, and the PRD's coverage target remain requirements to verify or deliver; they are not promised by this README. Business-side booking actions and refund-policy support do not imply a public customer-management flow.
Documentation is part of each relevant development change, not a one-time deliverable. Before completing a change:
- Update implemented features and routes when behavior or panel/role access changes.
- Recheck prerequisites, lockfile versions and setup instructions when dependencies or installation scripts change.
- Update configuration names/defaults without adding secrets or claiming unverified template contents.
- Update queue/scheduler commands, frequencies and provider requirements when operations change.
- Update the architecture map and deeper-document links when responsibilities move.
- Keep test isolation instructions aligned with scripts and
phpunit.xml; distinguish engine/provider validation from unit tests. - Record known limitations honestly; verify dependency reconciliation before changing installation guidance.
- Keep planned requirements separate from implemented behavior and never invent successful checks.
The Laravel framework is open-sourced software licensed under the MIT license.