Installation
Install the Odden packages, run the migrations, and schedule the commands each module needs.
Requirements
- PHP 8.3 or newer
- Laravel 12 or 13
- SQLite 3.26+, MySQL 8.0+, or PostgreSQL 15+. Every package is tested against all three in CI.
- Filament 5.9 or newer, only if you install the Filament admin
Install the packages
Require the modules you want. Each one requires getodden/crm-core, so you don't need to list it unless you only want Core:
composer require getodden/crm-sales getodden/crm-service getodden/crm-marketingEvery package registers itself through Laravel's package discovery and loads its own migrations. Run them:
php artisan migrateAll Odden tables are prefixed with odden_. Table names are configurable per package (the tables key in each config file) if they would clash with your own.
Add the Filament admin
composer require getodden/crm-filamentThen register the plugin in your panel provider. See Filament admin for the details.
Publish configuration (optional)
Each package works with its defaults. Publish a config file only when you need to change something:
php artisan vendor:publish --tag=odden-core-config
php artisan vendor:publish --tag=odden-sales-config
php artisan vendor:publish --tag=odden-service-config
php artisan vendor:publish --tag=odden-marketing-configMigrations can be published the same way with the odden-core-migrations, odden-sales-migrations, odden-service-migrations, and odden-marketing-migrations tags, if you want to modify them before they run.
Schedule the commands
Several features run on a schedule: campaign sends, workflow steps, SLA checks, quote expiry, and more. Odden doesn't register a schedule for you, so add the commands for the modules you installed to routes/console.php:
use Illuminate\Support\Facades\Schedule;
// Marketing
Schedule::command('marketing:dispatch-scheduled')->everyMinute()->withoutOverlapping()->onOneServer();
Schedule::command('marketing:process-workflows')->everyFiveMinutes()->withoutOverlapping()->onOneServer();
Schedule::command('marketing:evaluate-ab-tests')->hourly()->withoutOverlapping()->onOneServer();
Schedule::command('marketing:decay-lead-scores')->dailyAt('03:00')->withoutOverlapping()->onOneServer();
Schedule::command('marketing:sunset-subscribers')->dailyAt('03:30')->withoutOverlapping()->onOneServer();
// Sales
Schedule::command('sales:process-cadences')->everyFifteenMinutes()->withoutOverlapping()->onOneServer();
Schedule::command('sales:expire-quotes')->dailyAt('01:00')->withoutOverlapping()->onOneServer();
// Service
Schedule::command('service:check-sla')->everyFiveMinutes()->withoutOverlapping()->onOneServer();
Schedule::command('service:run-automations')->hourly()->withoutOverlapping()->onOneServer();These are the frequencies Odden Cloud uses. marketing:dispatch-scheduled should run every minute, because campaigns that send in each recipient's local time are released in five-minute windows. onOneServer() needs a cache store shared by all your servers; drop it if you run a single server.
Then make sure the scheduler runs, with php artisan schedule:work locally or a cron entry for php artisan schedule:run in production.
Configure mail
Configure a mailer in config/mail.php before you use any of the email features.
Run a queue worker. From v0.3, Odden queues every email it sends. Nothing is delivered from the request or command that triggers it: each email is pushed to the queue and a queue worker sends it. Without a worker, mail stays on the queue and is never delivered. This covers:
- Marketing: campaign messages, workflow email steps, campaign proofs, and the transactional email API
- Sales: sequence email steps and meeting confirmations
- Service: ticket notifications (confirmation, agent replies, resolution with a CSAT survey, and SLA breach alerts)
The one exception is the Send Test preview action on marketing templates in the Filament admin, which sends during the request.
Run a worker in production, for example with Supervisor:
php artisan queue:workBy default each package uses your default queue connection, its default queue, and your default mailer. Each package can send its mail on its own connection and queue:
| Package | Config keys | Environment variables |
|---|---|---|
| Marketing | odden-marketing.mail.mailer, .connection, .queue |
ODDEN_MARKETING_MAILER, ODDEN_MARKETING_MAIL_CONNECTION, ODDEN_MARKETING_MAIL_QUEUE |
| Sales | odden-sales.mail.mailer, .connection, .queue |
ODDEN_SALES_MAILER, ODDEN_SALES_QUEUE_CONNECTION, ODDEN_SALES_MAIL_QUEUE |
| Service | odden-service.notifications.connection, .queue |
ODDEN_SERVICE_NOTIFICATIONS_CONNECTION, ODDEN_SERVICE_NOTIFICATIONS_QUEUE |
Service notifications always use your default mailer. If you set a queue name, include it in your worker's --queue list, for example php artisan queue:work --queue=marketing-mail,sales-mail,support-mail,default. See Sending mail, Sales mail configuration (which also sets the Sales "from" address), and Service notifications.
With the sync queue connection the mail is sent during the request, which is fine for local development only.
Next steps
- Configuration: the user model, public routes, API tokens, and rate limits
- Core concepts