Skip to content

The skeleton ​

The iam module ​

FileWhat it does
app/Models/User.phpThe user, stored in the iam_users table. The name field is copied to the modules that keep a copy.
app/Actions/RegisterUser.phpCreates the user and emits iam.user.registered.
app/Actions/LoginUser.phpChecks the email and the password, and issues the session: the same refusal for an unknown email and a wrong password.
app/Actions/IssueToken.phpIssues the user's token: a JWT, or an opaque token with AUTH_TOKEN_VALIDATION_STRATEGY=issuer.
app/Dto/What each action receives, built by the request's toDto().
app/Repositories/UserRepository.phpThe single write path of a user: it forgets the cached answers and rewrites the gateway's copy once the change is committed.
app/Http/Controllers/Api/AuthController.phpPOST /iam/api/v1/register, login, refresh, logout, logout-all, and GET /iam/api/v1/me.
app/Rpc/*Handler.phpAnswer the FindUser, FindUserByToken and Grants messages with iam's own data. Declared in IamServiceProvider::$rpc, they serve every caller, in this process or another.
config/database.phpThe iam connection, and the owner role that migrates it.
config/auth.phpNames User as the authenticated user of iam's routes.

The foundation ​

foundation/Iam/ holds what the other modules are allowed to use from iam:

FileWhat it does
Rpc/FindUser.php, FindUserByToken.php, Grants.phpThe messages iam answers, with the rules that validate them.
Services/IamClient.phpEvery module calls iam through it: in this process when iam runs here, on the module's transport otherwise (HTTP with rpc-http).
Auth/TrustedIdentity.phpReads and signs X-Identity, the only thing a module authenticates (see Authentication).
Auth/JwtTokens.phpChecks a JWT iam issued, for the handler of FindUserByToken.
Auth/RpcPrincipals.phpThe request's user as iam answers findUser(), for a module that keeps no copy.
Events/IamEvent.php, Events/UserRegisteredPayload.phpThe names of the events iam publishes, and the typed payload of each: iam builds it, a consumer reads it with UserRegisteredPayload::from($payload). The payload class also declares its versions (version(), upcast()), so a handler only ever receives the current shape.
Shadows/UserShadow.phpThe copy of iam's users, used as it is by every module that lists it in $shadows, and their authenticated user (auth.principal).
database/shadows/The migration that creates the copy's table in the module that keeps it.

The package runs each handler in iam's context, so Apps\Iam\Rpc\FindUserHandler queries iam's database whichever module calls it.

The analytics module ​

FileWhat it does
app/Handlers/RecordSignup.phpHandles iam.user.registered and stores a signup.
app/Providers/AnalyticsServiceProvider.php$shadows = [UserShadow::class]: analytics keeps iam's users in its own iam_users table.
app/Console/ComputeDatasets.phpanalytics:compute rebuilds analytics_datasets, one row per hour, from the signups. AnalyticsServiceProvider schedules it hourly.
app/Enums/DatasetMeasure.php, MeasureNature.phpThe measures, and how a period folds them: a flow (signups_count) adds up, a state (users_total) keeps the last value.
app/Http/Controllers/Api/MetricController.php, app/Repositories/DatasetRepository.phpGET /analytics/api/v1/metrics?group=hour|day|week|month|none&measures[]=…&filter[from]=…&filter[to]=… folds the hourly rows onto the group; measure() folds one measure over a period.
app/Enums/DatasetGroup.php, app/Dto/, app/Database/MonthlyPartitions.phpThe groupings, what the reading receives (built by the request's toDto()), and the monthly partitions of the datasets table.
app/Providers/AnalyticsServiceProvider.phpDeclares the handler in $handlers and schedules analytics:compute.

A reading never walks the signups: it filters and folds the pre-computed rows, so its cost depends on the period, not on the volume.

The notifications module ​

FileWhat it does
app/Handlers/SendWelcome.phpHandles iam.user.registered and stores a welcome for the new user, once however many times the event arrives.
app/Models/Notification.phpWhat one person was told, in notifications_inbox. It is addressed to its recipient, so every query scopes on the authenticated user: there is no policy to write.
app/Enums/NotificationType.phpWhat a notification is about; it renders the title from lang/en/messages.php and the recipient's copy.
app/Http/Controllers/Api/NotificationController.phpGET /notifications/api/v1/notifications?filter[unread]=1&sort=-created_at&paginate=20 and PATCH /notifications/api/v1/notifications/{id}/read. Both scope on the recipient: someone else's notification is a 404.
app/Actions/MarkNotificationRead.phpMarks one notification of the recipient as read.
app/Dto/, app/Http/Requests/What the inbox and the action receive, built by the request's toDto().
app/Repositories/NotificationRepository.phpThe example of EloquentRepository: it declares the filters and sorts a request may use (any other is a 400), scopes the inbox to its recipient, and is the one place a notification is marked read.
app/Providers/NotificationsServiceProvider.php$shadows = [UserShadow::class]: its copy of iam's users, in iam_users, the authenticated user of its routes.
app/Observers/NotificationObserver.phpOnce the row is committed: pushes it live, then queues one job per channel its type names.
app/Events/NotificationPushed.phpThe live push, on private-user.{id} (Reverb), in the shape the inbox returns; a client that was offline finds it in the inbox.
app/Enums/Channel.php, app/Jobs/SendMail.phpThe channels beyond the inbox. SendMail reads the address in the module's copy of iam's users (UserShadow), so a mail leaves even when iam is down. A new channel (SMS, push) is a case and a job.
routes/api.phpAlso POST /notifications/api/v1/broadcasting/auth: Echo joins private-user.{id} with the API token; only that user may join (Foundation\Common\Broadcasting\PrivateUserChannel).
iam ─ user.registered ─► consumer ─► SendWelcome ─► notifications_inbox
                                                  ├─► NotificationPushed ─► Reverb :8080 ─► browser
                                                  └─► SendMail (queue) ─► worker ─► mail

foundation/Common ​

Foundation\Common\ holds the base classes every module uses. Like the rest of the foundation, any module can use it. These classes are yours: the package doesn't depend on them, so you can change or delete them.

How strongly each piece is meant is labelled:

LabelMeaning
Mechanismthe modules rely on it working this way across processes; replace it only with something that keeps the same guarantee
Defaulta sensible choice that works as shipped; swap it freely
Proposalone way to structure the code, shown so you can judge it
Tastea style choice, nothing depends on it
PathWhat it doesLabel
Http/Controllers/Controller, Http/Requests/ApiRequest, Http/Resources/Resource, Enums/ApiErrorCodeBase controller and request, and the response format: {success, data} or {success, code, message}.Taste
Http/Middleware/Authenticate, Contracts/Auth/TokenValidator, PrincipalResolver, PermissionSource, Principal, Auth/IdentityToken authentication for module routes, in three replaceable steps: see Authentication.Mechanism
Auth/LocalPrincipals, ClaimsPrincipals, ClaimsPermissions, Traits/IsPrincipal, HasPrincipalThe shipped answers to those steps, and the traits a user model and a controller use.Default
Policies/PolicyChecks a permission against the PermissionSource.Mechanism
Contracts/Actions/Action, OperationOne class per use case.Proposal
Contracts/Database/Repository, Repositories/EloquentRepository, Traits/SearchableQueries with filters, sorts and includes, and a single place for writes.Proposal
Dto/DtoTyped input and event payloads, built on spatie/laravel-data.Default
Validation/ReferenceRuleA field holding the id of another module's record: checks it exists through that module's client, then runs the constraints a subclass adds to $checks.Mechanism
Exceptions/DomainException, IntegrityExceptionBusiness errors (400) and system errors (500).Taste

bootstrap/app.php renders the exceptions that implement RendersApiEnvelope in the response format above.