Research synthesis
Mesh research synthesis
This synthesis has no review file of its own; the seven documents it summarises were each reviewed (see the index).
Date: 2026-10-01. Author: team lead (mesh-d6), from seven researcher documents that were each
fact-checked against source by an independent reviewer.
This document answers the two questions asked:
- Step 1. What Ash offers, how it is built, where it can be extended, which packages matter,
and where it is strong and weak — read as input for designing Mesh. - Step 2. Which tools Mesh should stand on, and for each one whether it is hardcoded in the
core or implemented as an adapter. This part is a recommendation for you to decide. - Step 3. A proposed architecture: goals, layers, the build-time and run-time workflows, and
the extension points. Added on request; not yet reviewed. No implementation plan is included.
Facts below come from the research documents. Design advice is mine and is marked as such
(“Recommendation”). Where a claim rests on thin evidence, the text says so.
How to read the evidence
| # | Document | Words | Reviewer verdict |
|---|---|---|---|
| 01 | 01-ash-features.md — feature inventory (ash 3.33.11) |
10,575 | Accept with fixes |
| 02 | 02-ash-dsl-and-extensions.md — Spark, extensions, Igniter |
16,202 | Accept |
| 03 | 03-ash-runtime-internals.md — lifecycle, data layer, policies |
16,049 | Accept with fixes |
| 04 | 04-ash-ecosystem-packages.md — top 20 packages |
16,756 | Accept |
| 05 | 05-ash-strengths-weaknesses.md — 183 verified quotes |
12,509 | Accept |
| 06 | 06-ts-prior-art.md — TypeScript projects in the same space |
14,079 | Accept with fixes |
| 07 | 07-ts-foundation-candidates.md — foundation tools |
16,255 | Accept with fixes |
Each review ends with a section ### Residual errors (for readers) in
notes/team-lead/reviews/<ref>.md. Those lists are short and were applied to the documents in a
cleanup pass. On fresh citation samples in the final checks, between 56% and 94% of citations
supported the claim at the exact line cited, and 90% to 97% carried a true fact once “right fact,
wrong line” is counted. Treat line numbers as approximate and facts as checked.
The “Implications for Mesh” section at the end of each document is the researcher’s opinion and
was not fact-checked. I used those sections as input, not as findings.
Step 1 — Ash
1. What Ash offers
Ash is a framework where one module per resource declares the data, the operations and the rules,
and the framework derives everything else: persistence, authorization, APIs, clients, admin UI.
The resource surface (document 01):
| Area | What Ash has | In the Mesh plan? |
|---|---|---|
| Resource DSL | 14 sections: attributes, relationships, actions, code_interface, resource, identities, changes, preparations, validations, pipelines, aggregates, calculations, multitenancy, temporal |
5 of them appear in the plan’s example |
| Domain DSL | 4 sections: domain, resources, execution, authorization |
Named, not detailed |
| Types | 31 built-in types, plus arrays, NewType wrappers, embedded resources, enums |
Types and constraints named; NewType and embedded resources are not |
| Relationships | belongs_to, has_one, has_many, many_to_many, join resources, manual relationships, manage_relationship |
The four kinds named; the rest is not |
| Actions | 5 types (create, read, update, destroy, generic); arguments, accept, primary actions, upserts, bulk actions, atomic updates, manual actions, soft destroy, pagination, hooks, transactions |
Types, changes, validations, hooks and transactions named; upserts, bulk, atomic, manual, pagination, preparations are not |
| Built-in behaviour | 24 changes, 24 validations, 4 preparations, 22 policy checks | Not enumerated |
| Expressions | 39 functions, 15 operators, 5 templates (^actor, ^tenant, ^arg, ^context, ^ref) |
“Translatable arrow functions”; no function list |
| Policies | A separate extension: policies and field_policies; bypasses, policy groups, three access types (:strict, :filter, :runtime), Ash.can?; no matching policy means forbidden |
policies block named; field policies, bypass, access types, can? are not |
| Multitenancy | 2 strategies (:context, :attribute), 4 per-action overrides |
Deferred, open question |
| Errors | 4 classes, 91 leaf error modules (built on Splode) | “Errors name the fix”; no class hierarchy |
| Tooling | 25 mix tasks, 9 generators, formatter, cheat sheets, 18 telemetry events, usage-rules.md plus 14 topic files for coding agents |
build, watch, inspect, a skill file and an MCP server |
Honest measure of density: the official tutorial resource uses 3 of the 14 sections, and a dense
real-world resource (Tunez.Music.Album, 146 lines) uses 8. The plan’s own example uses 5. “One
file declares everything” is true in Ash, but a typical resource uses about half the vocabulary.
2. Architecture
Ash has two halves that share almost nothing: a compile-time DSL engine and a run-time action
engine.
2.1 Compile time (document 02)
- Spark is a generic DSL toolkit; Ash is one user of it. Every Ash DSL is a list of section
and entity declarations with typed option schemas. - A DSL compiles to a plain map keyed by section path. There are no run-time DSL objects; the
map is baked into functions on the module. Introspection reads that map. - Three hook kinds run in order: transformers (may rewrite the whole map), persisters
(cache derived values), verifiers (read-only checks, run after the module compiles).
Ash.Resourceregisters 20, 8 and 27 of them. - Two behaviours matter for Mesh:
- Transformer errors abort the build. Verifier errors only print warnings.
- Ordering between transformers uses
before?/after?against module names. A contradictory
pair is dropped silently, and cycles are broken silently. Ash has live casualties: AshArchival
orders itself against two modules that no longer exist, and two Ash core transformers
contradict each other.
2.2 Run time (document 03)
- The write lifecycle is a nested onion:
around_transaction→before_transaction→
[transaction opens] →around_action→before_action→ data layer →after_action→
post-action authorization → [transaction closes] →after_transaction. Hooks are anonymous
functions stored on the changeset. - Changes and validations run when the changeset is built, not when the action runs. The
request object and the command object are the same thing. - “Atomic” is an expression-rewriting protocol. A change returns expressions, and the data
layer folds them into one UPDATE. A single-record update runschange/3at build time, then
rebuilds the changeset and runsatomic/3. The rebuild dropped filters added by the action’s own
changes: bug #2969, fixed on 2026-09-26. The atomic and non-atomic paths can disagree. - Write authorization happens in six different places, depending on action type, check type
and whether the action transacts. One of them — filter checks on non-atomic update and destroy —
is a SELECT that runs before the transaction opens, which is a check-then-act gap. - Bulk actions have three strategies (
:atomic,:atomic_batches,:stream). Only:stream
keeps per-record changes and hooks. - The data layer contract is one behaviour with 46 callbacks, 44 optional, plus a
can?/2
capability probe with 47 feature names. Core askscan?and degrades, but inconsistently:
sometimes an exception, sometimes a string, sometimes a silent in-memory fallback. - Expressions are a two-stage tree. The same tree can run in memory or compile to SQL.
- Policies compile to a boolean formula solved by a SAT solver. Read policies become query
filters, so a forbidden read looks like “not found”. The policy breakdown output is the best
debugging tool in the framework. - Notifications queue in the process dictionary until the outermost transaction commits.
- AshPostgres installs SQL functions so Elixir semantics hold in the database. That has a
measured cost: a filter written with&&ran in ~3,400 ms against ~110 ms withand, because
the function call defeats the index.
3. Extension points
Document 02 catalogues 44. They fall into five groups:
| Group | What an extension can do | Examples |
|---|---|---|
| Add vocabulary | New DSL sections, entities, option schemas; add entities to another extension’s section | AshStateMachine state_machine, AshArchival archive |
| Rewrite the resource | Transformers can add attributes, actions, changes, relationships, or generate whole new resources | AshPaperTrail generates a version resource per resource |
| Run-time behaviours | Data layer, authorizer, notifier, change, validation, preparation, policy check, calculation, custom type, custom expression, custom aggregate, manual action, manual relationship, tracer | AshPostgres, Ash.Policy.Authorizer |
| Tooling hooks | codegen, migrate, setup, install callbacks called by mix tasks; Igniter installers that edit the project’s source |
mix ash.codegen, mix igniter.install |
| Free tooling | Formatter, cheat sheets, editor autocomplete, generated module docs, test helper for verifier errors | mix spark.cheat_sheets |
Three facts shape what Mesh should copy:
- A basic extension is small. AshArchival is 430 lines. PaperTrail is 3,827 and AshOban is
5,196, because transformers become the dumping ground for anything hard. - Extensions write into each other with no contract. PaperTrail inserts entities straight
into AshPostgres’sreferencessection. It works, and nothing checks it. - The tooling callbacks are duck-typed.
Ash.Extensionhas seven optional callbacks and no
module in the ecosystem declares that it implements the behaviour.
4. The packages that matter
Ranked by domain relevance first, adoption second (document 04, hex.pm downloads fetched
2026-10-01):
| # | Package | Area | One line |
|---|---|---|---|
| 1 | reactor |
Sagas | Step graph with compensation and undo; the saga engine |
| 2 | ash_paper_trail |
Auditing | A version resource per resource, four tracking modes, actor attribution |
| 3 | ash_events |
Auditing, event log | One central event log with replay |
| 4 | usage_rules |
Agent tooling | Collects each dependency’s rules file into AGENTS.md |
| 5 | ash_authentication |
Security | Authentication strategies and tokens |
| 6 | ash_oban |
Background work | Actions as background jobs, triggers, cron |
| 7 | ash_state_machine |
Domain | Attribute-backed state machine tied into policies |
| 8 | ash_ai |
Agent tooling | Actions exposed as LLM and MCP tools; policies still apply |
| 9 | ash_archival |
Domain | Soft delete with a default filter |
| 10 | ash_cloak |
Domain | Attribute encryption |
| 11 | ash_money |
Domain | Money type |
| 12 | ash_rate_limiter |
Cross-cutting | Per-actor, per-action rate limits |
| 13 | ash_typescript |
Client codegen | Typed TypeScript client, one function per action |
| 14 | ash_double_entry |
Domain | Double-entry accounting |
| 15 | ash_onetime |
Domain | Idempotency keys (community, new) |
| 16 | ash_postgres |
Storage | The production data layer |
| 17 | ash_phoenix |
Transport | Forms and LiveView integration |
| 18 | ash_json_api |
Transport | JSON:API server |
| 19 | ash_admin |
Admin UI | Generated admin dashboard |
| 20 | ash_graphql |
Transport | GraphQL server |
The four areas you asked about:
- Sagas. Reactor is a real saga executor. Its best idea is the split between
compensate
(“this step failed: retry, continue or give up?”) andundo(“this step succeeded but a later
one failed: roll it back”). The step order is inferred from declared arguments, and cycles are a
compile error. Durable workflows are not solved: people hand-assemble Reactor + AshOban +
AshStateMachine. Persisted runs are fragile because Spark names inline functions by a hash of
their code, so editing a step changes its identity. - Auditing. Two different shapes. PaperTrail records versions per resource. AshEvents records
every write in one log and can replay it. PaperTrail cannot hydrate an old version back into a
record, does not support bulk actions and does not scrub sensitive inputs. - Testing. First-party, not a package: generators derived from the resource (
Ash.Generator),
a seed path that bypasses actions (Ash.Seed), four assertion helpers,Ash.can?for policy
tests, and in-memory data layers. The community factory package deprecated itself in favour of
these. - Event sourcing and CQRS. Thin. AshEvents is an event log with replay; whether that is event
sourcing is disputed, because the tables stay the source of truth. The only CQRS binding
(ash_commanded, third-party) has 880 downloads and a published release that does not work.
The evidence says this is a niche in the Ash world.
ash_typescript deserves a note because Mesh generates the same thing: one typed function per
action over two endpoints, field selection, Zod or Valibot schemas. Its known gap is that the
client carries no authorization metadata, so a UI cannot ask “may this user do X” without a
hand-written action.
5. Strengths, ranked
From 183 verified quotes (document 05):
- Derived operations arrive complete. Filtering, sorting, pagination, authorization and
calculations come attached to every generated function. This is the benefit users name most. - It replaces the bespoke in-house framework every large app grows anyway. The common
argument for Ash is consistency, not speed. - Escape hatches are layered and real: action → manual action → generic action → plain
Elixir → Ecto. Every resource is also an Ecto schema, so leaving is possible. - Authorization as data. Policies that become query filters, with a readable breakdown.
- One declaration feeds many surfaces: JSON:API, GraphQL, typed TypeScript client, admin UI,
agent tools. - The agent surface is deliberate: rules files shipped in every package, actions exposed as
MCP tools under the caller’s own identity.
6. Shortcomings, ranked
- Learning curve and concept overload. The maintainers say this first. The sharpest version:
four overlapping ways to express one rule (policies, filter checks, preparations, expression
calculations). - Errors and stack traces. Behaviour lives in the library, so traces are unhelpful and test
coverage of your own resource reads 0%. - Lock-in, proven by an exit. A team of ten with ~200k lines and a paid support contract
removed Ash after two years: “it should not be the default choice for any team”. - Compile cost. One resource: 11,260 ms with a code interface, 2,270 ms without. Ash itself
has 185 compile cycles. - Bus factor. One person wrote 79% of human commits (5,315 of 6,719).
- Documentation, by the maintainers’ own repeated admission.
- Run-time surprises: six authorization placements, atomic and non-atomic paths that diverge,
silent in-memory fallbacks, loads that are silently ignored,has_onesilently truncated,
telemetry costing 15–23% of a create. - Fragile extension machinery: silent ordering failures, verifier errors as warnings,
editor autocomplete that does not work in the new official Elixir language server.
One pattern is worth more than any single item: every safety default that Ash 3.0 changed moved
from permissive to conservative (accept instead of reject, empty default accept list,
attributes private by default, atomic required by default, ambient actor and tenant removed).
Ash 3.0 had other breaking changes too, mostly renames; the safety defaults all moved one way.
The maintainers paid for permissive defaults and reversed them.
7. What the evidence says about the plan’s premise
The plan’s theory is that the declarative resource model, not Elixir, produced the agent win.
Being blunt about what the research found:
- Nobody has measured this. No source compares a declarative spec against the same thing in
plain TypeScript. Wasp’s benchmark (DSL app 2.5M tokens against 4.0M) is one run per arm, run by
the vendor, with no correctness measure. Convex’s +20% pass rate measures curated rules files,
not language design. - The one controlled study cuts both ways (arXiv 2602.11198: ten Python agent frameworks, one
task). It blames “declarative novelty” — abstractions missing from training data — and its top
scorer is itself declarative but convention-aligned. - Field reports conflict. Your experiment favours Ash. The team that removed Ash reports that
frontier models “struggle with Ash” and fall into anti-patterns they avoid with Ecto. - The syntax is not new, the vocabulary is. Mesh resource files use Marko, an existing
language with its own parser and editor tooling; only the tag names are new. The off-distribution
risk for a coding agent is the tag vocabulary, not the syntax. - Wasp deleted its DSL in July 2026. The stated reasons were positioning, adoption friction
and editor-tooling cost — not agents. A custom language carries a permanent editor-tooling tax;
Mesh avoids it by reusing Marko.
Conclusion: the plan’s Phase 0 spike is not a formality. It is the only experiment anyone will
have run on this question, and it should gate everything after it.
8. Recommendations for the design
All of this section is my recommendation.
Copy from Ash
| Idea | Why |
|---|---|
| One registry per vocabulary (types, checks, functions), read by compiler, docs and errors | Ash’s own docs drift from its registries: 39 registered functions, about 30 documented |
| Plain-data IR that every tool reads | Already in the plan; Ash’s map-based DSL state proves it scales to 20+ extensions |
| The onion lifecycle | The phases are right. Make it an explicit, printable plan (explain) instead of closures |
Access types (strict, filter, runtime) |
Maps directly onto the plan’s translatable and opaque split |
| Default deny, and conservative defaults everywhere | Ash reversed every permissive default in 3.0 |
| Structured policy breakdowns | Best debugging tool Ash has |
| A capability probe on the data layer | Right shape; make the feature list a closed union and define what happens when a feature is missing |
| Compensate and undo as separate saga concepts; step order from arguments | Cleanest design in the ecosystem |
Generators, a seed path, can() and an in-memory data layer for tests |
The community converged here and abandoned factories |
| Typed client with one function per action and field selection | Proven by ash_typescript |
| Rules files and MCP tools generated from the IR | The best-evidenced agent aid (Convex +20%, and the Ash maintainer credits it) |
| Staged migration flags with a named removal version | How Ash shipped the 3.0 flips |
Do differently
| Ash does | Mesh should | Reason |
|---|---|---|
| Authorizes writes in six places, one with a check-then-act gap | Authorize every write in one place, inside the transaction | The plan’s pipeline puts authorization (step 4) before the transaction (step 5): that reproduces the gap for any check that needs the record |
| Runs changes at build time, then again on the atomic rebuild | Plan the write once, pick one strategy, never run a change twice | Bug #2969 |
Orders transformers by before?/after? on module names, failing silently |
Named phases, a hard error on cycles, and a printed order | Three live mis-orderings in Ash |
| Turns verifier errors into warnings | Make strictness a deliberate setting, strict in CI | Ash had to build a test helper to recover the lost signal |
| Lets extensions write into each other’s sections unchecked | Decide the rule up front: either forbidden, or declared in the extension manifest | PaperTrail into AshPostgres |
| Seven optional duck-typed tooling callbacks | One typed extension manifest: sections, transforms, phases, codegen hooks, docs | Readers cannot tell which callbacks matter |
| Keeps behaviour in the library | Emit committed TypeScript that contains the logic | Ash’s 0% coverage and poor stack traces share this root |
| Ambient actor and tenant (removed in 3.0) | Explicit context value on every call, like Ash.Scope |
Removed for “subtle bugs” |
| Silent fallbacks (in-memory filter, ignored loads, dropped filters) | Hard errors | Already a plan principle; Ash shows how many places it leaks |
| Step identity by content hash | Stable, declared step names | Persisted sagas break on redeploy |
| Client with no authorization metadata | Generate per-action capability metadata with the client | Cheap at build time, costly to retrofit |
| Four overlapping ways to express a rule | Hold the plan’s “one way to do each thing” | The most specific learning-curve complaint |
Do not build yet
- A SAT-based policy solver. Start with a simpler tier; keep the structured breakdown.
- Event sourcing, CQRS, projections. Ship an event log later if asked.
- GraphQL, an admin UI, a durable workflow engine. Design the seams (stable step identity,
events committed in the same transaction as state) and stop there. - Ash’s breadth. Two users say the value appears only at scale; the team that left was at scale.
Gaps in the plan that need a decision
- Where behaviour lives. The plan says “generated code does the heavy lifting” and also
defines a sharedcorepipeline. If generated handlers are thin calls intocore, Mesh
inherits Ash’s coverage and stack-trace problems. Decide how much logic is emitted per resource. - Write authorization placement (see above).
- Atomic updates and bulk actions. The plan has neither. Without them, concurrent updates
race, and batch work has no story. This is the largest piece of the runtime the plan omits. - Things the plan does not name and probably needs in v1: identities (unique keys), upserts,
preparations (read-side hooks), action arguments, pagination, an error class hierarchy,
generated test generators. Likely deferrable: embedded resources,NewType, field policies,
manual actions, join-resource options. - Closures in translatable expressions. Every TypeScript library that parses arrow functions
at run time loses captured variables. A build-time compiler avoids that, but Mesh must write it:
nothing reusable exists (the closest, Tinqer, has one maintainer, ~230 downloads a week, and no
source spans in errors). - Which parser the expression compiler uses. TypeScript 7.0 ships no compiler API. The
choice is the TypeScript 6 package, oxc or SWC, and it should be swappable. Step 2 covers this. - Extension ordering and cross-extension writes (see above). The plan says extensions are IR
transforms; it does not say how they are ordered or what they may touch. - Guarding the committed generated tree. Wasp’s checksum manifest protects a gitignored
directory and would not catch hand edits to a committed one. Mesh needs its own check: regenerate
in CI and fail on any difference.
Step 2 — Foundation tools
Revised on 2026-10-01 after review by the operator. The first version of this section classed
tools as “integral” when their names would appear in generated code, and called Bun integral. That
was the wrong test. The definitions that apply:
- Hardcoded (integral): part of the core. Only what Mesh cannot run without.
- Adapter (replaceable): an implementation of a contract the core owns.
- Extension: an optional feature built on the core’s extension points.
Anything a project can do without, and anything most projects do not use, is an adapter or an
extension. Facts below are from document 07 (and 06 for the parser and syntax questions). The
classification is a recommendation.
9. Neutral standards
Four neutral contracts exist today and should be used as the contract wherever they fit: the Fetch
handler (Request) => Response (standardized by Ecma TC55), Standard Schema, SQL, and the
OpenTelemetry API.
10. Every layer is an adapter
| Layer | What the core owns | First adapters, and what the research found |
|---|---|---|
| Runtime | Web-standard APIs only; no runtime-specific calls | Bun and Node. Bun compiles to one binary for eight targets and ships SQLite and Postgres drivers. better-sqlite3 does not run on Bun, so drivers are per-runtime adapters |
| Authoring syntax | The resource model every front end produces | Marko files with Mesh tags (an existing syntax; only the tag names are new). TypeScript declarations as a second front end |
| Expression parser | The expression tree | oxc, or the TypeScript 6 compiler. TypeScript 7’s programmatic API is marked unstable and changes in 7.1 |
| Data layer | A query and write contract plus a capability list | Postgres, SQLite, in-memory. Drizzle suits the SQL adapters; Kysely has no documented bun:sqlite dialect |
| Server | One Fetch handler per action | Elysia, Hono, Bun.serve. Both frameworks accept Standard Schema and both need an adapter package on Node |
| API protocol | Action and type metadata | Typed client, OpenAPI, JSON:API, GraphQL and agent tools, each an emitter |
| Validation | Validators generated from the model, exposed as Standard Schema | Zod or Valibot emitted for clients. Standard Schema is opaque to a compiler |
| Authentication | “Resolve an actor from a request” | Better Auth. Auth.js is in maintenance under the same team |
| Jobs and workflows | Multi-step operations with stable step names | In-process runner first. Durable engines were inventoried, not compared |
| Observability | One event per lifecycle phase | OpenTelemetry API; any logger |
| Migrations | A snapshot of the resource model | drizzle-kit to start; it is mid-rewrite |
Authorization is a fixed slot in the core lifecycle, filled by a first-party policy extension, as
in Ash.
11. Two cases worth spelling out
A web framework as the core (Elysia). Elysia is more than a router: plugins with scoped
lifecycle and typed dependency injection, macros (declarative per-route options), 12 official
plugins, 96 community plugins and 16 documented integrations (counted from elysiajs.com on
2026-10-01; the plugin model was read from the documentation, not tested). Its limit is that
everything hangs off an HTTP request (ten request phases), while Mesh actions must also run with
no request: jobs, tests, agent tools, daemons. One person wrote 86% of its commits and it had one
release in the last 90 days. Recommendation: Elysia as the first-class server adapter (Mesh mounts
as an Elysia plugin and the ecosystem stays available), not as the core.
Data access. A contract across Drizzle, Kysely, TypeORM and Prisma can cover select, insert,
update, delete, transactions and raw SQL. Filters, joins and aggregates have four different
shapes. Relation loading, schema ownership, migrations and pooling cannot be covered. So the
contract has to be Mesh’s own, at the level of resources, as Ash’s is (46 callbacks, 44 optional,
plus a capability list). The query library inside an adapter is that adapter’s private choice.
12. Risks the research surfaced
- Drizzle v1 is a release candidate and relations v2 is a mandatory upgrade.
- Bun 1.4 is the first release written in Rust.
- Bun’s
findSourceMapalways returnsundefined; run-time error mapping needs a prototype. AsyncLocalStoragedoes not propagate intoWorkerorMessagePortevents in Bun.- Idle memory of a Bun server with SQLite is unmeasured, and single-binary builds are unchecked
for every candidate except Elysia. - Most candidates have 60–97% of commits from five people.
13. What the research did not cover
- Job, workflow, event-sourcing and authorization tools were inventoried, not compared.
- No JSON:API library for TypeScript was evaluated.
- Marko’s compiler, including whether it hands expressions over as a syntax tree.
- Elysia’s plugin and macro model in practice.
Step 3 — Proposed architecture
A proposal, not a finding, and not yet reviewed. The implementation plan comes after this is
agreed. The slide deck shows the same content as diagrams.
14. Design goals
- One declaration per resource; everything else derived.
- A small hardcoded core; databases, servers, runtimes and syntax are adapters; features are
extensions. - Generated code carries the behaviour: committed, readable TypeScript.
- Conservative by default: deny unless allowed, accept only listed inputs, private unless exposed,
context passed on every call. - Predictable execution: one lifecycle, fixed places for permission checks, hard errors instead
of silent fallbacks. - Legible to coding agents: a machine-readable model, generated rules files, conventional output,
errors that name the fix.
15. Three rings
| Core (hardcoded) | Adapters (one per choice) | Extensions (optional features) |
|---|---|---|
| Resource model as plain data | Front end (Marko by default) | Policies (first-party, on by default) |
| Vocabulary registries | Expression parser | Audit trail, event log |
| Compiler pipeline and phases | Data layer and driver | State machine, soft delete |
| Expression tree | Runtime host | Multitenancy |
| Action engine and lifecycle | Server and API protocol | Encryption, rate limits |
| Extension host and manifests | Actor resolver | Multi-step operations (sagas) |
| Error classes, explicit scope | Tracer, job runner | Typed client, OpenAPI, agent tools |
16. Build-time workflow
| # | Stage | What happens | Extension point |
|---|---|---|---|
| 1 | Load | Resource files written in Marko are read into declarations with source positions | Front end |
| 2 | Check structure | Nesting, options and types checked against the registered vocabulary | Vocabulary, types |
| 3 | Build model | One plain-data document per resource | Core |
| 4 | Transform | Extensions rewrite the model in named phases; a cycle is a hard error; the order is printed | Model transforms |
| 5 | Verify | Read-only checks across resources; a failure stops the build | Verifiers |
| 6 | Compile expressions | Each expression becomes a tree, marked for the database or in-process | Expression functions, parser |
| 7 | Emit | Types, handlers, schema, migrations, routes, client, rules files, as committed files | Emitters |
| 8 | Guard | CI regenerates and fails on any difference; errors point at resource files | Core |
17. Run-time workflow (one action call)
| # | Phase | What happens | Extension point |
|---|---|---|---|
| 1 | Enter | A route, job, agent tool or direct call supplies actor, tenant, context | Transports, actor resolver |
| 2 | Cast input | Only accepted fields pass; values cast and constrained | Types |
| 3 | Plan | Changes, validations and hooks ordered; one strategy chosen; printable | Changes, validations, preparations |
| 4 | Pre-check | Checks that need no data run first | Authorizer |
| 5 | Transaction | Opens; before hooks; checks that read data run inside it | Policy checks |
| 6 | Data layer | One contract; atomic changes fold into the statement; reads get a policy filter | Data layer |
| 7 | Commit | After hooks run inside the transaction, then it closes | Hooks |
| 8 | After commit | Notifications; typed result or classed error; one tracing event per phase | Notifiers, tracer |
18. Extension points
Build time: front end, vocabulary, model transform, verifier, expression function, emitter,
tooling hook. Run time: data layer, authorizer and checks, change and validation, preparation and
calculation, notifier, transport, runtime host and tracer. One typed manifest per extension
declares which points it uses and what it requires from adapters.
19. Open design questions
- Does the hypothesis hold? Build one application from Mesh resources and from hand-written
TypeScript, and measure a coding agent on both. - How much logic is generated per resource, and how much stays in the shared engine?
- Atomic and bulk writes: scope of the first version.
- Which data-layer capabilities are mandatory?
- May extensions contribute to each other’s part of the model? Proposed: only through
contributions declared in the manifest. - Policy engine depth: a simple tier first, or a solver from the start.
- Durable workflows: which engine, if any.
- First server adapter: Elysia, Hono, or both.