DEV Community

Cover image for Inside Cabloy's Dual-Engine Architecture: How Vona and Zova Work Together
Uncle Pushui
Uncle Pushui

Posted on

Inside Cabloy's Dual-Engine Architecture: How Vona and Zova Work Together

Vona provides the backend framework and runtime; Zova provides the frontend framework and application layer. They are not a "backend folder + frontend folder" inside one big package. They are two engines that evolve independently, meshed together by generated artifacts, runtime integration, and bidirectional type contracts.

The first thing people notice when opening the Cabloy repository is usually the two large top-level directories: vona/ and zova/. Each has its own package.json, CLI, and workspace configuration, and each even carries its own __CABLOY_BASIC__ edition marker. This is not simply a "backend directory plus frontend directory" — it is the core design of Cabloy's fullstack architecture: one fullstack system, two engines.

This article walks through that dual-engine architecture end to end, using the Cabloy Basic repository (version 5.1.202, MIT licensed) as the baseline.

The Big Picture First

                     ┌──────────────────────────────────────────┐
   Browser  ───────▶ │  Vona (backend runtime, Koa, port 7102)  │
                     │   ├─ SSR Site dispatch (Web/Admin/...)   │
                     │   └─ Renders with Zova-built SSR output  │
                     └───────────▲──────────────────┬───────────┘
       Frontend metadata         │                  │  OpenAPI contracts
       (routes/components/icons) │                  ▼  flow downstream
                     ┌───────────┴──────────────────────────────┐
                     │  Zova (frontend, Vue3 + Vite + TSX)      │
                     │  Standalone SSR dev server (port 9000)   │
                     └──────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The key principle: frontend and backend concerns are separated and can evolve independently — but they do not collaborate by hand-copying interfaces or manually copying build files. They collaborate through generated artifacts, runtime integration, and shared conventions.

  • Backend technology base: Koa, Knex, Redis, and multiple database drivers (CI covers MySQL and PostgreSQL)
  • Frontend technology base: Vue, Vite, the Quasar toolchain, and TanStack libraries
  • Edition UI layer: Cabloy Basic uses DaisyUI + Tailwind CSS; Cabloy Start uses Vuetify. Quasar is part of the engineering toolchain, not the edition's UI component library

Part 1. Vona: The Backend Engine

Vona is the backend half of Cabloy. Its goal is to support SSR, SPA, Web, and Admin experiences within one system while keeping frontend and backend decoupled enough to evolve independently.

1. IoC Containers and the Bean System

Every capability in Vona is organized as a framework-managed Bean. All Beans inherit from BeanBase, whose working surface already includes app, ctx, bean, scope, $scope, $logger, $loggerChild, $text, and more — business code never needs to reassemble these access points by hand.

There are two container levels:

Container Responsibility
app container Global backend Beans (singleton-style)
ctx container Request-scoped Beans

A Bean has three forms of identity, and naming is not a style preference — it directly affects injection, config override paths, and how the CLI organizes generated code:

  • Bean identifier: {moduleName}.{sceneName}.{beanName}, e.g. training-student.service.student
  • Onion name: {moduleName}:{beanName}, e.g. training-student:student
  • Bean scene: the operational family a Bean belongs to — service, model, entity, dto, controller, startup, queue, broadcast, and so on. The built-in global bean scene is a deliberate exception with a plain shorthand surface

There are four styles of accessing a Bean:

Access style Best for Representative shape
Dependency injection Explicit wiring in a class @Use('training-student.service.student') serviceStudent
Dependency lookup (the recommended concise default) Ordinary module business code this.scope.service.student.findOne()
Direct bean access Container-level control this.bean._getBean(...) / this.ctx.bean._getBean(...)
Fresh bean creation Flows that must not reuse the default instance this.bean._newBean(...)

Note that this.bean._getBean(...) goes through the app-level container, while this.ctx.bean._getBean(...) goes through the request-scoped container.

