Skip to content

Zeyn Framework: Vision and Architecture

Historical design note. This document was written before the code and is kept for the reasoning behind the decisions. Several APIs and names here were never built or changed (for example zeyn.toml, #[guard], DataGrid::server, the MCP tool names). For the current API read examples/docs/pages/, skills/zeyn/SKILL.md and AGENTS.md; when they disagree with this page, they are right.

Status: draft, 2026-11-02. This document is open for discussion. It will be finalized before any code is written.

1. Why Zeyn

Zeyn is a Rust web framework for data heavy applications (grids, forms, workflows), where AI agents write the application code and people read and review it.

Ext.NET reached end of support on 15 December 2023. For data heavy screens (dense grids, long forms, windows, fast keyboard use) there is no open source option that takes its place. Low code tools work well for simple admin panels but get stuck on a screen with 40 fields and five tabs.

The way code is written has changed too. AI agents now write most application code. Existing frameworks were designed for people to write. Zeyn is designed around this assumption:

AI writes the code. A person reads it, reviews it and changes it when needed.

Three principles follow from this assumption:

  1. The compiler is the first reviewer. Typical AI mistakes (null, wrong type, a forgotten error branch, unauthorized access) should be caught at compile time, not left to runtime. That is why Zeyn uses Rust.
  2. There is one way to do each thing. If there are ten ways to do something, the agent will use all ten and the code becomes unreadable. Application code stays inside a narrow, plain subset. Complexity stays inside the framework.
  3. The framework describes itself. The agent does not guess from documentation. It asks over MCP: "What are the valid settings for this component? Does this screen follow the rules? Run the test."

2. Goals and scope

Goals:

  • A strong grid that handles large data
  • Secure defaults: CSRF, CSP, role based authorization and audit log are on in the core
  • Predictable memory use and single binary deployment
  • A modern default UI kit, with the option to write your own
  • Full keyboard use and WCAG 2.2 AA accessibility
  • A built-in MCP server and llms.txt

Out of scope (at least in the first releases):

  • Marketing sites and content sites (Astro and Nuxt already do this well)
  • Mobile apps
  • Offline apps

Whether the core will be built on Leptos or written from scratch will be decided after analyzing existing frameworks (see docs/framework-analysis.md).

3. Architecture

┌─────────────────────────────────────────────┐
│ UI Kit (replaceable)                        │
│   design tokens + render contract           │
├─────────────────────────────────────────────┤
│ Headless components                         │
│   Grid engine, Form, Layout, Window,        │
│   Tree, Tabs; a11y, keyboard, test id,      │
│   WebMCP tools                              │
├─────────────────────────────────────────────┤
│ Core                                        │
│   component model, SSR, action,             │
│   security, audit, stateless requests,      │
│   MCP server, screen validator              │
└─────────────────────────────────────────────┘

3.1 Core

The HTTP layer is Axum. The component model, render engine and server calls are specific to Zeyn; reactivity inside islands uses reactive_graph. The reasoning is in docs/framework-analysis.md. The core provides:

  • #[action]: a server function where authorization, input validation and audit logging are mandatory
  • #[screen]: a screen definition that carries route, permission and title
  • Stateless requests: the server keeps no UI state between requests (no socket, no per user session state), so deploys and reconnects lose nothing and servers scale horizontally. Memory is bounded per request and per island
  • The MCP server (section 5)

3.2 Hybrid client model

  • Default: The page is rendered on the server. User interactions go to the server as an action. Business logic, authorization checks and raw data never reach the client.
  • Island: Heavy components such as the grid, charts and rich editors are loaded as WASM only where needed.
  • Transport: HTTP. WebSocket is often blocked on corporate networks, so live data (for example the kitchen screen) comes over optional WebSocket or SSE.
  • Fallback: If JavaScript is off or WASM fails to load, the grid appears as a read-only HTML table.

3.3 Grid

The grid is the most important part of the project and is treated as a separate product.

  • The engine is pure Rust: sorting, filtering, grouping, totals, pivot.
  • The same engine runs in two places:
    • Client mode: For medium sized data (target 100 thousand rows), with WASM inside a Web Worker. The main thread does not freeze.
    • Server mode: For large data, on the server. Only the visible page is sent to the client.
    • Switching between the two modes is a single setting. Application code does not change.
  • Row and column virtualization
  • Inline editing, locked columns, column grouping, selection
  • Full keyboard navigation (similar to Excel)
  • Export: CSV and XLSX

Performance targets (to be finalized):

ScenarioTarget
100 thousand rows, sorting in client mode< 150 ms
1 million rows, fetching a page in server mode< 100 ms (excluding database time)
Scrolling60 fps
Grid island size (gzip)< 300 KB

3.4 UI kit contract

The headless component provides behavior, accessibility and test ids. The UI kit provides only the appearance. A kit has two parts:

  1. Design tokens: color, spacing, typography, density
  2. An implementation of the render trait for each component

The default kit comes with a modern, dense design and supports light and dark themes.

4. What application code looks like

Screens are written with a fluent builder similar to Ext.NET. Plain Rust is preferred over a JSX style macro: a developer who does not know Rust can still read it, and the agent makes fewer mistakes.

Draft:

#[entity(table = "products")]
pub struct Product {
    pub id: ProductId,
    pub code: String,
    pub name: String,
    pub price: Money,
    pub stock: i64,
}

/// Returns products page by page.
#[action(permission = "stock.product.read")]
async fn search_products(q: DataQuery, db: Db) -> Result<Page<Product>> {
    db.products().query(q).await
}

/// Product list screen.
#[screen(route = "/stock/products", permission = "stock.product.read")]
pub fn product_list() -> Screen {
    Screen::new("Products")
        .toolbar([Button::new("New product").on_click(new_product)])
        .body(
            DataGrid::server(search_products)
                .column(Product::code).width(120).filter()
                .column(Product::name).flex(1).filter()
                .column(Product::price)
                .column(Product::stock).sum()
                .on_row_open(edit_product),
        )
}

Points to note:

  • Columns are not strings. They are typed fields generated by #[entity(table = "products")]. A wrong field name does not compile.
  • The Money type handles currency and rounding itself.
  • #[action] makes the authorization check mandatory. An action without a defined permission does not compile.
  • Every screen can also be exported to a JSON schema. MCP and test generation use this schema.

4.1 Enforced structure, guards and interceptors

Angular's strength was that it forced a project into a single structure: everything has a known place, and there are ready, easy patterns for common needs such as guards and interceptors. Zeyn takes this and applies it more strictly. It does not take the NgModule complexity, the learning cost of RxJS, or zone.js.

Project structure

app/
  zeyn.toml
  src/
    main.rs
    app.rs
    features/
      stock/
        feature.rs
        entities/
        actions/
        services/
        screens/
        tests/
      pos/
    shared/

The rules are checked by zeyn check. A project that breaks a rule does not compile or fails in CI:

  • Each feature lives in its own folder. Like Angular's standalone structure: there is no module file, and feature.rs only defines the route prefix, the guards and the public API.
  • Layer boundaries: screens cannot access the database directly, they can only call actions. Actions use services, services use the data layer.
  • Feature boundaries: a feature cannot reach into another feature, it can only use that feature's pub API.
  • Naming and file placement are fixed. The agent does not guess where to put something.

Guard

In Angular, guards run on the client, so they serve the user experience and do not provide security. Zeyn is server driven, so guards run on the server and are a real security layer.

/// Checks whether the user has access to the given warehouse.
#[guard]
async fn warehouse_access(ctx: &Ctx, warehouse: WarehouseId) -> GuardResult {
    ctx.user().can_access(warehouse).into()
}

#[screen(route = "/stock/warehouse/:warehouse", guards = [SignedIn, warehouse_access])]
pub fn warehouse_detail(warehouse: WarehouseId) -> Screen { ... }
  • Guards can be defined at the feature, screen and action level. A guard at a higher level is inherited by the levels below it.
  • The same guard protects both the screen and that screen's actions. It is not possible to hide a screen while leaving its action open.

Interceptor

Every action, screen and API call passes through the app's interceptors. The developer sees a small trait with two hooks; returning an error from before stops the call:

/// Logs slow calls. `after` runs for every call, also refused ones.
struct SlowCalls;

impl Interceptor for SlowCalls {
    fn after(&self, ctx: &Ctx, call: &Call, outcome: &CallOutcome) {
        if outcome.duration.as_millis() > 500 {
            tracing::warn!(screen = call.screen, user = ?ctx.identity().user, ms = outcome.duration.as_millis() as u64, "slow call");
        }
    }
}

Built in: AuditLog (with StderrAudit, MemoryAudit or an app store). Timeouts, rate limits and tenant context are app settings, not interceptors.

Dependency injection

Services are registered in app.rs with their scope (application wide or per request) and injected into actions as parameters. A missing registration is caught at application startup and during zeyn check, not in the middle of a run.

CLI

CommandPurpose
zeyn newNew application
zeyn g feature stockNew feature folder
zeyn g screen stock/product-listScreen, action and test skeleton
zeyn g guard, zeyn g interceptorGuard and interceptor skeleton
zeyn checkStructure, layer and naming rules
zeyn testTests
zeyn mcpMCP server

4.2 Combining applications (composition)

In Ext.NET a screen could load a page from another project using a URL and query string. For example, the patient card was written once, and the outpatient, laboratory and cashier screens showed it inside themselves. Zeyn offers this with three differences: parameters are typed, the embedded screen takes on the host application's appearance, and the security boundary is kept.

Level 1: Combining as a package (compile time)

A team publishes its feature as a crate. The other application adds it as a dependency.

use patient_card::{PatientCard, PatientCardParams};

pub fn outpatient_exam(patient: PatientId) -> Screen {
    Screen::new("Examination")
        .body(
            Layout::border()
                .west(Embed::screen(PatientCard, PatientCardParams { patient, read_only: true }))
                .center(exam_form()),
        )
}
  • Parameters are a struct, not a query string. A missing or wrong parameter does not compile.
  • The embedded screen's guards, actions and audit rules come with it. The host application cannot skip them.
  • Permission names are namespaced by package name (patient_card.view). Even if two packages define a permission with the same name, they do not clash.

Level 2: Remote screen (runtime)

Not in the scope of the first release. Level 1 comes first: different applications built by the same team in the same company.

Applications that different teams publish separately on separate servers can also be combined. This is the modern equivalent of loading by URL in Ext.NET.

Embed::remote(remotes::laboratory::ResultList, ResultListParams { patient })

How it works:

  1. The remote application publishes a manifest: its screens, parameter types, events and required permissions.
  2. The command zeyn add remote laboratory https://lab.institution.local generates typed Rust code from this manifest (similar to OpenAPI client generation). Remote screens also go through type checking at compile time.
  3. The remote screen sends a component tree, not HTML. The host application renders this tree with its own UI kit. So the embedded screen looks like a local screen, with the host application's theme and density. There is no iframe.
  4. User interactions are forwarded to the remote application through the host application. Guards also run on the remote side.
  5. The manifest is versioned. An incompatible change is caught by zeyn check and CI.

The advantage of the server driven model here: micro frontend problems, such as two separate JavaScript bundles clashing in the browser, do not happen. Everything sent to the browser comes from one application with one UI kit.

Communication between screens

The embedded screen and the host screen talk through typed events and context:

Embed::screen(PatientSearch, PatientSearchParams::default())
    .on(|e: PatientSelected, ctx| ctx.navigate(outpatient_exam(e.patient)))
  • Event: The embedded screen publishes defined events (PatientSelected). The host screen can only subscribe to these events.
  • Context: Shared context such as "active patient", "active warehouse" or "active branch" is passed from the parent screen to child screens. Each screen does not need to query this context again.

Security rules

  • Remote applications are listed in an allow list in zeyn.toml. An address that is not on the list cannot be embedded.
  • The user identity is passed to the remote application with OIDC token exchange, with limited permissions. The host application's session key is not shared.
  • The component tree coming from the remote side is validated against a schema. Raw HTML or scripts are not accepted. This closes the XSS risk.
  • Every remote call is written to the audit logs of both sides.

Later: loading as a plugin

To load a new feature without recompiling the application, isolated plugins with the WebAssembly Component Model will be evaluated. Not in the scope of the first release.

5. MCP and agent support

The development server also opens an MCP server. Planned tools:

ToolPurpose
components_listComponent catalog with short descriptions
component_docsA component's settings, examples and rules
screens_listScreens in the application, their routes and permissions
screen_inspectA screen's schema
screen_validateReturns compile errors and Zeyn rule violations in a form the agent can read
scaffoldGenerates entity, action and screen skeletons
runtime_treeComponent tree and state of the running screen
test_generateGenerates tests from the screen schema
test_runRuns tests and returns structured results
logsServer logs and audit records (sensitive fields masked)

Also:

  • llms.txt at the repo root and machine readable documentation per component
  • Every component produces a stable test id and ARIA role. The agent does not guess selectors.
  • In production, components can optionally be exposed as WebMCP tools.

5.1 Zeyn Studio

A development environment distributed as a single .exe. It is written with Zeyn and will be the first large application where the framework proves itself.

It has three parts:

  1. Live preview: The screens of the application under development. Clicking a component on the screen jumps to the matching line of code.
  2. Code editor: Monaco or CodeMirror 6, embedded as a Zeyn island. We do not write the editor from scratch.
  3. Embedded chat (optional): Connects to the AI agent the user chooses.

The chat is not tied to a single agent. Agent Client Protocol (ACP) is supported. ACP is an open protocol in which the editor and the agent speak JSON-RPC over stdio; Zed and JetBrains use it, and opencode supports it with the opencode acp command. This lets the user pick opencode, Claude Code or another agent that supports ACP. Studio's MCP server (section 5) connects to the agent automatically.

A user who does not want the chat uses Studio only as a preview and editor. They run their agent in their own terminal and connect it with zeyn mcp.

Packaging: the Zeyn server and the system's own WebView (WebView2 on Windows, WebKitGTK on Linux, WKWebView on macOS) in a single binary. Tauri's underlying libraries wry and tao can be used for this.

Order: work on Studio starts after the core, server and client runtime are working, following the 0.1 release.

6. Testing

  • Screen tests: A test driver inside Rust that runs without a browser. Fast and deterministic.
  • End to end tests: Playwright tests generated from the screen schema.
  • Goal: After writing a screen, the agent should be able to verify it with test_generate and test_run without human help.

7. Security

  • CSRF protection, strict CSP and secure cookie settings on by default
  • A permission definition is mandatory for every action
  • Audit log in the core: who changed which record, when, and what changed
  • Field level masking (for personal data, under KVKK and GDPR)
  • Dependency auditing: cargo audit, cargo deny and an SBOM with every release

8. Sample applications

Two flagship applications and four small samples to prove the framework. Each one shows specific capabilities.

Flagship applications

Inventory management

  • Product, warehouse, unit, stock movement, stock count, transfer
  • Shows: large grid, server mode, inline editing, master/detail, long form, authorization, audit, XLSX export

POS (restaurant and cafe point of sale)

  • Table layout, order taking, kitchen screen, bill splitting, payment, end of day report
  • Connected to inventory management: a sold product is deducted from stock according to its recipe
  • Shows: touch interface, live data (kitchen screen), concurrent work on the same record, two applications working together

Small samples

SampleShows
hello-crudThe smallest application: one entity, one list, one form
big-grid1 million rows, comparison of client and server mode
approval-flowLeave request: form with several steps, approval flow, screens based on role
dashboardCharts, KPI cards and report filters

9. Roadmap (first 3 months, draft)

MonthOutput
1Core skeleton: #[action], #[screen], security defaults, Form and Layout components, hello-crud
2Grid engine (client and server mode), big-grid, first version of the MCP server
3Window, Tabs, Tree; default UI kit; first version of inventory management; llms.txt; first public release (0.1)

POS comes in the second phase, after the first release.

Success criterion: an agent should be able to write a new screen at the level of hello-crud from a single request and pass its tests.

10. Open questions

  • Database layer: sqlx, sea-orm, or a thin Zeyn layer? PostgreSQL is the first target. Oracle and SQL Server later.
  • Fluent builder only, or a dual model that also supports Leptos's view! macro?
  • Should internationalization (i18n) be in the first release?
  • The visual identity of the default UI kit
  • Results of the spike that will validate the core decision (see docs/framework-analysis.md, section 6)

11. License

Dual licensed under MIT and Apache-2.0. Contributions under the DCO.