Skip to content

Framework Defaults

The starter configures opinionated behaviors across its service providers and bootstrap that differ from stock Laravel. These defaults affect how you write code in this application. Understanding them helps avoid surprises.

Configured in EloquentServiceProvider. These affect every model in the application.

Model::unguard();

Laravel default: Models are guarded, you must declare $fillable or $guarded to allow mass assignment.

Starter behavior: All mass assignment is allowed on all models. $fillable and $guarded are ignored.

Rationale: User input MUST pass through Laravel’s form request validation before reaching models. The validator returns only known validated fields, solving the problem $fillable was designed for, without keeping arrays in sync with your schema.

Model::preventLazyLoading(! App::isProduction());

Laravel default: Lazy loading is allowed everywhere, which silently introduces N+1 query problems.

Starter behavior: Prevention depends on the environment and debug mode:

EnvironmentBehavior
Non-production with debug modeThrows an exception, you see the N+1 immediately
Non-production without debugReports to your exception handler (Sentry and logs) without breaking the request
ProductionAllowed silently, nothing is thrown or reported

Lazy loading is not checked in production at all, so catch violations locally and in non-production environments before they ship.

Model::preventAccessingMissingAttributes(! App::isProduction());

Laravel default: Accessing a non-existent attribute returns null with no warning.

Starter behavior: Throws in non-production, falls back to null in production.

This catches typos during development (e.g., $user->frist_name instead of $user->first_name) that would otherwise produce null.

Relation::morphMap(array_merge(
// Model::MORPH_TYPE => Model::class,
));

Laravel default: Polymorphic relationships store fully-qualified class names in *_type database columns.

Starter behavior: The morph map is scaffolded but empty. When you add polymorphic relationships, register aliases here so the database stores stable strings (e.g., user) instead of class paths like App\Domains\User\Models\User.

This prevents renaming/moving a model from breaking existing polymorphic references, and keeps implementation details out of the database. The recommended pattern is to define a MORPH_TYPE_MAP constant on each model’s base class and merge it here.


Configured in AppServiceProvider and EagerLoadEloquentUserProvider.

app/Providers/EagerLoadEloquentUserProvider.php
$this->withQuery(function (Builder $query) {
$query->with(['roles.role_type', 'roles.permissions']);
});

A custom auth user provider (EagerLoadEloquentUserProvider) replaces Laravel’s default EloquentUserProvider. It eager-loads the roles, roles.role_type, and roles.permissions relationships on every authenticated request, preventing N+1 queries when checking authorization.

If you need additional relationships available on the authenticated user, add them to the withQuery callback in this class.

app/Providers/AppServiceProvider.php
Gate::before(static function (User $user): ?true {
if ($user->currentAccessToken() instanceof \Laravel\Passport\Contracts\ScopeAuthorizable) {
return null;
}
return $user->hasPermissionTo(SystemPermission::ManageAll) ? true : null;
});

Users with the ManageAll permission bypass Gate and Policy checks: every $this->authorize(), @can, and Gate::allows() call returns true for them. This is the mechanism behind super admin access. It doesn’t apply to requests made with a token, which stay limited to the token’s scopes and the person’s own permissions.


Configured in AppServiceProvider and bootstrap/app.php.

$localOverHttp = App::isLocal() && ! str_starts_with((string) config('app.url'), 'https://');
if (! App::environment(['ci', 'testing']) && ! $localOverHttp) {
URL::forceScheme('https');
}

All generated URLs use HTTPS in deployed environments. CI and testing environments are excluded so tests don’t require SSL certificates. Locally, URLs follow APP_URL: HTTPS under Herd, and plain HTTP when a worktree or agent serves the app on another port. The session cookie’s secure flag follows the same rule unless SESSION_SECURE_COOKIE is set.

DB::prohibitDestructiveCommands(App::isProduction());

migrate:fresh, migrate:refresh, migrate:reset, migrate:rollback, and db:wipe are blocked in production. db:seed is not blocked. This prevents accidentally destroying a production database.

if (App::environment(['ci', 'testing'])) {
Http::preventStrayRequests();
}

In CI and testing environments, any outbound HTTP request made via Laravel’s Http client that hasn’t been explicitly faked will throw an exception. This ensures your test suite doesn’t accidentally make real HTTP calls to external services, verifying that all network dependencies are properly mocked. If you see a stray request error, you need to add Http::fake() for that endpoint.

if (App::environment(['local', 'ci', 'testing'])) {
RequestException::dontTruncate();
}

In local, CI, and testing environments, HTTP client exceptions include the full response body instead of truncating it. This makes debugging failed API calls easier.


Configured in FilamentServiceProvider, except the impersonation banner. These apply to every panel.

All DateTimePicker, TextColumn, and TextEntry components that display datetime values automatically use the authenticated user’s timezone. This means timestamps are displayed in each user’s local time in every panel. No manual ->timezone() calls are needed. Users choose their timezone on the Account area’s Preferences page.

SettingValue
Default pagination25 per page
Pagination options10, 25, 50, 100
Deferred filtersDisabled (filters apply immediately)
DateTime formatFrom config('platform.datetime_display_format')

All Select and SelectFilter components render as Filament’s custom searchable select instead of the browser’s native <select> element. This provides a consistent, searchable dropdown experience across every panel.

All Filament export actions default to CSV format and write to the s3 disk.

An impersonation warning banner is injected at the top of the page body when a user is being impersonated, ensuring administrators always know when they’re acting as another user.

Each panel turns the banner on through the Northwestern theme plugin, in AppPanelProvider and AdministrationPanelProvider:

NorthwesternTheme::make()
->impersonationBanner()

A panel you add needs the same call to show it.


Configured in bootstrap/app.php.

API routes return errors as structured RFC 9457 Problem Details JSON instead of Laravel’s default error format. This is handled by ProblemDetailsRenderer and applies to all api/* routes and requests that send Accept: application/json.

When a database connection times out (common with scale-to-zero RDS in non-production), the application renders a custom “database paused” error page instead of a generic 500. In non-production environments, these timeout errors are also suppressed from error reporting to reduce noise during RDS wake-up periods.

->withEvents(discover: [
__DIR__ . '/../app/Domains/*/Listeners',
])

Laravel’s event auto-discovery is scoped to app/Domains/*/Listeners. Listener classes in these directories are automatically registered without needing an EventServiceProvider. The framework infers the event from the handle() method’s type-hint.