2. Scope: The Module Resource Facade

The reason Vona backend code stays concise instead of degrading into arbitrary-path imports everywhere is Scope. Each module's service, model, entity, config, constant, locale, and error resources all hang behind one structured facade:

// Resources of the current module
this.scope.model.stockBalance

// A cross-module resource statically known in source
this.$scope.commerceCatalog.model.sku

// In tests or standalone scripts, where you hold an app reference
app.scope('commerce-catalog').model.sku
Enter fullscreen mode Exit fullscreen mode

One important design decision: Scope lookup does not create a module dependency edge. this.$scope.xxx merely resolves a resource from the module set already composed into the application; it neither installs nor loads the target module, and it cannot cause circular dependencies. Only when you genuinely need availability guarantees, dependency-first ordering, or a minimum compatible version do you declare vonaModule.dependencies.

3. AOP and the Structured Request Pipeline

Vona provides AOP across three aspect families — controller, internal, and external — plus a structured request path:

middleware → guard → interceptor → pipe (Zod validation) → filter
Enter fullscreen mode Exit fullscreen mode

The @Core.transaction(...) and @Core.retryable(...) decorators you see in everyday development are both applications of this AOP:

  • @Core.transaction(...) defaults to REQUIRED propagation: start a transaction if none exists, otherwise join the current one; never escalate isolation casually. Use REQUIRES_NEW only when an independently committed boundary is explicitly required
  • @Core.retryable(...) retries only safely replayable transient failures through an explicit errorCodes allowlist. It retries the downstream AOP suffix and never blindly retries external side effects

4. Contracts and the Data Layer: Source of Truth for the Forward Chain

The main backend request-handling chain is:

Controller → Service → Model → Entity
             (DTO inference/generation + Zod validation)
Enter fullscreen mode Exit fullscreen mode

Controller, DTO, and Zod schemas — together with decorator metadata such as @Api.field(...) — ultimately produce the OpenAPI document. That OpenAPI output is the single source of contract truth for frontend SDK generation, which becomes crucial in the contract loop below.

Vona also ships a broad set of long-term capabilities: CRUD-oriented workflows, multi-tenancy (a tenant is an instance; ordinary resource CRUD is automatically instance-scoped), multi-database/multi-datasource with sharding, two-layer caching (including the cross-Model modelsClear / modelsClearedBy dependency graph), transactions, authentication/captcha/RBAC/menus, file and image uploads, mail, queues, workers, schedules, election, broadcast, Redlock, WebSocket, metrics, and telemetry.

In terms of code organization, the framework core lives in vona/packages-vona/, the CLI in vona/packages-cli/cli/, and business code in vona/src/suite/ and vona/src/module/.


Part 2. Zova: The Frontend Engine

Zova is the frontend half of Cabloy. It combines the strengths of three mature frameworks: Vue3 reactivity, React-style TSX rendering, and Angular-style IoC. When reading Zova code, do not degrade it into "plain Vue plus a pile of utilities" — enter through its controller / bean / IoC architecture.

1. Three IoC Containers Unify All State Sharing

Many Vue 3 projects mix component-local state, composables, provide/inject, and Pinia depending on where state must be shared. Zova asks a different first question: which Bean owns this state or behavior, and which container scope should own that Bean?

Container Scope Rough Vue-world equivalent
sys System-wide singleton; survives across SSR requests Module-level singletons / global long-lived state
app Application / request level App stores, globally provided state
ctx Component-instance local Component-local state, in-component composables

The four typical sharing scopes therefore all fold into one model:

class ControllerPage {
  @Use() $$localCounterState: CounterState;                          // within a component
  @Use({ injectionScope: 'host' }) $$hostCounterState: CounterState; // between components (hierarchical injection)
  @Use({ injectionScope: 'app' })  $$appCounterState: CounterState;  // app-global
  @Use({ injectionScope: 'sys' })  $$sysCounterState: CounterState;  // system-level
}
Enter fullscreen mode Exit fullscreen mode

