Getting Started
This guide sets up a minimal stack with only @livon/* packages.
Before you start: generated client requires sync
For the generated client workflow (import {api} from './generated/api'), @livon/cli sync is required.
If sync is not running (or has never run), your generated client API file is missing or outdated.
Install
Server runtime stack
pnpm add @livon/runtime @livon/schema @livon/node-ws-transport
Client runtime stack
pnpm add @livon/runtime @livon/client @livon/client-ws-transport
Required for generated client workflow
pnpm add -D @livon/cli
1. Define schema once
Use schema composition (and, union, or) so you can extend schemas without duplicating field definitions.
import {
and,
api,
literal,
object,
operation,
or,
string,
subscription,
union,
} from '@livon/schema';
const MessagePayload = union({
name: 'MessagePayload',
options: [
string().min(1),
object({
name: 'AttachmentPayload',
shape: {
url: string(),
},
}),
],
});
const RoomTarget = or({
name: 'RoomTarget',
options: [literal({name: 'GlobalRoom', value: 'global'}), string()],
});
const MessageInput = object({
name: 'MessageInput',
shape: {
author: string().min(2),
payload: MessagePayload,
room: RoomTarget,
},
doc: {
summary: 'Chat message payload',
example: {author: 'Alice', payload: 'Hello', room: 'global'},
},
});
const WithId = object({
name: 'WithId',
shape: {
id: string(),
},
});
const Message = and({
left: MessageInput,
right: WithId,
name: 'Message',
});
const sendMessage = operation({
input: MessageInput,
output: Message,
exec: async (input) => ({...input, id: 'msg-1'}),
publish: {
onMessage: (output) => output,
},
});
const ChatApi = api({
operations: {sendMessage},
subscriptions: {
onMessage: subscription({payload: Message}),
},
});
export const serverSchema = ChatApi;
serverSchema is used directly by schemaModule(...).
No extra schema-module input adapter is required.
Parameters in this example
object({...}):
name(string): schema name.shape(Record<string, Schema>): payload fields.doc(SchemaDoc, optional): summary/example metadata used by generated docs.
and({...}):
left(Schema): reusable base schema.right(Schema): additive schema.name(string, optional): composed schema name used in generated client types.
union({...}) / or({...}):
options(Schema[]): multiple allowed variants without duplicating object definitions.
operation({...}):
input(Schema): receive boundary schema.output(Schema): send boundary schema.exec((input, ctx) => result): operation logic with validated input.publish(Record<string, (output) => payload>): publish mapping for subscription topics.
api({...}):
operations(Record<string, Operation>): operation map.subscriptions(Record<string, Subscription>): subscription map.
2. Mount server runtime
import {runtime} from '@livon/runtime';
import {schemaModule} from '@livon/schema';
import {nodeWsTransport} from '@livon/node-ws-transport';
runtime(
nodeWsTransport({server: wsServer}),
schemaModule(serverSchema, {explain: true}),
);
Parameters in this example
nodeWsTransport({server}):
server(WebSocketServerLike): websocket server instance from your host runtime.
schemaModule(serverSchema, {explain}):
serverSchema(Api | ComposedApi): executable schema bundle fromapi(...)orcomposeApi(...).explain(boolean): exposes schema explain metadata for client generation.
3. Start client API sync (required)
livon \
--endpoint ws://127.0.0.1:3002/ws \
--out src/generated/api.ts \
--poll 2000 \
-- pnpm dev
Parameters in this example
livon --endpoint ... --out ... --poll ... -- <command>:
--endpoint(string): endpoint used to fetch explain metadata.--out(string): generated client module output path.--poll(number): repeat interval in milliseconds.--(delimiter): separates LIVON flags from the linked command.<command>(string): starts your app process after sync starts (for examplepnpm dev).
Why this step is required
@livon/client in generated mode consumes schema code emitted by the sync process.
This keeps operation/subscription signatures structurally aligned with the server schema.
LIVON is kill-all by design in linked mode: if sync exits, the linked command is terminated; if the linked command exits, LIVON exits.
4. Mount client runtime and call operation
import {runtime} from '@livon/runtime';
import {clientWsTransport} from '@livon/client-ws-transport';
import {api} from './generated/api';
runtime(
clientWsTransport({url: 'ws://127.0.0.1:3002/ws'}),
api,
);
api({
onMessage: (payload) => {
payload.id;
},
});
await api.sendMessage({author: 'Alice', payload: 'Hello', room: 'global'});
Parameters in this example
clientWsTransport({url}):
url(string): websocket endpoint URL.
api({onMessage}):
onMessage((payload) => void): typed subscription callback.
api.sendMessage(input):
input(MessageInput): typed operation input generated from server schema.
5. Run required processes
Keep these processes running during development:
- server runtime process (with
schemaModule(..., {explain: true})) - one linked process that runs sync and your app command together (
livon ... -- pnpm dev)
In linked mode, process lifecycle is shared (kill-all semantics).
6. Minimal troubleshooting
If client API usage fails, check in this order:
schemaModule(..., {explain: true})is enabled.livon --endpoint ...points to the same endpoint your client transport uses.- generated output file from
--outexists and is current.