Phases
The ten spec phases and the thirteen-step build order, as a delivery plan: what each phase contains, what "done" means for it, and what it unblocks.
Status: spec only — nothing is in progress and no phase has started. Every phase below is not implemented. Reviewed 2026-08-26.
Two orderings, and which one wins
00-build-spec.md contains both a phase list (the DSL surface, grouped by capability) and a build order (thirteen steps, the order to construct them in). They disagree in one place, on purpose:
- Test DSL + Minitest wrapper + parallel runner (build this early — TDD the rest)
Phase 9 is the testing surface; build-order step 5 is where it actually gets built. Where the two disagree, build order wins. Phases describe what a construct belongs to; build order describes when it is written.
| Build step |
Phase it delivers |
Why here |
1. Rack skeleton + App.define boot + Sequel connection |
1 |
nothing can be declared before there is a boot to declare into |
2. model DSL → Sequel, migrations, the option tables + magik describe |
1 |
every later construct references a model, and the option tables are what the coercer, the guardrails, the docs anchors and describe all read. Retrofitting an option-table representation onto constructs already implemented is the same shape of migration as retrofitting policy: |
3. screen/component/action → HTML+htmx compiler, plus policy and layout |
2 |
the first thing a user can see — and a screen and an action are the first two surfaces needing authorization and the first thing needing a shell |
| 4. Router + dev server + hot reload |
2 |
closes the edit loop |
| 5. Test DSL + Minitest wrapper + parallel runner |
9 |
early, so steps 6–13 are test-driven |
6. Realtime over LISTEN/NOTIFY |
3 |
first opt-in cost; needs screens to attach to |
| 7. Jobs (Postgres-backed queue) |
4 |
needed by notifications, webhooks and billing |
| 8. Media — derivatives, transcoding seam, direct upload, signed delivery |
4b |
derivative generation is job work, and signed delivery is blocked on policy |
9. Ledger + audited/immutable_after |
5 |
the fintech claim, and the flagship guardrail |
10. Auth + billing + admin_panel scaffolds |
7 |
batteries; all three sit on models, screens, actions and jobs |
| 11. API/webhook DSL |
6 |
independent of the UI path, so it can land after it |
| 12. i18n/PWA/notifications |
8 |
cross-cutting polish over a working app |
13. Domain module system + boot enforcement + magik check |
12 |
enforces boundaries across everything that exists by now |
The phases
Each row's "Done when" is the gate for calling the phase finished. Every one of them is a statement about tests and guardrails, never about a demo working once.
Phase 1 — Foundation
|
|
| Contains |
App.define, model, migrate, :money, :file and :duration types, computed, UUIDv7 ids, tenant_id injection, the option tables, magik new/generate/console/server/describe |
| Done when |
a generated app boots, a model round-trips through Postgres, a migration applies and reverses, :money refuses a float, magik describe model --json lists every option the coercer accepts, and every model carries tenant_id and a UUIDv7 id — each covered by a test |
| Unblocks |
everything. No other phase can start. |
| Guardrails landed |
MAGIK_MODEL_FLOAT_MONEY, MAGIK_MODEL_NO_TENANT, MAGIK_SCHEMA_IRREVERSIBLE, MAGIK_BOOT_REDEFINED |
Also carries (10-saas-coverage.md) |
searchable declarations and Model.search over the existing search seam; soft delete / archive / restore as a model annotation; an environment block in App.define selecting seams per environment, with magik doctor --env; liveness and readiness endpoints and magik migrate as a deploy gate |
Phase 2 — Rendering & actions
|
|
| Contains |
component, screen, action, policy, layout, the component kit including the layout primitives, the theme system, the router, hot reload |
| Done when |
the spec's headline is true — a model + screen + action under 30 lines renders a working CRUD page, navigation included, with zero HTML, JS or CSS written by hand; the generated htmx endpoint provably matches the action that answers it; a cross-tenant actor is denied by both the screen and the action; and the generated app renders correctly at 375px |
| Unblocks |
realtime (something to attach live to), auth screens, admin, flows — and every later surface, each of which names a policy verb rather than writing a check |
| Guardrails landed |
MAGIK_ROUTER_PATH_CONFLICT, MAGIK_RENDER_SCREEN_STATEFUL, MAGIK_ACTION_STATEFUL, MAGIK_RENDER_TIMESTAMP_NO_ZONE, MAGIK_POLICY_UNDECLARED, MAGIK_POLICY_IO, MAGIK_POLICY_UNKNOWN_VERB, MAGIK_POLICY_NO_DEFAULT, MAGIK_POLICY_NULL_PASSES, MAGIK_LAYOUT_MISSING, MAGIK_LAYOUT_UNKNOWN_SCREEN |
Also carries (10-saas-coverage.md) |
data_table fully specified — cursor pagination, sorting, filters with URL state, bulk selection, column control, export, narrow-screen mode; chart specified as server-rendered SVG themed from tokens; responsiveness as a kit property with a 375px render test; empty, loading and error states on every collection component; spacing, type, elevation, motion and icon tokens; accessibility as a component-contract element; screen-level search and filter; fragment caching on state; CSRF, CSP, HSTS, frame options and cookie flags as defaults; a form honeypot and timing check on by default; per-IP/per-actor/per-action throttling on action; output escaping stated as a guarantee with raw as the single hatch |
Phase 9 — Testing (built fifth)
|
|
| Contains |
the test DSL over Minitest, inferred factories, generated invariant tests, the parallel runner, magik test --watch/--changed |
| Done when |
the framework's own suite runs on the runner, isolation holds under parallelism, and --report=timing exists to measure the 1,000-under-10s target (../architecture/04-testing-strategy.md) |
| Unblocks |
every phase after it, honestly. Steps 6–12 are written test-first against this. |
| Guardrails landed |
none — this phase is how the others prove theirs |
Also carries (10-saas-coverage.md) |
assert_queries(n) for N+1 absence; email content assertions beyond assert_notified; a generated authorization test per policy verb including a cross-tenant denial; render_screen … at: :mobile |
Phase 3 — Realtime (opt-in)
|
|
| Contains |
live, channel, broadcast, presence, Postgres LISTEN/NOTIFY |
| Done when |
a screen with one live line updates from a write in another process, a screen without one opens no socket and issues no realtime query, and the Redis backend passes the same conformance suite (04-swap-points.md) |
| Unblocks |
live dashboards, presence, the realtime half of notifications |
| Proves |
axiom 5 — cost is opt-in — measurably, not rhetorically |
Also carries (10-saas-coverage.md) |
a channel evaluates the same policy verb as the request path — there is no second door to the data |
Phase 4 — Jobs & async
|
|
| Contains |
job, retries, schedule, the Postgres transactional queue, magik worker |
| Done when |
enqueue inside a rolled-back transaction enqueues nothing, retries back off, cron fires once across N workers, and assert_enqueued works in the test DSL |
| Unblocks |
notifications, outbound webhooks, billing dunning, anything slow |
Also carries (10-saas-coverage.md) |
CSV and spreadsheet import and export as job factories with a progress surface |
|
|
| Contains |
attachment on a model, direct-to-storage presigned upload, magic-byte content sniffing, size and dimension limits, image derivatives and srcset, format conversion, EXIF stripping, signed expiring URLs for private files, a media-processing seam for video and audio, blob lifecycle and orphan cleanup |
| Why its own phase |
it spans four others — the field type is phase 1, the upload component is phase 2, derivative generation and transcoding are phase 4 jobs, and signed delivery is blocked on policy. A capability whose value arrives only when all four have landed is a phase, not a line item; otherwise each phase ships a quarter of it and the app still cannot store a photo (10-saas-coverage.md) |
| Done when |
an upload never passes through a request server; a .png that is really an .svg is rejected on magic bytes; an attachment with no size and content-type bounds fails the boot; a private file's URL is issued only after its record's policy verb passes; deleting a record deletes its blobs; and the video path is a wrapped service, not a transcoder Magik wrote |
| Unblocks |
ecommerce and marketplaces — two of the four verticals the spec's mission sentence names |
| Guardrails landed |
MAGIK_MODEL_UNCONSTRAINED_UPLOAD |
| Built at |
build-order step 8 — after jobs, after policy |
Phase 5 — Money & compliance
|
|
| Contains |
ledger, audited, immutable_after:, idempotent_by:, flow |
| Done when |
an unbalanced entry fails the boot with MAGIK_LEDGER_UNBALANCED and a file:line, posted entries refuse mutation, a retried idempotent action posts once, and a flow resumes across two different app servers |
| Unblocks |
billing, and the fintech claim in the spec's success criteria |
| Guardrails landed |
MAGIK_LEDGER_UNBALANCED, MAGIK_LEDGER_ENTRY_MUTATED, MAGIK_MODEL_IMMUTABLE_VIOLATION, MAGIK_ACTION_IDEMPOTENCY_REQUIRED, MAGIK_MODEL_FORBIDDEN_FIELD |
Also carries (10-saas-coverage.md) |
basis-point rate arithmetic as :money's sibling, for tax and fees — the reference app already needed it; GDPR subject export and erasure, including the interaction with audited and append-only ledgers; retention policies and scheduled purge; a staff-versus-customer dimension on the audit trail |
Phase 6 — API & integration
|
|
| Contains |
api resources, pagination/filter/sort, webhook :incoming/:outgoing, bearer/API-key/JWT auth, plan-based rate limiting |
| Done when |
a resource declaration serves all five verbs with pagination and filtering, an unsigned inbound webhook fails the boot, and an outbound webhook retries through the job queue |
| Unblocks |
mobile clients, third-party integrations, Stripe's inbound events for billing |
| Guardrails landed |
MAGIK_WEBHOOK_UNVERIFIED |
Also carries (10-saas-coverage.md) |
API deprecation and sunset headers on a version; a webhook delivery log with replay and a customer-facing endpoint surface |
Phase 7 — Auth, billing, admin
|
|
| Contains |
auth (Rodauth-backed), billing provider:, admin_panel, tenant_by |
| Done when |
a generated app has working signup/login/reset/2FA, a subscription lifecycle drives plan state through the ledger, and admin_panel :Model produces CRUD with filters and search |
| Unblocks |
an app that can take money and be operated — the practical definition of shippable |
Also carries (10-saas-coverage.md) |
teams, memberships, seats and invitations, generated the way auth generates accounts; support impersonation with a mandatory audit record and a visible banner; admin_panel depth benchmarked against Avo — detail views, association browsing, forms, bulk actions with arguments, typed filters, saved views, global search, admin dashboard cards; a bot-challenge seam, off by default; enumeration-resistant login and reset by default; disposable-domain policy on signup; login throttling and lockout; feature flags as a policy factory |
Phase 8 — i18n, PWA, notifications
|
|
| Contains |
locales, translatable: fields, timezone-safe :timestamp, pwa, notification |
| Done when |
a missing locale key fails magik check, an app installs as a PWA with no offline caching, and one notification reaches email, in-app and push through the job queue |
| Unblocks |
non-English products and mobile-shaped installs |
| Guardrails landed |
MAGIK_I18N_MISSING_KEY, MAGIK_PWA_OFFLINE_UNSUPPORTED |
Also carries (10-saas-coverage.md) |
email production — magik mail preview rendering to a file, CSS inlining, a generated plain-text alternative, mail layouts distinct from screen layouts, a framework-owned suppression list consulted before delivery, unsubscribe tokens, and inbound bounce/complaint events over the webhook DSL; an in-app notification centre; currency, number, address and pluralisation formatting and RTL; per-screen meta, canonical and Open Graph tags; PDF and print rendering |
Spec item 12 — Domains & magik check
|
|
| Contains |
domain declarations, depends_on/exposes/publishes_events, boot-time boundary enforcement, the magik check linter and --scale |
| Done when |
a cross-domain model reference fails the boot naming both domains and the reference site, an undeclared dependency fails, cycles fail, and magik check --scale reports unscoped query sites as warnings with --json |
| Unblocks |
large apps — the point at which multiple teams can work in one codebase without a review culture holding the boundary |
| Guardrails landed |
MAGIK_DOMAIN_BOUNDARY, MAGIK_SCALE_UNSCOPED_QUERY |
Cross-phase rules
| Rule |
Detail |
| A phase is not done without its guardrails |
the guardrail is the product; shipping the construct without its boot check ships the half that does not differentiate (03-guardrails.md). |
| A phase is not done without its swap conformance |
any phase introducing a backend ships the seam's conformance suite green on every listed backend (04-swap-points.md). |
| Every phase ships YARD |
public methods are documented as they land, never in a later pass (../architecture/00-conventions.md). |
| Version means nothing yet |
0.0.1 is a name reservation. No phase completion implies a release number until the release policy says so. |
| The plan is a plan |
this page is re-derived from the spec, not from progress. When work starts, status moves here and in CHANGELOG.md — never into prose claiming behaviour. |