Under SSR, the distinction between app and sys is especially critical: app-level behavior can bind to a single application instance or a single request, while sys-level behavior is not request-bound and survives across SSR requests.

All Beans likewise inherit from BeanBase, whose core members include sys, app, ctx, bean, scope, $el, and $event; SSR modules extend it with $ssr and $useMeta, page-controller bases provide $params and $query, and component-controller bases provide $props.

2. Controller / Page / Component: TSX Rendering

Every Zova page and component has a controller bean as its logic carrier, with rendering described in TSX. The surrounding frontend engineering capabilities include:

  • a-router routing: routes with dynamic params must explicitly define route.name; static routes generate canonical URLs via $router.getPagePath(...)
  • Page meta code generation
  • Two-layer tabs navigation, router stack, routed dialog
  • Explicit requiresAuth and ssrProfile declarations (Web defaults to public; cookie-backed or protected first paint uses session)

3. Unified State Management with Models and ModelResource

Async state that drives rendering is owned by Models, not scattered across fetch/cache variables in individual controllers:

  • Establish query state during render with $useStateData(...), and read the reactive surface owned by the current query (query.data or a model-derived projection)
  • Resource-style business data is owned by a ModelResource, which uniformly handles queries, caching, and mutations; $apiSchema or the owning resource's schema directly drives automatic rendering of ZForm and ZTable
  • Only low-reuse, one-off page actions that need no shared state or cache ownership may call the generated $api directly

The purpose of these rules is clear: there is exactly one copy of schema metadata (on the backend); the frontend neither duplicates schemas nor hand-edits generated code.

4. Multi-Flavor Output: One Codebase, Many SSR Sites

The same Zova business source code can be built into multiple flavors (i.e., SSR Sites). The actual build artifacts in this repository (zova/dist/) include:

  • ssr-cabloyBasicWeb / ssr-cabloyBasicAdmin: the Web and Admin sites of Cabloy Basic
  • ssr-cabloyCommerce / ssr-cabloyCommerceAdmin: the e-commerce demo sites
  • rest-cabloyBasicWeb and other rest artifacts: frontend metadata and type packages consumed by Vona

Each flavor has its own env files (zova/env/.env.*), and at build time the SSR bundle and rest output must be produced as a pair and moved together.


Part 3. How the Two Engines Mesh: Two Channels

Between Vona and Zova there are two channels of an entirely different nature.

Channel 1: Vona Integrated SSR (the Runtime Channel)

The same frontend SSR application has two entry points during development. The difference is not "whether it is SSR" but who receives the browser request and owns the response:

Vona integrated SSR:   Browser → Vona(7102) → SSR Site matching → Zova build artifacts → browser hydration
Zova standalone SSR:   Browser → Zova dev server(9000) → frontend SSR (APIs still point to Vona)
Enter fullscreen mode Exit fullscreen mode
  • npm run dev starts Vona integrated SSR: Web entry http://localhost:7102/, Admin entry http://localhost:7102/admin/
  • npm run dev:zova:web / npm run dev:zova:admin start Zova standalone SSR (port 9000 by default) for fast hot-reload iteration on pages, routes, and hydration
  • Opening port 9000 directly cannot replace integrated acceptance at 7102: it bypasses Vona's SSR Site dispatch and the built-artifact handoff
  • The release/acceptance path is: build the SSR and rest artifacts → sync them into Vona with npm run deps:vona → run full acceptance from the Vona entry

Note that "standalone" only means you can open the Zova development server directly; it does not mean it is an independently deployable SSR application. Adding a mountable site requires a matching flavor, a paired set of SSR/rest artifacts, and an @SsrSite registration on the Vona side.

Channel 2: The Bidirectional Contract Loop (the Type and Codegen Channel)

Type information flows in both directions, and each side has its own source of truth.

Forward chain (backend → frontend)

Controller / DTO / Zod (contract truth)
        │  Vona emits OpenAPI
        ▼
