Users, routes, and configuration
How Core resolves your user model, the route helpers, API token middleware, and rate limiters it provides to the other modules, and every odden-core config key.
Core provides a few helpers that every Odden module uses to fit into your app: user model resolution, route group attributes, a shared-token middleware for server-to-server endpoints, a CSRF exemption helper, and two rate limiters. You can use them in your own routes too. For settings shared across packages, see Configuration.
The user model
Odden never references App\Models\User. Owners, creators, and authors (owner_id, creator_id, user_id, created_by_id) point at whatever model Odden\Core\Support\UserModel resolves:
odden-core.user_model, set with theODDEN_USER_MODELenvironment variable.- Otherwise
auth.providers.users.model.
ODDEN_USER_MODEL="App\Models\Admin"The class must extend Illuminate\Database\Eloquent\Model, or UserModel::className() throws a RuntimeException.
The migrations create foreign keys to this model's table, so set it before you run them.
use Odden\Core\Support\UserModel;
UserModel::className(); // "App\Models\User"
UserModel::query()->where('email', $email)->first();
UserModel::make(); // new, unsaved instance
UserModel::table(); // "users"
UserModel::displayName($contact->owner); // the user's name, or "Unknown"
UserModel::displayName($contact->owner, 'System'); // custom fallbackdisplayName(?Model $user, string $fallback = 'Unknown') returns the user's name attribute when it's a non-empty string, and the fallback otherwise.
Route groups
Modules keep their route settings in config as a domain, prefix, and middleware block. Odden\Core\Support\RouteGroup::attributes(string $configKey): array turns that block into Route::group() attributes. It drops null, empty-string, and empty-array values, so an unset ODDEN_*_DOMAIN doesn't pin routes to an empty domain. A missing key returns [].
// config/acme-crm.php
'routes' => [
'webhooks' => [
'domain' => env('ACME_CRM_DOMAIN'),
'prefix' => 'hooks/acme',
'middleware' => [],
],
],// routes/web.php
use Odden\Core\Http\Middleware\RequireApiToken;
use Odden\Core\Support\CsrfExemption;
use Odden\Core\Support\RouteGroup;
use Illuminate\Support\Facades\Route;
Route::group(RouteGroup::attributes('acme-crm.routes.webhooks'), function (): void {
Route::post('inbound', InboundWebhookController::class)
->withoutMiddleware(CsrfExemption::middleware())
->middleware([RequireApiToken::class.':acme-crm.api.token', 'throttle:odden-api'])
->name('acme-crm.webhooks.inbound');
});The Marketing, Sales, and Service packages build their routes this way from odden-marketing.routes.*, odden-sales.routes.web, and odden-service.routes.*.
API token middleware
Odden\Core\Http\Middleware\RequireApiToken protects server-to-server endpoints such as webhooks and sending APIs with a shared secret. Its parameter is the config key that holds the token:
->middleware(RequireApiToken::class.':odden-marketing.api.token')- If the config value is not a non-empty string, every request gets a
403"This endpoint is disabled until an API token is configured." Endpoints stay closed until you set a token. - The client can send the token as a bearer token (
Authorization: Bearer <token>), as anX-Odden-Tokenheader, or as a?token=query parameter for providers that can only be given a URL. They're checked in that order. - A missing or wrong token gets a
401"Invalid or missing API token." The comparison useshash_equals().
Query-string tokens can end up in access logs. Prefer the header forms where the caller supports them.
CSRF exemption
Webhooks and cross-site form posts can't carry Laravel's CSRF token. Odden\Core\Support\CsrfExemption::middleware() returns the CSRF middleware classes present in the installed framework, so you can pass them to withoutMiddleware(). It returns whichever of PreventRequestForgery and ValidateCsrfToken exist. Laravel 13 renamed the CSRF middleware to PreventRequestForgery, so on Laravel 13 withoutMiddleware(ValidateCsrfToken::class) alone leaves CSRF protection on.
Route::post('inbound', InboundWebhookController::class)
->withoutMiddleware(CsrfExemption::middleware());Rate limiters
CoreServiceProvider defines two named limiters, keyed by client IP and counted per minute:
| Limiter | Use | Config key | Env var | Default |
|---|---|---|---|---|
odden-public |
Browser-facing submissions: forms, chat, portal replies | odden-core.rate_limits.public |
ODDEN_PUBLIC_RATE_LIMIT |
30 |
odden-api |
Token-authenticated webhooks and sending APIs | odden-core.rate_limits.api |
ODDEN_API_RATE_LIMIT |
600 |
Route::post('contact-us', ContactFormController::class)->middleware('throttle:odden-public');ODDEN_PUBLIC_RATE_LIMIT=30
ODDEN_API_RATE_LIMIT=600Behind a load balancer or proxy, configure Laravel's trusted proxies so $request->ip() returns the client's address. Otherwise all traffic shares one bucket.
Configuration reference
Publish the file with php artisan vendor:publish --tag=odden-core-config to change values that have no environment variable.
| Key | Default | Description |
|---|---|---|
tables.contacts |
odden_contacts |
Table names for each model. Read by the models and migrations, so set them before migrating. |
tables.companies |
odden_companies |
|
tables.properties |
odden_properties |
PropertyDefinition table. |
tables.property_groups |
odden_property_groups |
Not used. Core creates no such table. |
tables.associations |
odden_associations |
|
tables.association_types |
odden_association_types |
|
tables.activities |
odden_activities |
|
tables.property_history |
odden_property_history |
|
tables.lists |
odden_lists |
|
tables.list_memberships |
odden_list_memberships |
|
tables.lifecycle_stage_transitions |
odden_lifecycle_stage_transitions |
|
tables.custom_object_definitions |
odden_custom_object_definitions |
|
tables.custom_object_records |
odden_custom_object_records |
|
user_model |
env('ODDEN_USER_MODEL'), null |
See the user model. |
auto_associate_companies |
false |
Run domain auto-association on every CreateContactAction call. |
freemail_domains |
[] |
Extra domains treated as freemail. |
lifecycle.strict_transitions |
false |
Enforce the transition graph. |
rate_limits.public |
env('ODDEN_PUBLIC_RATE_LIMIT', 30) |
Requests per minute per IP for odden-public. |
rate_limits.api |
env('ODDEN_API_RATE_LIMIT', 600) |
Requests per minute per IP for odden-api. |
enrichment.driver |
env('ODDEN_ENRICHMENT_DRIVER', 'heuristic') |
Default enrichment driver. |
enrichment.auto_enrich |
false |
Enrich every company created through CreateCompanyAction. |
If you rename tables.contacts, note that the has_downloaded_asset and has_attended_event list rules refer to odden_contacts.id by name and stop working.