Limits
What Magik will never do, why, and how each refusal is made loud rather than silent.
Status: spec only — these are permanent design decisions, not a backlog. None of the refusal mechanisms described here is implemented. Reviewed 2026-08-26.
The rule about limits
From 00-build-spec.md's success criteria:
Explicit documented limits: no offline, no heavy client-side compute — do not silently fail on these, refuse/clarify in docs.
A limit that degrades quietly is worse than no limit: the app appears to work, ships, and fails on a customer's laptop. So every limit on this page gets one of two treatments, and the doc says which:
| Treatment | Meaning |
|---|---|
| Refused | there is a boot or check failure with a MAGIK_* code. The app does not start. |
| Documented | no mechanism can detect it, so the docs say no plainly and the framework ships nothing that would help. |
Nothing gets a third treatment. There is no "works but is discouraged".
The list
Two kinds, and the distinction matters: an architectural limit is a property of how Magik is built and cannot change without building a different framework. A product-scope limit is a thing Magik could technically do and deliberately will not, because doing it well is a second product.
Architectural
| Limit | Treatment | Code |
|---|---|---|
| No offline | Refused | MAGIK_PWA_OFFLINE_UNSUPPORTED |
| No heavy client-side compute | Documented | — |
| No SPA framework | Documented, with one refused mechanism | MAGIK_RENDER_CLIENT_STATE |
| No ActiveRecord | Documented | — |
| No RSpec | Documented | — |
Product scope
Found by the coverage audit (10-saas-coverage.md), which asked what a SaaS needs across its whole life and then asked which answers Magik should refuse to be. All six are Documented — no mechanism can detect them, so the docs say no plainly and the framework ships nothing that would help.
| Limit | Why it is a second product |
|---|---|
| No email marketing or campaigns | a different deliverability model, and mixing it with transactional mail damages both |
| No native mobile application | pwa makes an app installable; there is no native shell and channel :push means web push |
| No CMS or marketing-site authoring | an editorial workflow and a page builder are not an application framework |
| No ad-hoc BI or report builder | stat and chart render declared queries; a semantic layer is an analytics product |
| No workflow or BPM engine | flow is a linear wizard, and stretching it produces a second, worse execution model beside job |
| No multi-region or data residency | one Postgres, one region, tenancy by column |
No offline
The server is the single source of truth for every byte of state, always. There is no client-side data store, no service-worker cache of application data, no queue of pending mutations, and no reconciliation on reconnect.
| Why | |
|---|---|
| Offline is a distributed systems problem, not a caching feature | it needs conflict resolution, causal ordering and a client-side authorization story. Each is a product. |
| Two sources of truth is two authorization systems | which is the failure that killed Meteor-shaped frameworks (01-thesis.md). |
| The 99% of SaaS the spec targets is online | dashboards, CRUD, fintech, ecommerce, marketplaces — all of them require the server to be reachable to be correct. |
The pwa construct exists and makes an app installable — an icon, a name, a standalone window. It does not make it work offline. Declaring an offline strategy fails at boot with MAGIK_PWA_OFFLINE_UNSUPPORTED rather than emitting a service worker that pretends (02-dsl-surface.md).
What a Magik app does when the network drops: htmx requests fail, and the framework shows a connection-lost state. That is the honest behaviour, and it is the designed one.
No heavy client-side compute
Canvas editors, games, video processing, 3D, real-time drawing and anything else whose work must happen at 60fps in the browser are out of scope. The client story is ~14kb of htmx swapping server-rendered fragments; that is the whole budget.
| Why | |
|---|---|
| The rendering model is server-authored HTML | a framework that compiles declarations to markup cannot also be a client runtime, and pretending otherwise produces two frameworks in one repo. |
| It is not the target | the spec's target is SaaS: CRUD apps, dashboards, fintech, ecommerce, marketplaces. |
| No mechanism can detect it | an app can always load its own JavaScript. Magik does not stop it and does not help it. |
Where the line actually is
An ambiguous permanent limit is worse than a stricter one clearly stated, and this one has been read too broadly. The test is whether client-side computation is the product's core value, not whether any JavaScript runs.
| Out of scope — the compute is the product | Fine, and sometimes necessary — a small bounded widget |
|---|---|
| a canvas or vector editor, a game, an in-browser video or audio editor, a spreadsheet engine, a 3D viewer, a live-collaborative rich text surface | an avatar crop-and-scale before upload, a date picker, a colour picker, a masked input, a drag-to-reorder handle, upload progress on a direct-to-storage PUT |
| the app is unusable without it, it holds its own state model, and it owns a rendering path | it operates on one input, it hands its result to the server, it holds nothing across requests, and the page works without it |
The second column is not an exception to the rule — it is the rule read correctly. 08-component-overrides.md already permits attaching your own JavaScript behaviour to server-rendered markup, and the framework itself ships one such asset: a small, versioned, non-bundled upload script on the same terms as htmx. No toolchain, no bundler, no node_modules, and the app author still writes no JavaScript.
Documented, not refused. If an app needs a canvas editor, the honest answer is that the editor is a separate front-end asset the app serves and Magik knows nothing about — and that Magik is the wrong framework if the editor is the product.
No SPA framework
No React, no Vue, no Ember, no client router, no hydration step, ever. UI is server-rendered HTML plus hx-* attributes compiled from component and screen declarations.
| Why | |
|---|---|
| One rendering path | two means every feature is built twice and every authorization check has two homes. |
| No build step | no bundler, no node_modules, no toolchain between an edit and a reload — a first-order property for the audience and for the agent writing the code (07-ai-first.md). |
| htmx is sufficient for the target | a swap of a server-rendered fragment covers the interactivity a SaaS screen needs. |
One piece of it is refused: a screen holding client-owned state that the server does not know about — the DSL provides no way to declare it, and a screen attempting to hold state across requests fails the stateless guardrail (03-guardrails.md). Loading your own JavaScript for a date picker is fine and unpoliced; building your application's state model in the browser is not something the grammar can express.
No ActiveRecord
Sequel, explicit queries, no lazy loading. The model DSL wraps Sequel and nothing else.
| Why | |
|---|---|
| Implicit lazy loading is an N+1 factory | the query an app runs should be visible in the code that runs it. |
| Callback chains are untraceable | a framework whose product is boot-time guardrails cannot have a mutation path nobody can statically follow. |
| Two ORMs is two migration stories | and two sets of generated SQL for an agent to learn. |
Documented, not refused. Nothing stops an app from adding the activerecord gem to its own Gemfile. Doing so means the tenant scoping, the :money type, the audit trail, the ledger guardrails and every generated test stop applying to those models — the framework's guarantees are properties of the model DSL, not of the database.
No RSpec
Minitest and Rake, in the framework and in generated apps. The test DSL compiles to Minitest (../architecture/04-testing-strategy.md).
| Why | |
|---|---|
| Boot cost is a feature of the test loop | the target is 1,000 tests under 10 seconds; a heavier runner spends the budget before the first assertion. |
| A simple object model | a Minitest test is a method on a class. Running those in parallel worker threads with transactional rollback per test is tractable; against a DSL-heavy runner with its own lifecycle it is not. |
| One test grammar | test :Name do it "…" end compiles down to Minitest, so assert_* is always available underneath. There is no second way to write a test. |
Documented, not refused. An app that adds rspec to its own Gemfile can run it. What it will not get is magik test's parallel runner, the transactional rollback, the inferred factories or the generated invariant tests — those are built on the Minitest layer.
No email marketing or campaigns
notification is transactional email. Bulk campaigns, list management, drip sequences, segmentation and engagement tracking are not in scope and no construct will be added for them.
| Why | |
|---|---|
| Sharing a sending reputation is how a password reset lands in spam | transactional and marketing mail want different domains, different IPs and different warm-up. A framework that made them one construct would be making that mistake for every app that used it |
| The compliance surface is different | consent records, preference centres, jurisdiction-specific opt-in rules. Each is a product |
| The workflow is a product | a campaign editor, a schedule, an audience builder and a report are a marketing tool, not a framework feature |
The app's job is to keep the list in sync with a campaign provider. Suppression, unsubscribe tokens and bounce handling are in scope, because transactional mail needs them too.
No native mobile application
pwa makes an app installable — an icon, a name, a standalone window. There is no native shell, no App Store artifact, no React Native or Flutter bridge, and notification channel :push means web push, not APNs or FCM through a native SDK.
| Why | |
|---|---|
| A native shell is a second rendering target | with its own store review, its own release cadence, its own crash reporting and its own version skew against the server |
| Web push is the honest subset | it works where the platform supports it and says so where it does not, rather than implying a native delivery path the framework does not have |
If the product needs a native app, the API in 02-dsl-surface.md is what it talks to, and the native client is somebody else's codebase.
No CMS or marketing-site authoring
Magik renders application screens for known actors. There is no page builder, no editorial workflow, no content model for marketing pages, and no blog engine.
| Why | |
|---|---|
| The audiences are different | an application screen is authored by a developer in a declaration; a marketing page is authored by a marketer in a browser. Serving both means an admin UI for content, a preview pipeline and a publishing state machine |
| A static site is better at it | and it can sit in front of the app at the same domain |
Per-screen SEO metadata — title, description, canonical, Open Graph — is not covered by this limit and should exist: server-rendered HTML is indexable, so it costs a declaration.
No ad-hoc BI or report builder
stat and chart render queries the app declared. There is no pivot table, no drill-down, no saved-query builder, no semantic layer and no end-user report designer.
| Why | |
|---|---|
| It is an analytics product | a query builder safe enough to expose to a customer needs its own permission model, its own cost controls and its own query planner |
| The escape is an export | (a) export to a warehouse or a spreadsheet, and let a tool built for it do the analysis |
No workflow or BPM engine
flow is a linear, resumable wizard — onboarding, KYC, checkout. It is not a workflow engine: no parallel branches, no human task assignment, no compensating transactions, no visual designer, no long-running process state beyond the step token.
| Why | |
|---|---|
| It would be a second execution model | Magik already has job for durable work. A BPM engine beside it means two answers to "where does this run", which is exactly the ambiguity axiom 1 forbids (07-ai-first.md) |
Stretching flow is worse than refusing |
a wizard that grew branches and compensations is a state machine nobody declared |
No multi-region or data residency
One Postgres, one region, tenancy by column. Sharding, regional pinning, cross-region replication and per-tenant data residency are a deployment topology Magik does not model.
| Why | |
|---|---|
| It is a topology, not a feature | it changes the connection story, the migration story, the job story and the realtime story at once |
| The option is kept open, not delivered | UUIDv7 keys and magik check --scale exist so a future shard is possible. They are not a shard (../ops/README.md) |
Saying no
The bar for adding to this page, so it stays a design document and not a wish list:
| Test | Question |
|---|---|
| Permanent | is this a "no", or a "not in phase 1"? A phase belongs in 06-phases.md, not here. |
| Structural | does supporting it require a second rendering path, a second source of truth, or a second authorization system? |
| Loud | can it be refused with a code? If not, does the doc say no in a place the reader will actually hit? |
| Honest about the alternative | does the page name what the user should do instead, including "use a different framework"? |