Domain-Driven Design
The starter follows Domain-Driven Design (DDD) principles to organize code around business domains rather than technical layers.
Directory Structure
Section titled “Directory Structure”Directoryapp
DirectoryConsole
DirectoryCommands/
- …
DirectoryDomains
DirectoryAccess
DirectoryEnums/
- …
DirectoryHttp/
- …
DirectoryModels/
- …
DirectoryPolicies/
- …
DirectorySeeders/
- …
DirectoryApi
DirectoryActions/
- …
DirectoryEnums/
- …
DirectoryHttp/
- …
DirectoryModels/
- …
DirectoryPassport/
- …
- …
DirectoryAuth
DirectoryActions/
- …
DirectoryEnums/
- …
DirectoryHttp/
- …
DirectoryModels/
- …
- …
DirectoryCore
DirectoryCasts/
- …
DirectoryData/
- …
DirectoryEnums/
- …
DirectoryExceptions/
- …
DirectoryHealth/
- …
DirectoryModels/
- …
DirectoryServices/
- …
- …
DirectorySupport
DirectoryActions/
- …
DirectoryEnums/
- …
DirectoryGateways/
- …
DirectoryModels/
- …
DirectoryRepositories/
- …
DirectorySeeders/
- …
- …
DirectoryUser
DirectoryActions/
- …
DirectoryEnums/
- …
DirectoryModels/
- …
DirectoryPolicies/
- …
DirectoryQueryBuilders/
- …
- …
DirectoryFilament
DirectoryAdministration/ the administration panel
DirectoryClusters/
- …
DirectoryPages/
- …
DirectoryResources/
- …
- …
DirectoryApp/ the app panel
DirectoryPages/
- …
DirectoryStarter/Pages/
- …
- …
DirectorySupport/ helpers both panels use
- …
DirectoryHttp/
DirectoryControllers/
- …
DirectoryLivewire/
- …
DirectoryMiddleware/
- …
DirectoryMcp/ the MCP server and its tools
- …
DirectoryProviders/
- …
DirectoryView/
DirectoryComponents/
- …
Domain Organization
Section titled “Domain Organization”Core Domain
Section titled “Core Domain”The Core domain contains foundational functionality that applies across the entire application:
Location: app/Domains/Core/
Responsibilities:
BaseModel, which models extend. Model concerns shared with other applications, such asPrunesAfterRetentionPeriodfor records that expire andRecordsCustomAuditsfor audit events a model’s own changes don’t capture (which the starter’sRecordsAuditEventsbuilds on), come from Chassis- The typed preferences base class (
Data/Preferences), whichUserPreferencesextends - The audit log: the
Auditmodel, theAuditEventenum that names every event, andRecordsAuditEventsfor recording one - Filament’s table
Exportmodel, which prunes exports and their files afterplatform.retention.exportsdays - The
ExternalServiceenum - Configuration validators (
Services/ConfigValidation/) - Shared exceptions (
NoRollbackException,ServiceDownError,SentryExceptionHandler) - Custom health checks (
Health/) - Shared casts (
MarkdownWithJiraLinksCast)
When to add code to Core:
- Functionality needed by multiple domains
- Infrastructure concerns (logging, caching, etc.)
- Shared enums
- Base classes and traits
- Framework extensions
Auth Domain
Section titled “Auth Domain”The Auth domain handles who someone is: signing in and out.
Location: app/Domains/Auth/
Responsibilities:
- The sign-in methods and the step every sign-in ends with (
SignIn), and single sign-on through Online Passport or Entra ID - Email login codes (
LoginCodes) - Sign-in-as for seeded users in the
localenvironment - Impersonation and its log
Access Domain
Section titled “Access Domain”The Access domain handles what someone may do: roles and permissions.
Location: app/Domains/Access/
Responsibilities:
SystemPermission, the single source of truth for permissions, and the system roles- Roles, role types and permissions, and their seeding
- Recording changes to roles and permissions (
AuditsRoles,AuditsPermissions), and finding which role grants a permission (TracksPermissionSources)
Api Domain
Section titled “Api Domain”The Api domain handles the credentials that call the application:
Location: app/Domains/Api/
Responsibilities:
- Who may see, issue, change, revoke or use each credential (
CredentialAccess), and every OAuth scope (ApiScopes) - Service clients, personal access tokens, OAuth applications and the connections people make to them
- The OAuth consent screen
- The API’s middleware: authenticating tokens, scopes, rate limits and request logging
User Domain
Section titled “User Domain”The User domain handles everything related to user data and management:
Location: app/Domains/User/
Responsibilities:
- User management and profile data
- User segmentation and affiliation
- Directory integration (searching, syncing)
- User login tracking
- Wildcard photo management
Support Domain
Section titled “Support Domain”The Support domain handles the changelog, support tickets and announcements:
Location: app/Domains/Support/
Responsibilities:
- Changelog entries, synced from
resources/changelogs/byChangelogSeeder - Support tickets submitted from the Contact Support page
- Ticket gateways (TeamDynamix and mail), with a fallback to mail when the primary gateway fails
- Announcements: publishing, audiences, dismissals and notifications
Component Types
Section titled “Component Types”Actions
Section titled “Actions”Purpose: Encapsulate single-responsibility business operations
Location: app/Domains/{Domain}/Actions/
Characteristics:
- Single method,
__invoke(), usually on areadonlyclass - Focused on one specific operation
- Testable in isolation
- Resolved from the container: injected into controllers, other actions and Filament action closures, or called with
resolve(SomeAction::class)(...) - The rules belong inside: an action that issues, changes or revokes a credential takes the person acting, asks
App\Domains\Api\CredentialAccesswhether they may, and records its audit event when someone else’s credential changes, so every caller gets the same behavior (CreatePersonalAccessToken,DisconnectApplication,RegisterOAuthApplication). Only seeders and scheduled sweeps pass no one
Example:
namespace App\Domains\User\Actions;
class DetermineUserSegment{ public function __invoke(User $user): UserSegment { // Business logic to determine user segment }}When to use Actions:
- Complex business logic that doesn’t belong in a model
- Operations that span multiple models
- Reusable operations called from multiple places
- Operations that need to be tested in isolation
Purpose: Define sets of related constants with behavior
Location: app/Domains/{Domain}/Enums/
Characteristics:
- Backed by strings or integers
- Can have methods for labels, descriptions, behavior
- Type-safe throughout the application
Example:
namespace App\Domains\Auth\Enums;
use Filament\Support\Contracts\HasLabel;
enum AuthType: string implements HasLabel{ case SSO = 'sso'; case Local = 'local'; case API = 'api';
public function getLabel(): string { return match ($this) { self::SSO => 'NetID', self::Local => 'Verification Code', self::API => 'API', }; }}Models
Section titled “Models”Purpose: Represent database tables and their relationships
Location: app/Domains/{Domain}/Models/
Characteristics:
- Extend
BaseModel, except where a package needs to own the parent class:UserextendsAuthenticatable,RoleandPermissionextend Spatie’s, andOAuthClientandOAuthTokenextend Passport’s.ApiRequestLogextends Eloquent’sModeldirectly, so the request log isn’t audited. - Include relationships, scopes, and accessors
Base Model:
All models should extend BaseModel which provides:
- Automatic audit logging
- Shared scopes and utilities
Services
Section titled “Services”Purpose: Complex operations or integrations with external systems
Location: app/Domains/{Domain}/Services/
Characteristics:
- More complex than Actions
- Often stateful
- Handle external integrations
- May coordinate multiple Actions
When to use Services:
- External API integrations
- Complex calculations or transformations
- Operations requiring significant setup
Query Builders
Section titled “Query Builders”Purpose: Encapsulate complex queries and scopes
Location: app/Domains/{Domain}/QueryBuilders/
Characteristics:
- Extend Eloquent’s query builder
- Provide fluent, chainable query methods
- Keep query logic out of controllers or models
Example:
namespace App\Domains\User\QueryBuilders;
use App\Domains\Auth\Enums\AuthType;use Illuminate\Database\Eloquent\Builder;
/** * @template TModel of User * * @extends Builder<TModel> */class UserBuilder extends Builder{ public function sso(): self { return $this->where('auth_type', AuthType::SSO); }
public function whereEmailEquals(string $email): self { $normalized = strtolower(trim($email));
return $this->where('email', 'ilike', $normalized); }}Adding New Domains
Section titled “Adding New Domains”As your application grows, you may want to add new domains. For example, if building a course management system, you might add:
Directoryapp/Domains/Course/
DirectoryActions/
- EnrollStudent.php
- UnenrollStudent.php
DirectoryEnums/
- CourseStatus.php
DirectoryModels/
- Course.php
- Enrollment.php
- Section.php
DirectoryServices/
- CourseRegistrationService.php
Guidelines for new domains:
- Domain should represent a business concept - Not a technical layer
- Domain should have clear boundaries - Minimal coupling with other domains
- Domain should be cohesive - All code in domain relates to same concept