Zova generates SDK ($api) and schema helpers ($apiSchema)
        │  generated code must never be hand-edited
        ▼
Frontend adds only thin semantic wrappers, reusing the same resource owner
Enter fullscreen mode Exit fullscreen mode

When changing a backend contract, change the backend truth first, then regenerate the frontend consumers — never hand-patch generated files.

Reverse chain (frontend → backend)

Zova's route, component, and icon metadata is the truth; after building, it flows back into Vona's tooling and type hints. The correct order is to build Zova first, then sync dependencies:

npm run build:zova:admin   # Admin-side changes
npm run build:zova:web     # also required when the Web flavor is affected — SSR bundle and rest must move as a pair
npm run deps:vona          # then sync the .zova-rest artifacts into Vona
Enter fullscreen mode Exit fullscreen mode

The contract loop also has two typical exception branches, handled in entirely different ways:

  • Consumer drift: fixed by regenerating; do not hand-edit generated output
  • Local dependency drift: the .zova-rest artifacts already contain the expected changes, but Vona-side types remain stale — delete vona/node_modules and reinstall dependencies instead of continuing to debug or hand-patching dependency links

The repository itself also wires a contract-loop-gate into git hooks, which automatically checks whether this chain has been broken at commit time. That means the "bidirectional contract" is not a documentation slogan — it is a workflow enforced by engineering guardrails.


Part 4. The Shared Organizational Skeleton: Suite / Module / Package

Both engines organize business capability by the same structural boundaries: suite → module → package. This is an architectural boundary, not just directory placement. Frontend and backend suites of the same name correspond one to one:

Suite Backend Frontend What it demonstrates
a-commerce vona/src/suite/a-commerce zova/src/suite/a-commerce A complete e-commerce Admin, Web site, personal center, and payment flows
a-training vona/src/suite/a-training zova/src/suite/a-training Master–detail forms, nested detail records, image and file uploads
a-demo / a-home Mirrored Mirrored Demo and home pages

Business consumers are also explicitly split into two kinds:

  • Admin Resource: back-office management, reusing the generic Resource owner and resource pages
  • Web self-service: the self-service Web frontend, with its own state, pages, and server-side scope

When one persisted resource serves both kinds of consumers, the domain and persistence boundary stays single, while the API/DTO contracts, server-side scope, frontend state ownership, and page architecture are separated.


Part 5. One Diagram to Remember It All

Zova (frontend truth)                     Vona (backend truth)
──────────────────────                    ──────────────────────
Page/Component/Controller                 Controller → Service → Model → Entity
sys/app/ctx containers + Bean             app/ctx containers + Bean (scene-based)
ModelResource + TanStack Query            DTO + Zod validation + OpenAPI output
flavors: Basic Web/Admin, Commerce        SSR Site registration, multi-tenant instances
route/component/icon metadata  ─reverse──▶  consumed via deps:vona
$api / $apiSchema generated SDK ◀─forward──  OpenAPI contracts
        │                                         │
        └───────── build artifacts: ssr-* / rest-* ┘
               Vona integrated SSR (7102) faces the world
Enter fullscreen mode Exit fullscreen mode

Once you understand this architecture, many Cabloy working conventions fall into place naturally:

  • Why regenerating the frontend SDK — not hand-editing $api — is the response to backend API changes
  • Why frontend route changes must be followed by a Zova build and then deps:vona
  • Why debugging fullstack issues requires distinguishing 9000 (frontend iteration) from 7102 (integrated truth)
  • Why schema-driven forms (ZForm/ZTable) never need field definitions duplicated on the frontend
  • Why business code is organized along suite/module boundaries instead of technical-type directories

Vona and Zova are each complete, independent, independently evolvable frameworks. Cabloy's value is precisely in assembling them — through the SSR runtime channel and the bidirectional contract loop — into one complete fullstack system built for AI Spec-Driven Development.


Further Reading

Top comments (0)