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) │
└──────────────────────────────────────────┘
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
beanscene 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
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
The @Core.transaction(...) and @Core.retryable(...) decorators you see in everyday development are both applications of this AOP:
-
@Core.transaction(...)defaults toREQUIREDpropagation: start a transaction if none exists, otherwise join the current one; never escalate isolation casually. UseREQUIRES_NEWonly when an independently committed boundary is explicitly required -
@Core.retryable(...)retries only safely replayable transient failures through an expliciterrorCodesallowlist. 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)
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
}
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
paramsmust explicitly defineroute.name; static routes generate canonical URLs via$router.getPagePath(...) - Page meta code generation
- Two-layer tabs navigation, router stack, routed dialog
- Explicit
requiresAuthandssrProfiledeclarations (Web defaults topublic; cookie-backed or protected first paint usessession)
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.dataor a model-derived projection) - Resource-style business data is owned by a ModelResource, which uniformly handles queries, caching, and mutations;
$apiSchemaor 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
$apidirectly
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-cabloyBasicWeband 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)
-
npm run devstarts Vona integrated SSR: Web entryhttp://localhost:7102/, Admin entryhttp://localhost:7102/admin/ -
npm run dev:zova:web/npm run dev:zova:adminstart 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
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
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-restartifacts already contain the expected changes, but Vona-side types remain stale — deletevona/node_modulesand 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
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.
Top comments (0)