# Application Structure
Six primitives divide a magic app's write-heavy screens the way Laravel divides a controller-heavy one: each owns exactly one kind of state, and none of them reaches into another's.
- [Introduction](#introduction)
- [Controllers Own Read State](#controllers-own-read-state)
- [Actions Own Writes](#actions-own-writes)
- [Form Objects Own Form State](#form-objects-own-form-state)
- [Repositories Own Rows](#repositories-own-rows)
- [SessionScope Resets Every Tenant Holder](#sessionscope-resets-every-tenant-holder)
- [Listeners Replace a Central Realtime Dispatcher](#listeners-replace-a-central-realtime-dispatcher)
- [How the Six Compose](#how-the-six-compose)
## Introduction
A controller that also validates, also writes, also owns a paginated collection, and also resubscribes a broadcast channel is not one class doing six jobs well; it is six responsibilities racing each other on one `notifyListeners()`. This page names the six pieces a magic app now has for that: `MagicController` (read state), `MagicAction` (a write), `MagicFormObject` (a screen's form), `Repository`/`RepositoryQuery` (a resource's rows), `SessionScope` (the tenant boundary), and `BroadcastListeners` (the shared channel registry). None of them is new architecture on top of magic; each is the existing primitive from `doc/basics/controllers.md`, `doc/basics/forms.md`, `doc/eloquent/repositories.md`, `doc/digging-deeper/session-scope.md`, and `doc/digging-deeper/broadcasting.md`, read together as one map.
## Controllers Own Read State
A `MagicController` answers "what does this screen show right now": `RxStatus`, a `MagicFormObject`'s field values, a `RepositoryQuery`'s pages. It does not persist a write itself; it runs a `MagicAction` through `RunsActions.runAction` and reacts to the result.
```dart
class MonitorController extends MagicController
with MagicStateMixin>, RunsActions {
static MonitorController get instance => Magic.findOrPut(MonitorController.new);
Future pause(String id) async {
final outcome = await runAction(MagicAction.resolve(PauseMonitor.new), id, key: id);
if (!outcome.succeeded) return;
}
}
```
See `doc/basics/controllers.md` for `MagicStateMixin`, fetch helpers, and lifecycle.
## Actions Own Writes
A `MagicAction` is one stateless write, Fortify's `Actions\*` ported: no `MagicController` base, no `notifyListeners`, nothing to dispose. It throws on failure (a `ValidationException` for a 422 or a failed client rule, anything else for a transport fault) rather than answering a bool, because `RunsActions.runAction` is the one place both are already handled.
```dart
class PauseMonitor extends MagicAction {
const PauseMonitor();
@override
Future handle(String monitorId) async {
final response = await Http.update('monitors', monitorId, {'status': 'paused'});
if (!response.successful) {
throw Exception(response.errorMessage ?? 'Failed to pause monitor');
}
}
}
```
A test swaps the real action for a fake via `MagicAction.bind(() => _FakePauseMonitor())` and clears every override with `MagicAction.flush()` in `tearDown`. See `doc/basics/actions.md`.
## Form Objects Own Form State
A `MagicFormObject` is a Livewire-style Form object: one field-bound, validated, persistable unit for a single screen's write, created once per `State` and never registered in the container (`onClose` asserts against it). It composes `MagicFormData`, `ValidatesRequests`, and `RunsActions` rather than hand-wiring all three per screen.
```dart
class MonitorFormObject extends MagicFormObject {
MonitorFormObject({this.editing});
final Monitor? editing;
@override
Map get initial => {'name': editing?.name ?? '', 'url': editing?.url ?? ''};
@override
FormRequest get request =>
editing == null ? const StoreMonitorRequest() : const UpdateMonitorRequest();
@override
Future persist(Map validated) async {
final monitor = editing ?? Monitor();
monitor.fill(validated, strict: true);
return monitor.save();
}
}
```
See `doc/basics/forms.md`.
## Repositories Own Rows
A `Repository` is an id-keyed cache of one remote resource's rows (Eloquent's identity map); a `RepositoryQuery` is one ordered, filtered, paginated view over it, so a list screen and a show screen reading the same row see the same cached copy without a refetch. Neither owns navigation or form state.
```dart
class MonitorRepository extends Repository {
static MonitorRepository instance = MonitorRepository();
@override
String get resource => 'monitors';
@override
Monitor Function(Map) get fromMap => Monitor.fromMap;
@override
Set get showOnlyKeys => const {'uptime_24h'};
}
```
See `doc/eloquent/repositories.md`.
## SessionScope Resets Every Tenant Holder
`SessionScope` is the seam that keeps a login or a team switch from ever showing one tenant's cached rows to another: it resets every `SessionScoped` controller (found via `Magic.controllers`) and every registered `SessionScoped` holder (a `Repository` registers itself in its constructor) on an identity change, in place, never by deleting and recreating the instance.
```dart
@override
Future boot() async {
SessionScope.identity = () => Auth.check() ? '${Auth.id()}:$teamId' : null;
SessionScope.attach(); // explicit, and LAST: realtime and polling must already point at the new session
}
```
See `doc/digging-deeper/session-scope.md`.
## Listeners Replace a Central Realtime Dispatcher
`BroadcastListeners` owns exactly one `AuthChannelSubscription` per alias, no matter how many controllers listen on it, fanning out to every registered handler instead of one controller's subscription silently dropping another's. A controller declares what it wants via `ListensToBroadcasts.listeners` and never talks to `Echo` directly.
```dart
class MonitorController extends MagicController with ListensToBroadcasts {
@override
Map get listeners => {
'team:check.recorded': (event) => MonitorRepository.instance.patch(
event.data['monitor_id'] as String,
{'last_status': event.data['status']},
),
};
}
```
See `doc/digging-deeper/broadcasting.md#auth-scoped-subscriptions`.
## How the Six Compose
A monitor list screen, put together: `MonitorController` (reads) holds a `RepositoryQuery` over `MonitorRepository.instance` (rows), runs `PauseMonitor` (a write) through `RunsActions`, edits through `MonitorFormObject` (form state), stays correct across a team switch because the repository is `SessionScoped`, and stays live because `MonitorController` mixes in `ListensToBroadcasts` instead of opening its own channel. No primitive here is optional scaffolding: each closes a failure mode the others do not (a write racing itself, a tenant's rows outliving its session, two controllers fighting over one socket) that a single fat controller used to absorb by accident.