Build a real-time counter
Build a browser counter that publishes state through the BigState HTTP API and renders updates received through a WebSocket delivery. The implementation is small, but its structure follows the same object, state, and delivery model used by larger applications.
What you will build
A Vite vanilla application that publishes and consumes counter state.
What you will learn
How objects, state, deliveries, publishers, and subscribers fit together.
What you need
Node.js, a BigState API key, and your owner identifier.
The BigState mental model#
Before writing code, separate the four parts of the data flow:
Object definition
The contract and metadata for a named stream of state: its value type, retention settings, and descriptive information.
State
The changing value of that object. Each publication creates a new version with its own timestamp.
Delivery
A routing rule that selects objects and sends matching activity through a configured delivery type.
Publisher and subscriber
The HTTP client writes state; the WebSocket client receives matching messages.
For this tutorial, the object is simple:counter@YOUR_OWNER, its changing state looks like { count: 1 }, and the delivery routes that object's activity to a WebSocket subscriber.
HTTP publisher → counter state → matching delivery → WebSocket subscriber
The publisher and subscriber run in one page so you can see the complete round trip with one command. They are still independent roles: the page does not render the value returned by stateSet; it renders the value that comes back through the delivery. In production, publication usually belongs in a trusted backend or data-producing service, while browsers or other consumers subscribe separately.
Keep production credentials private
This browser example uses an API key to keep the tutorial self-contained.
In a production application, publish from a trusted backend and do not expose a privileged API key in public client-side code.
Before you begin#
You need:
- Node.js 22.12 or later.
- A BigState owner identifier, used to namespace the counter object.
- A BigState API key with permission to create objects, configure a delivery, and set state.
If you do not have these values yet, follow Create an account and save your credentials.
You can also fork the live StackBlitz demo or browse the source in bigstate-examples.
Create the project#
Purpose: create the runnable browser application and install the current BigState JavaScript client.
npm create vite@latest bigstate-counter -- --template vanilla
cd bigstate-counter
npm install
npm install bigstate.client.javascript
The files used in this tutorial are:
bigstate-counter/
├── index.html
└── src/
├── main.js
└── style.css
Key lines:
--template vanillacreates a JavaScript project without a framework.bigstate.client.javascriptsupplies both the HTTP publisher and WebSocket delivery clients.
Checkpoint: npm run dev should open the default Vite starter. Stop the server before continuing if you want to reuse the same terminal.
Build the presentation layer#
Replace index.html with:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BigState counter</title>
</head>
<body>
<div class="container">
<div id="counter"></div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Replace src/style.css with:
body {
margin: 0;
}
.container {
width: 100%;
height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
background: #0b1a2a;
}
#counter {
font-size: 100px;
font-weight: bold;
color: #9d50bb;
}
The page is only a centered counter. Delivered values write into #counter. The module script loads the entry file through Vite.
Write the app entry#
Replace YOUR_API_KEY and YOUR_OWNER, then paste this into src/main.js or src/main.ts:
import {
BigStateHttpClient,
BigStateWsDeliveryClient,
DeliveryType,
NotificationType,
ObjectStateType,
} from 'bigstate.client.javascript';
import './style.css';
const API_KEY = 'YOUR_API_KEY';
const OWNER = 'YOUR_OWNER';
const COUNTER_OBJECT = `simple:counter@${OWNER}`;
const COUNTER_DELIVERY = 'deliveryWsCounter';
const httpClient = new BigStateHttpClient({
baseUrl: 'https://api.bigstate.dev',
apiKey: API_KEY,
});
const wsClient = new BigStateWsDeliveryClient({
baseUrl: 'https://ws.delivery.bigstate.dev',
apiKey: API_KEY,
});
const counterElement = document.querySelector('#counter');
const configureResources = async () => {
const { error: objectError } = await httpClient.objectCreate(COUNTER_OBJECT, {
type: ObjectStateType.JSON,
ttl: 100000,
validity: 60,
versionDeep: 20,
info: {
name: 'Tutorial counter',
desc: 'Counter state used by the JavaScript tutorial',
example: { count: 0 },
},
});
if (objectError) {
throw new Error(
`Could not configure the counter object: ${objectError.message}`,
);
}
const { error: deliveryError } = await httpClient.deliveryCreate(
COUNTER_DELIVERY,
{
name: 'Counter WebSocket delivery',
type: DeliveryType.WS,
objects: [COUNTER_OBJECT],
},
);
if (deliveryError) {
throw new Error(
`Could not configure the delivery: ${deliveryError.message}`,
);
}
};
const handleMessage = message => {
if (message.type !== NotificationType.Message) return;
if (message.data.object !== COUNTER_OBJECT) return;
counterElement.textContent = message.data.state?.value?.count ?? '';
};
const subscribe = () => {
wsClient.setDeliveries([COUNTER_DELIVERY]);
wsClient.setMessageHandlers([handleMessage]);
};
const startPublishing = () => {
let count = 0;
return window.setInterval(async () => {
count += 1;
const result = await httpClient.stateSet(COUNTER_OBJECT, {
at: new Date().toISOString(),
value: { count },
});
if (result.error) {
console.error('Could not publish state:', result.error);
}
}, 1000);
};
const start = async () => {
await configureResources();
subscribe();
startPublishing();
};
start().catch(error => {
console.error(error);
});
How the code works#
The entry file has four layers: clients and names, resource setup, subscription, then publication. Startup runs them in that order.
Clients and resource names#
const COUNTER_OBJECT = `simple:counter@${OWNER}`;
const COUNTER_DELIVERY = 'deliveryWsCounter';
const httpClient = new BigStateHttpClient({
baseUrl: 'https://api.bigstate.dev',
apiKey: API_KEY,
});
const wsClient = new BigStateWsDeliveryClient({
baseUrl: 'https://ws.delivery.bigstate.dev',
apiKey: API_KEY,
});
COUNTER_OBJECTis the stable name shared by definition, publication, and delivery matching.httpClientperforms resource configuration and state publication.wsClientmaintains the delivery connection and receives messages.
Object and delivery#
await httpClient.objectCreate(COUNTER_OBJECT, {
type: ObjectStateType.JSON,
ttl: 100000,
validity: 60,
versionDeep: 20,
info: {
name: 'Tutorial counter',
desc: 'Counter state used by the tutorial',
example: { count: 0 },
},
});
await httpClient.deliveryCreate(COUNTER_DELIVERY, {
name: 'Counter WebSocket delivery',
type: DeliveryType.WS,
objects: [COUNTER_OBJECT],
});
The object fields describe how BigState handles values — they are not the counter value itself:
typedeclares JSON state, matching the{ count }values published later.ttlconfigures the object's lifetime.validityconfigures the state validity window.versionDeepretains up to 20 historical state versions.
objects: [COUNTER_OBJECT] makes the delivery match this counter. DeliveryType.WS selects WebSocket as the subscriber transport.
See Create or update an object for the complete object definition.
Subscriber#
const handleMessage = message => {
if (message.type !== NotificationType.Message) return;
if (message.data.object !== COUNTER_OBJECT) return;
counterElement.textContent = message.data.state?.value?.count ?? '';
};
wsClient.setDeliveries([COUNTER_DELIVERY]);
wsClient.setMessageHandlers([handleMessage]);
setDeliveries configures the delivery list and opens the WebSocket. setMessageHandlers installs the callbacks that process messages. Both run before publishing starts.
The handler reads this notification envelope:
message
├── type
└── data
├── object
└── state
└── value
└── count
message.typefilters for state-bearingNotificationType.Messagenotifications.message.data.objectignores unrelated matched objects.message.data.state?.value?.countreads the published JSON value and writes it into#counter.
Publisher#
await httpClient.stateSet(COUNTER_OBJECT, {
at: new Date().toISOString(),
value: { count },
});
Every second the page publishes a new state version:
atis the publication timestamp.value: { count }satisfies the object's JSON type.- The local
countvariable only generates values; BigState state is the published, versioned value onCOUNTER_OBJECT.
Startup order#
await configureResources();
subscribe();
startPublishing();
- Configure the object and delivery first.
- Subscribe before publishing, so the first update does not precede handler registration.
- Log configuration failures to the console.
Run the app#
Start Vite:
npm run dev
Open the local URL printed by Vite. The purple counter should start incrementing about once per second.
This confirms the complete loop: HTTP publication created a new state version, the delivery matched the counter object, and the WebSocket subscriber rendered the message.
To demonstrate that publisher and subscriber are separate roles, open a second tab. Each tab subscribes to the same delivery, and each also publishes because this tutorial combines both roles. Consequently, both tabs render updates from either instance.
Browser/CDN alternative#
For a quick single-file prototype, load the UMD build and use its BigState global. See the working example on StackBlitz.
<script src="https://cdn.jsdelivr.net/npm/bigstate.client.javascript@0.0.21/dist/index.umd.js"></script>
<script>
const {
BigStateHttpClient,
BigStateWsDeliveryClient,
DeliveryType,
NotificationType,
ObjectStateType,
} = BigState;
</script>
Use the same configuration, subscription, and publication logic. Do not add type="module" to the UMD script. Vite remains the primary runnable path for this tutorial.
Troubleshooting#
- Confirm the WebSocket base URL is
https://ws.delivery.bigstate.dev. - Confirm that
YOUR_API_KEYandYOUR_OWNERwere replaced and the delivery containsCOUNTER_OBJECT. - Check the browser console for authorization, configuration, or WebSocket errors.
- Close extra tabs if the counter advances faster than once per second; every tab also runs a publisher.