ZeroZ Stack · The full-stack framework
A field travels from a Java class, through a JSON encoder, across HTTP, into a TypeScript interface, and onto a DOM node — and at every one of those boundaries the compiler stops being able to help you. ZeroZ Stack removes the boundaries rather than optimising them.
The cost
Java objects are serialized into text (JSON), sent over HTTP, and parsed back into JavaScript/TypeScript objects on the client. Every field crosses on faith.
JavaScript or TypeScript is required to mutate a browser DOM — fracturing the codebase's language ecosystem in the one place Java never reached.
Rename a field on the server and nothing breaks — not at compile time, not at deploy. It breaks in a browser, for a user, as undefined. Add a nullable field and three artefacts have to agree about it: the entity, the DTO, and the TypeScript interface. Ask an AI agent to add a column end to end and it must hold four languages in context at once, which is where it starts inventing endpoints that do not exist.
The solution
A ZeroZ Stack client is compiled Java running as WebAssembly in the browser. It calls a Java interface. The call travels as packed binary over a persistent WebSocket and arrives at a CDI bean as the same object, with the same type. There is no controller, no deserializer and no mapping layer, because there is nothing to map between.
01 · How it works
You avoid boilerplate HTTP mapping, JSON translation, and ORM schemas entirely. Define a model, an interface, its implementation, and a UI — all Java, all sharing the exact same object.
// becomes serializable + persistable @DataModel public class ChatMessage implements BinaryPackable { private String author; private String text; public ChatMessage() {} // constructor, getters, setters… }
// the RPC contract — one interface @RmiService public interface ChatService { void sendMessage(ChatMessage msg); }
@ApplicationScoped public class ChatServiceImpl implements ChatService { @Inject ZeroZDbNode db; @Override public void sendMessage(ChatMessage msg) { // receives the exact object sent — persist it db.localDb().write(ctx -> { root.getMessages().add(msg); ctx.store(root.getMessages()); }); } }
public class ChatView extends Div { ChatView(ChatService chat) { Button b = new Button("Send"); b.onClick(e -> { // calls the backend over binary WS chat.sendMessage(new ChatMessage("Alice", "Hi!")); }); add(b); } }
→ scaffold the multi-module project with the Maven archetype, then mvn clean install and open http://localhost:8080
02 · The model
@LiveSync is state synchronization; signals are reactivity. The first keeps the object true; the second decides what the interface does about it. Bind a synchronized object to a signal and a server-side change redraws the UI without a line of glue.
Why binary
The client and server speak a dense binary RPC protocol over a persistent WebSocket. It is not a smaller JSON — it removes the text layer, and its costs, altogether.
Under the hood
Performance in the browser is guaranteed at compile time, not hoped for at runtime.
During Maven compilation, the annotation processor scans for @DataModel and @RmiService, generating _Serializer and _Stub classes. No runtime reflection — which WebAssembly handles poorly.
The client module transpiles Java bytecode — including generated stubs and serializers — directly into WasmGC to run in the browser.
Wasm runs on the single UI thread, so it cannot block. An RMI call suspends the coroutine, sends its frame, yields to the browser, and resumes exactly when the response arrives.
Each incoming WebSocket frame is handed to a virtual thread, so the server's I/O threads never block on long queries — thousands of persistent connections stay responsive.
The core is an agnostic CDI engine and RMI dispatcher; a server binding supplies the HTTP/WebSocket transport.
Unlike Vaadin, ZeroZ4j keeps no server-side DOM state. Components live entirely in the client Wasm heap, so a session costs the server nothing but its connection.
Deployment
The same application code runs three ways. Embedded, and the store is private to the JVM with no socket in the path. Auto-server, and the first JVM to start owns the store and serves it while the rest become clients — no configuration decides which is which. Or point every JVM at a dedicated database server and let none of them own data. The application code does not change between them; only the mode does.