- name
- magic-framework
- description
- Write correct, idiomatic code in a Flutter app that depends on the `magic` framework (Laravel-inspired: IoC container, 18 facades, Eloquent-style ORM, service providers, reactive controllers, GoRouter routing, validation, auth, broadcasting, MagicAction writes, Repository row caches, SessionScope tenant resets). Use whenever code imports `package:magic/magic.dart` or `package:magic/testing.dart`, or the work touches Magic.init, MagicApp, a facade (Auth/Http/Cache/DB/Echo/Event/Gate/Config/Lang/Launch/Log/Pick/MagicRoute/Schema/Session/Storage/Vault/Crypt), a Model, MagicController, a MagicView, MagicFormData, FormRequest, MagicAction, MagicFormObject, Repository, SessionScope, BroadcastListeners, a ServiceProvider, a migration, or the artisan make:* CLI. UI styling is Wind (separate wind-ui skill). Do NOT use for plain Flutter or Wind-only work with no magic import.
- when_to_use
- Use proactively when editing or scaffolding a magic app: Magic.init / a facade / a Model / a MagicController or MagicView / a form (MagicFormData, FormRequest, Validator, MagicFormObject) / a write (MagicAction, RunsActions) / a resource cache (Repository, RepositoryQuery) / a tenant boundary (SessionScope, SessionScoped) / a shared realtime channel (BroadcastListeners, ListensToBroadcasts) / a ServiceProvider / a route or MagicMiddleware / a migration / MagicStateMixin + RxStatus + fetchList / Session flash + old() + trans() / testing with MagicTest + Http.fake/Auth.fake / the artisan make:* CLI / the magic_deeplink, magic_notifications, magic_social_auth, magic_starter, magic_payments, magic_devtools, or magic_sentry plugins. Trigger even when the user does not say the word 'magic'. Do NOT trigger for plain Flutter or Wind-only UI with no package:magic import.
- version
- 0.1.72
<!-- magic 0.0.35 | Skill v0.1.72 (2026-10-28). API surface verified against lib/src. -->
# Magic Framework
Laravel-inspired Flutter framework: IoC container, 18 facades, Eloquent-style ORM, service providers, reactive controllers, and GoRouter-backed routing. This skill makes an agent write code that an experienced magic developer would write: facade-first, IoC-resolved, reactive, and verified against the real API in `lib/src`. All visual styling is handled by Wind (load the `wind-ui` skill for className work); this skill owns architecture, data, navigation, auth, and testing.
The host app already depends on `package:magic/magic.dart`. The accuracy contract for this skill: every API you write must exist in `lib/src`. When unsure of a signature, open the source or the matching `doc/**` page rather than guessing; magic is pre-1.0 and the surface is exact, not approximate.
## 0. Before writing code in this project
Three checks, each pays off across the whole session.
1. **Read `lib/main.dart` and `lib/config/app.dart`.** Note the `providers` list and its ORDER (AppServiceProvider must precede AuthServiceProvider so `setUserFactory` is set before auth restore runs), and whether `configFactories` or `configs` is used.
2. **Scan one existing controller + view pair in `lib/app/`** for the project's idioms: the singleton accessor shape, how views resolve controllers, how forms are wired. Match the surrounding code, do not invent a dialect.
3. **CLI invocation.** Magic ships an `artisan` executable, so every command runs as `dart run magic:artisan <cmd>` from any app that depends on magic (no package-name placeholder, no global activate).
## 1. Core Laws
Hard constraints for every line of magic code.
1. **`await Magic.init()` first.** It must be awaited in `main()` before any facade call and before `runApp()`. Never `.then()`; providers are not booted until the future completes.
2. **Facade-first.** Reach for `Auth`, `Http`, `Config`, `Cache`, `DB`, `Schema`, `Log`, `Event`, `Echo`, `Lang`, `MagicRoute`, `Gate`, `Session`, `Vault`, `Storage`, `Pick`, `Crypt`, `Launch`. Resolve from the container manually (`Magic.make<T>('key')`) only when extending the framework.
3. **Controllers are singletons.** `static X get instance => Magic.findOrPut(X.new);` is the canonical accessor. Views resolve controllers via `Magic.find<T>()` (automatic in `MagicView`), never through constructors.
4. **IoC over `new` for services.** Bind in a provider's `register()`, resolve via the facade or `Magic.make<T>('key')`. Do not scatter `Service()` construction across the app.
5. **Provider discipline.** `register()` is synchronous and is where routes and bindings go. `boot()` is async and may resolve other services; set `Auth.manager.setUserFactory(...)` here.
6. **Reactive state, not setState.** Controllers extend `MagicController` (a `ChangeNotifier`); state flows through `MagicStateMixin` + `RxStatus`. Use `refreshUI()` (guarded `notifyListeners`, and the single seam every controller notification goes through, including validation), `setLoading/setSuccess/setError/setEmpty`, `MagicBuilder` for a section backed by a `ValueListenable`, and `MagicSelector` for a section backed by a plain controller field (it caches its child, so it survives the parent's `setState` and is the tool for a field that changes on every keystroke). `MagicPerfHooks.sink` is a null-by-default static tooling sets to observe those notifications (and repository, query, action, event, cast, timer and broadcast activity); there is no `onRefreshUI`. Local `setState` belongs only to genuine widget-local UI state inside a `MagicStatefulView`.
7. **Typed attribute access.** Models use `get<T>('key')` and `set('key', v)`, never raw `getAttribute`. Declare `fillable`; use `fill(validated, strict: true)` after validation so schema drift throws `MassAssignmentException`.
8. **Context-free navigation and feedback.** `MagicRoute.to/back/replace`, `Magic.snackbar/toast/dialog/confirm/loading`. Never depend on a `BuildContext` for navigation or feedback. Never navigate or fetch inside `build()`.
9. **Validate at the boundary.** `MagicFormData` for forms, `FormRequest` for complex payloads, `Validator` for ad hoc checks. Surface server errors with `handleApiError(response)` (from the `ValidatesRequests` mixin).
10. **Trailing commas, multi-line collections.** Always. Match the project's existing style.
## 2. Bootstrap
```dart
import 'package:magic/magic.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Magic.init(
configFactories: [
() => appConfig, // factories: evaluated AFTER Env.load(), so env() works inside them
() => authConfig,
() => networkConfig,
],
);
runApp(MagicApplication(title: 'My App'));
}
```
Real lifecycle (from `lib/src/foundation/magic.dart`): `Env.load()` then `configFactories` evaluate, then `MagicApp.init` (config merge), then the web URL strategy is applied if `routing.url_strategy == 'path'`, then core bindings, then providers `register()` (sync), then `await boot()` (async), then the router pre-builds, then ready.
Use `configFactories` (not `configs`) whenever a config value reads `Env.get()`: `configs` is evaluated before Env is loaded. `MagicApplication` accepts `title`, `titleSuffix`, `windTheme`, `themeMode`, `locale`, `localizationsDelegates`, `onThemeChanged`, `onInit`, `initialRoute`, and `builder`, which wraps the router like `MaterialApp.builder` so a layer above every page (a floating player) survives each navigation.
## 3. Mental model: Laravel to magic (and where it diverges)
Magic mirrors Laravel's vocabulary; it diverges wherever Dart lacks PHP's runtime reflection or where the target is a Flutter client, not an HTTP server. Internalize the divergences; they fail silently (null, not an exception).
| Laravel | magic | Note |
|---|---|---|
| `Container` autowiring, `__callStatic` facades | string-keyed factory closures + explicit static facade stubs | No reflection, no autowiring; an unregistered key throws at runtime |
| `ServiceProvider::boot()` (sync) | `boot()` is `async` | await it; dropped futures leave a half-booted provider |
| `Router::resource` returns Responses | routes resolve to WIDGETS; middleware runs on NAVIGATION | not an HTTP request cycle |
| controllers = per-request handlers | controllers = reactive `ChangeNotifier` singletons | live for the session, drive UI via `RxStatus` |
| Eloquent lazy load + `with()` eager load | relations cast from nested API Maps, cached on first access | NO lazy load, NO `with()`, NO query-builder relations: if the payload did not nest it, it is null |
| `Gate`/`Policy` server-authoritative | `Gate`/`Policy` run CLIENT-side, advisory only | always re-authorize on the backend |
| server sessions | tokens in `Vault` (secure storage), cache-first restore | `Auth.restore()` on cold start |
| `Encrypter` AES + HMAC/AEAD JSON envelope | AES-256-CBC `iv:ciphertext` (base64), no MAC | not cross-decryptable with Laravel's `Crypt` |
The five assumptions a Laravel developer gets wrong most: (1) the container autowires (it does not, register explicitly); (2) `user.posts` lazy-loads (it does not, embed in the payload); (3) `Gate.allows()` is real security (advisory only); (4) `with()` exists (it does not); (5) `boot()` is sync (it is async). Full mapping with Laravel source citations: `${CLAUDE_SKILL_DIR}/references/bootstrap-lifecycle.md`.
## 4. Facades and the container
### IoC container (the methods an app uses)
| Call | Purpose |
|---|---|
| `Magic.bind('key', () => Svc(), {shared})` | factory binding (new instance per resolve; `shared: true` caches) |
| `Magic.singleton('key', () => Svc())` | lazy shared singleton |
| `Magic.make<T>('key')` | resolve a service (throws if unbound) |
| `Magic.bound('key')` | is the key registered |
| `Magic.put<T>(ctrl)` / `Magic.find<T>()` / `Magic.findOrPut<T>(T.new)` | controller register / resolve / get-or-create |
| `Magic.delete<T>()` / `Magic.isRegistered<T>()` | controller remove / check |
| `Magic.flush()` / `MagicApp.reset()` | clear controllers / full container reset (testing) |
### The 18 facades
`Config` and `Gate` resolve through their managers (no plain IoC key); the rest bind to the key shown.
| Facade | Key | Surface you reach for (all verified in `lib/src/facades/`) |
|---|---|---|
| `Auth` | `auth` | `login(data, user)`, `logout()`, `check()`, `guest` (getter), `user<T>()`, `id()`, `getToken()`, `refreshToken()`, `restore()`, `registerModel<T>(factory)`, `guard([name])`, `stateNotifier`, `manager`, `fake({user})` |
| `Http` | `network` | `get/post/put/delete`, `upload`, RESTful `index/show/store/update/destroy`, `fake([stubs])`, `response([data, code])`, `unfake()`. NO `patch` |
| `Config` | (manager) | `get<T>`, `getOrFail<T>`, `set`, `has`, `all`, `merge`, `prepend`, `push`, `forget`, `flush`, `repository` |
| `Cache` | `cache` | `put(key, value, {ttl})`, `get`, `has`, `forget`, `flush`, `remember<T>(key, ttl, cb)`, `fake()` |
| `DB` | (lazy) | `table(name)` (query builder), `select/statement/insert/update/delete` (raw SQL), `transaction(cb)`, `beginTransaction/commit/rollback` |
| `Schema` | (manager) | `create(table, (b){})`, `table`, `drop`, `dropIfExists`, `hasTable`, `hasColumn`, `getColumns`, `rename` |
| `Log` | `log` | `info/error/warning/debug/notice/critical/alert/emergency`, `log(level, msg)`, `channel(name)`, `fake()` |
| `Event` | (dispatcher) | `dispatch(MagicEvent)`; `listen<T extends MagicEvent>(() => Listener())` (named `T`, register in `register()` not `boot()`); or `EventDispatcher.register(Type, [() => Listener()])` directly |
| `Echo` | `broadcasting` | `channel/private/join`, `listen`, `leave`, `connect/disconnect`, `socketId`, `connectionState`, `onReconnect`, `addInterceptor`, `manager`, `fake()` |
| `MagicRoute` | (router) | `page`, `group`, `layout`, `resource(name, ctrl, {only, except})`, `to`, `toNamed`, `push`, `back({fallback})`, `replace`, `setTitle`, `currentTitle`, `config` |
| `Gate` | (manager) | `define`, `before`, `allows`, `denies`, `allowsAny(list)`, `allowsAll(list)`, `has`, `abilities`, `flush` |
| `Session` | (store) | `flash(map)`, `flashErrors(map)`, `old(field, [fallback])`, `oldRaw`, `error(field)`, `errors(field)`, `hasError`, `hasFlash`, `tick()` |
| `Lang` | (translator) | `get(key, [replace])`, `has`, `current`, `isLoaded`, `supportedLocales`, `setLocale`, `detectLocale`, `detectAndSetLocale`, `setSupportedLocales`, `addListener/removeListener`, `delegate` |
| `Vault` | `vault` | `put(key, value)`, `get`, `delete`, `flush`, `fake([initial])` |
| `Storage` | (manager) | `disk([name])`, `put`, `get`, `getFile`, `exists`, `delete`, `url`, `download`, `setManager`, `flush` |
| `Pick` | (static) | `image`, `images`, `camera`, `media`, `video`, `recordVideo`, `file`, `files`, `directory`, `saveFile` |
| `Crypt` | `encrypter` | `encrypt`, `decrypt`, `encryptWithDeviceKey`, `decryptWithDeviceKey`, `hasDeviceKey`, `generateDeviceKey`, `clearDeviceKey` |
| `Launch` | `launch` | `url(u, {mode})`, `email`, `phone`, `sms`, `canLaunch` |
View on GitHub