Swap points

Every opinionated default in Magik, and the config-level swap that replaces it without touching app code.

Status: spec only — no seam is implemented, no default backend exists, and no alternative has been proven. Every row below is planned. Reviewed 2026-08-26.

The rule

Swap points required for every "opinionated default." DB engine, cache backend, job backend, search backend, realtime backend must be config-switchable without app code changes. (Lesson from Meteor's death: magic with no escape hatch = eventual rewrite.) — 00-build-spec.md

This is the load-bearing lesson of the whole design (01-thesis.md). A framework that is opinionated and inescapable is a framework its users eventually rewrite away from, all at once, at the worst possible moment.

A swap ships proven, not promised. From the spec's success criteria: every "opinionated default" has a working, tested config-level swap (proven before merge, not promised). Concretely:

A backend may be listed as available only when Evidence
it passes the seam's conformance suite the same test file runs against every backend for that seam, green
CI runs that suite against it on every push a job in the workflow, not a manual step
an example app boots on it unchanged zero app-code diff between backends
its limits are documented what it does not support, on this page

Until all four hold, the backend is a candidate and this page says so. A candidate never appears in a table without that word.

How a swap is expressed

One line in App.define, and nothing else in the app changes.

App.define :Shop do
  database url: ENV.fetch("DATABASE_URL")

  use :cache,    :redis,    url: ENV.fetch("REDIS_URL")
  use :jobs,     :postgres
  use :realtime, :redis,    url: ENV.fetch("REDIS_URL")
  use :search,   :postgres
end
Rule Detail
One namer use is the only place a backend product is named. App code says cache.fetch, never Redis.current.
Boot resolution seams resolve before declarations load. An unknown or unshipped backend fails with MAGIK_CONFIG_UNKNOWN_BACKEND (03-guardrails.md).
No call-site choice there is no per-call override. Two backends live in one app only when the seam is explicitly multi-instance (caches with different TTL policies), never as a fallback chain.
Narrow contracts a seam's interface is the smallest thing every backend can honour. A capability only one backend has does not enter the interface — it is configuration on that backend.

The seams

The five the spec names explicitly, plus seven the same rule catches by implication — each a backend an app cannot avoid needing, and each therefore a seam whether or not the sentence in item 11 lists it.

Seam Default Why this default Alternatives Status
Database Postgres via Sequel UUIDv7, LISTEN/NOTIFY, transactional job queue and full-text search all come from one dependency MySQL/MariaDB, CockroachDB — candidates not implemented
Cache in-process memory zero infrastructure for a small app; cost is opt-in Redis, Memcached — candidates not implemented
Jobs Postgres queue, Que-style transactional enqueue: a rolled-back transaction enqueues nothing, which no external broker can offer Kafka (named in the spec), Redis/Sidekiq — candidates not implemented
Search Postgres full-text + pgvector one datastore until scale forces otherwise Elasticsearch, Meilisearch, Typesense — candidates not implemented
Realtime Postgres LISTEN/NOTIFY the database already knows what changed Redis pub/sub (named in the spec), NATS — candidates not implemented
Storage Shrine, local disk uploads work before an S3 account exists S3 and compatibles via Shrine — candidate not implemented
Billing Stripe the widest coverage of the plan shapes billing declares Paddle (named in the spec) — candidate not implemented
Mail transport SMTP the one transport every provider speaks Postmark, SES, Resend — candidates not implemented
Throttle store Postgres rate limits are counters, and decision 9 forbids a counter living in a process — so this is a seam by construction. ../../wiki/API-And-Webhooks.md already names it; this page did not Redis — candidate not implemented
Bot challenge none — off by default a CAPTCHA has a real accessibility and privacy cost, so imposing one on every app is the wrong opinion. The vendor space is volatile on both price and policy, which is exactly what item 11 exists to survive Turnstile, hCaptcha, reCAPTCHA — candidates not implemented
JSON codec ruby/json from the standard library the fast path is engine-dependent: ruby/json selects a pure-Ruby generator under TruffleRuby, and the C-extension alternatives do not name TruffleRuby as a supported platform. A dependency that is a C extension, or that reaches for a runtime primitive, is a seam until a probe says otherwise on the production engine (../architecture/10-performance-defaults.md) use :json, :oj — candidate, and only where a probe shows it loads and wins on the production engine not implemented
Media processing a media service transcoding, packaging and adaptive delivery are a specialist product. A framework that builds one has become a media company (10-saas-coverage.md) Mux, Cloudflare Stream, Bunny — candidates; ffmpeg in a job for teams that want no vendor, at the cost of a dedicated queue, a machine shape and the input-validation burden not implemented

The UI seam

The component kit is an opinionated default too, and item 11's rule does not stop at infrastructure. It is a declaration-level swap rather than a use line, so it has its own page — the four-rung override ladder, the component contract verified at boot, and the resolution order — 08-component-overrides.md.

Seam Default Swapped by Status
Component kit the built-in kit (button, form, field, data_table, modal, toast, card, list, grid, tabs, stat, chart, sidebar, topbar, nav_item, breadcrumbs, account_menu, dashboard_grid) tokens → extends: → a shadowing component in app/components/ → kit :name for a house library not implemented

Auth is deliberately not a swap point: Rodauth is a structural dependency of the auth DSL, not a backend behind an interface. Saying so is more honest than pretending to a seam nobody could implement against.

Authorization is not a swap point either, for a stronger reason: a second authorization backend is a second authorization system, which is the failure this whole design is organised against (01-thesis.md). policy (02-dsl-surface.md) is one evaluator with no alternative — the same status as tenancy and the primary key strategy, and architecture decision 13 states it as a decision rather than as a preference.

What a swap does not buy

Escape hatches that promise more than they deliver are the failure mode being avoided, so the limits are part of the design:

Limit Detail
Semantics are not identical a Kafka job backend cannot offer transactional enqueue. Choosing it means accepting at-least-once delivery with app-visible consequences, and the seam's docs must say so at the point of choosing.
The cheapest rung can be the one that breaks the guarantee a second Postgres dedicated to jobs loses transactional enqueue too. It is the obvious first move when the job table's dead-tuple churn hurts, and it looks like staying on the default — but a cross-database enqueue cannot join the app's transaction any more than a Redis LPUSH can. Recovering the guarantee means the outbox pattern, which is machinery rather than configuration (../architecture/11-jobs-backend.md).
Data does not migrate itself swapping the cache backend is a restart; swapping the database is a data migration Magik does not perform.
The narrow contract is the contract code written against a backend-specific feature has left the seam and will not survive a swap. That is a supported choice, not a supported portable choice.
Candidates are not fallbacks an unshipped backend fails at boot rather than degrading to the default. Silent degradation is the thing guardrails exist to prevent (05-limits.md).

Adding a seam or a backend

# Step
1 Write the conformance suite first — the behaviour every backend for this seam must exhibit, including failure modes.
2 Implement the interface under lib/magik/<subsystem>/backends/<name>.rb (../architecture/01-module-map.md).
3 Register it so use :<seam>, :<name> resolves, and so an unknown name raises with the list of known ones.
4 Run the suite against every backend for the seam in CI, not just the new one.
5 Boot an example app on it with a zero-line app diff.
6 Add the row here, move it out of "candidate", and record it in CHANGELOG.md.

The loop, with commands: ../architecture/05-adding-a-feature.md.