When you first run Cabloy Basic, you will usually encounter two kinds of commands:
# Start the complete Cabloy service
npm run dev
# Start the Zova frontend SSR development service
npm run dev:zova:web
# or
npm run dev:zova:admin
Both can open an SSR page, but they serve different goals:
-
npm run devnormally serves the project athttp://localhost:7102. -
npm run dev:zova:*normally serves the Web or Admin page athttp://localhost:9000.
A newcomer does not need every SSR detail at the start. Remember this first: use 9000 to iterate on the frontend quickly; use 7102 to confirm that the project runs through its complete production-style path.
Two ports, two working rhythms
7102: Vona integrated SSR
When you visit 7102, the request first reaches the Vona backend service. Vona identifies the SSR Site for the URL, loads the corresponding Zova SSR build output, and returns the final HTML response to the browser.
So 7102 represents the complete runtime path:
Browser
→ Vona backend service
→ Zova SSR build output renders the page
→ browser takes over the page
This matches the core production-style model: Vona carries the HTTP request and SSR integration, while Zova renders the frontend page.
Use 7102 when you need to confirm that:
- the Web or Admin path is correct;
- Vona can find and load the right frontend build output;
- backend APIs, SSR pages, and the final HTTP response work together;
- the project runs correctly through its complete path before delivery.
9000: Zova standalone SSR
When you visit 9000, the browser enters the Zova frontend development service directly. It still server-renders the page, but its purpose is faster frontend work: edits to user code can hot-reload, and page, route, first render, and hydration issues are easier to inspect quickly.
Browser
→ Zova frontend development service
→ SSR-rendered page + user-code hot reload
Use 9000 first while you are:
- changing page layout or interactions;
- editing components, routes, or frontend state;
- checking the SSR first paint and the behavior after browser hydration;
- shortening the edit-to-feedback loop with hot reload.
7102and9000are the default Cabloy Basic development ports. Environment configuration can change the numbers, but not the responsibilities of the two entry points.
How a Cabloy project works as a fullstack system
Cabloy’s fullstack model is built around two simple principles.
1. Frontend build output participates directly in backend SSR
- Zova owns frontend application source: pages, components, routes, and frontend state.
- Zova’s frontend bundle and SSR-related output are loaded and used by the Vona SSR flow.
- Server rendering and browser hydration therefore stay on one coordinated delivery path.
A complete page visit can be understood as:
Browser request
→ Vona receives the request and finds the matching site
→ Zova SSR build output renders the page and prepares initial state
→ Vona returns HTML
→ the browser hydrates and continues the page
That is the basic meaning of integrated SSR: frontend SSR is not an isolated page prerender. It is one fullstack request completed together by Vona and Zova.
2. Type information flows in both directions
Cabloy uses a frontend-backend separation architecture. It does not simply place one shared types.ts file between the two sides. Instead, bidirectional contracts enable frontend and backend types to be generated and shared automatically.
- Backend → Frontend: Vona emits Swagger / OpenAPI contracts; Zova uses them to generate SDKs, types, and schema helpers.
- Frontend → Backend: Zova generates structural metadata and typing surfaces for routes, components, icons, renderers, and related resources; Vona tooling and type hints can consume them.
The next section uses one small example to make both directions concrete. For now, the key idea is enough: Vona owns backend entry and SSR integration, Zova owns the frontend application and rendering, and build output plus contracts let them collaborate.
Why do types need to synchronize in both directions?
One common fullstack problem is that the backend and frontend maintain two definitions that look alike: the backend changes a field, but the frontend forgets to update it; the frontend adds a renderable resource, but the backend cannot safely reference it.
Vona → Zova: when a backend API changes
When a Controller, DTO, validation rule, or entity field changes, Vona owns the business fact. Vona expresses it as Swagger / OpenAPI, then Zova generates the matching API, types, and schema helpers:
Vona API / DTO / validation
→ Swagger / OpenAPI
→ generated Zova API and types
→ frontend Model / page consumes them
The existing summary/:id endpoint in training-student shows the whole process. It returns a student summary, including a level title, summary text, and description length.
1. Define the endpoint and response DTO on the backend
The Vona Controller declares the URL, parameter, and response DTO:
// vona/.../training-student/src/controller/student.ts
@Web.get('summary/:id', { summary: $locale('StudentSummary') })
@Api.body(v.optional(), v.object(DtoStudentSummary))
@Core.serializer()
async summary(
@Arg.param('id', v.tableIdentity()) id: TableIdentity,
): Promise<DtoStudentSummary | undefined> {
return await this.scope.service.student.summary(id);
}
The response DTO declares the actual fields. If the backend adds summaryText, this DTO is the contract source:
// vona/.../training-student/src/dto/studentSummary.tsx
@Dto<IDtoOptionsStudentSummary>()
export class DtoStudentSummary extends $Dto.get(() => ModelStudent, {
columns: ['id', 'name', 'mobile', 'level'],
}) {
@Api.field(v.title($locale('LevelTitle')))
levelTitle: string;
@Api.field(v.title($locale('Summary')))
summaryText: string;
}
2. Regenerate the Zova API and types
First make sure Vona’s Swagger output contains the new field, then run:
npm run zova :openapi:generate training-student
This refreshes the generated Zova API method, OpenAPI response type, and schema facade. Do not edit generated files directly; the next generation will overwrite them.
3. Consume the generated API in the frontend
After generation, the Zova API surface provides trainingStudent.summary(...) with its generated response type. A frontend Model can wrap it with a thin semantic method:
// zova/.../training-student/src/model/student.ts
summary(id: TableIdentity) {
return this.$$modelResource.queryItem({
id,
action: 'summary',
queryFn: async () => {
const res = await this.scope.api.trainingStudent.summary({
params: { id },
});
return res ?? null;
},
});
}
A page or table action only needs to call student.summary(id) to receive fields such as summaryText and levelTitle. The frontend does not need to handwrite a second StudentSummary interface: when the backend DTO changes, regenerate the consumer and keep using the updated type.
The complete flow is: Vona DTO → Swagger / OpenAPI → openapi:generate → Zova API → Model / page.
Zova → Vona: when a frontend resource changes
Some facts belong to the frontend. A custom form field, table cell, route, or icon is implemented by Zova. Vona needs a stable resource identity to reference it, but it does not execute the frontend component source itself.
The current training-student module provides a compact example: Vona defines what a Student Level means and which values are allowed; Zova implements the matching level selector and level badge.
Vona: what Level means, its allowed values, and the renderer key to use
→ Zova: implements the form field and table cell behind that key
→ build and synchronize the handoff
→ Vona can safely reference the refreshed frontend resource
When a Zova resource like this changes, use the complete build and synchronization flow for the relevant flavor. For Admin, for example:
npm run build:zova:admin
npm run deps:vona
The role of these two steps becomes concrete in the training-student level renderer. Zova first declares a stable renderer key and implements the actual form control:
// zova/.../training-student/src/component/formFieldLevel/controller.tsx
declare module 'zova-module-a-openapi' {
export interface IResourceFormFieldRecord {
'training-student:formFieldLevel'?: IResourceFormFieldLevelOptions;
}
}
@Controller()
export class ControllerFormFieldLevel extends BeanControllerBase {
protected render() {
const { items = [], itemValue = 'value', itemTitle = 'title' } = this.$props.options ?? {};
return (
<div>
{items.map(item => (
<button key={String(item[itemValue])} type="button">
{item[itemTitle]}
</button>
))}
</div>
);
}
}
npm run build:zova:admin produces the Admin SSR and REST handoff artifacts. npm run deps:vona then synchronizes that handoff to Vona. After synchronization, Vona DTO or field metadata can reference the key and pass typed options:
// vona/.../training-student/src/entity/student.tsx
@Api.field(
v.title($locale('Level')),
ZovaRender.field('training-student:formFieldLevel', {
items: studentLevelItems,
placeholder: $locale('Level'),
}),
ZovaRender.cell('training-student:level', { items: studentLevelItems }),
z.union([z.literal(1), z.literal(2), z.literal(3)]),
)
level: number;
On the backend, DtoStudentSelectResItem inherits from ModelStudent and uses DTO field metadata to define how the list and form should be presented. After receiving the DTO, the frontend dynamically renders it from the renderer key and options: Zova still executes the actual JSX component, while the DTO only describes which renderer to use and which arguments to pass. It does not import or execute the frontend component source directly.
You do not need to memorize every command. The important direction is: the backend owns APIs and business rules; the frontend owns pages and renderers. When something changes, pass the contract from the side that owns the fact to the side that consumes it.
Where to explore next
If you are new to Cabloy, this is a useful learning sequence:
- Change a page through
9000to experience Zova development and hot reload. - Visit the same page through
7102to see how Vona carries the complete SSR request. - Change an API field and inspect how generated frontend types change from OpenAPI.
- Add a frontend renderer and learn why it needs a build and synchronization handoff to Vona.
As a project grows, this division makes problems easier to locate: is it a page-development issue, a Vona integration issue, or a contract-synchronization issue? You no longer have to describe every mismatch as simply “frontend and backend are out of sync.”
Further reading
- GitHub: https://github.com/cabloy/cabloy
- Docs: https://cabloy.com
- Demo: https://cabloy.com/demo
Create fast feedback at 9000, then prove the complete runtime path at 7102.

Top comments (0)