Build StatusPost
A step-by-step guide to building a real-time message board with ARO.
WebSockets, repositories, templates, and more.
11 steps
~20 minutes
Intermediate
Imagine you want to build a simple real-time message board. Users can post short
status updates, and everyone connected sees new messages instantly. No page refresh.
No polling. Just real-time updates via WebSocket.
StatusPost is exactly that. It's a single-page app where you type a message,
hit Post, and it appears for everyone. The server stores messages in a repository,
broadcasts them via WebSocket, and even cleans up old messages automatically.
By the end of this tutorial, you'll understand:
Contract-First APIs
Define your HTTP routes in OpenAPI before writing code
WebSocket Support
Enable real-time bidirectional communication
Repositories
Store and retrieve data with simple actions
Repository Observers
React automatically when data changes
Templates
Render dynamic HTML with ARO expressions
Let's build it, step by step.
An ARO application is a directory containing .aro files and an optional
openapi.yaml for HTTP routing. Here's what StatusPost looks like:
StatusPost/
openapi.yaml # HTTP routes (contract-first)
main.aro # Application lifecycle
api.aro # HTTP handlers
websocket.aro # WebSocket handlers
templates/
index.html # Main page template
static/
style.css # Styles
app.js # Client-side JavaScript
Create this directory structure. We'll fill in each file as we go.
ARO automatically discovers all .aro files in your application directory.
No imports needed. Feature sets are globally visible.
ARO uses contract-first development. You define your HTTP routes
in an OpenAPI specification, and ARO routes requests to feature sets based on
the operationId.
StatusPost needs four endpoints: serve the homepage, serve static files, get messages,
and post a new message.
openapi: 3.0.3
info:
title: StatusPost API
version: 1.0.0
paths:
/:
get:
operationId: homePage
summary: Serve the main HTML page
/static/{file}:
get:
operationId: serveStatic
summary: Serve static files (CSS, JS)
parameters:
- name: file
in: path
required: true
schema:
type: string
/messages:
get:
operationId: getMessages
summary: Get last 20 messages
post:
operationId: postMessage
summary: Post a new message
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
message:
type: string
components:
schemas:
Message:
type: object
properties:
id:
type: string
message:
type: string
createdAt:
type: string
format: date-time
Each operationId maps directly to a feature set name. When a request
hits GET /, ARO looks for a feature set named homePage.
Every ARO application needs exactly one Application-Start feature set.
This is where you initialize services and prepare for events.
StatusPost needs an HTTP server with WebSocket support. We enable WebSocket by
passing a configuration object to the Start action.
(Application-Start: StatusPost) {
Log "Starting StatusPost..." to the <console>.
Start the <http-server> with { websocket: "/ws" }.
Log "StatusPost ready on http://localhost:8080" to the <console>.
Log "WebSocket available on ws://localhost:8080/ws" to the <console>.
Keepalive the <application> for the <events>.
Return an <OK: status> for the <startup>.
}
(Application-End: Success) {
Log "StatusPost shutting down..." to the <console>.
Stop the <http-server> with {}.
Return an <OK: status> for the <shutdown>.
}
The Keepalive action is essential for long-running applications.
Without it, the application would exit after startup completes.
Now we need to handle HTTP requests. The homePage feature set
serves the main HTML page. It retrieves recent messages from the repository
and transforms them through a template.
(serveStatic: StatusPost API) {
Extract the <file> from the <pathParameters: file>.
Create the <template-path> with "static/" ++ <file>.
Transform the <content> from the <template: template-path>.
Return an <OK: status> with <content>.
}
(homePage: StatusPost API) {
Retrieve the <all-messages> from the <message-repository>.
Extract the <recent-messages: 0-19> from the <all-messages>.
Transform the <html> from the <template: index.html>.
Return an <OK: status> with <html>.
}
(getMessages: StatusPost API) {
Retrieve the <all-messages> from the <message-repository>.
Extract the <recent-messages: 0-19> from the <all-messages>.
Return an <OK: status> with <recent-messages>.
}
Notice the Extract with 0-19? That's a range specifier.
It extracts the first 20 elements from the list. ARO supports
first, last, index access, and ranges.
The Transform action processes a template file. Variables from
the current scope (like recent-messages) are available inside the template.
When a user posts a message, we need to create a message object, store it in
the repository, and broadcast it to all connected WebSocket clients.
(postMessage: StatusPost API) {
Extract the <body> from the <request: body>.
Extract the <message-text: message> from the <body>.
Create the <message: Message> with {
message: <message-text>,
createdAt: <now>
}.
Store the <message> into the <message-repository>.
Broadcast the <message> to the <websocket>.
Return a <Created: status> with <message>.
}
The Store action saves the message to an in-memory repository.
Repositories are named collections that persist for the lifetime of the application.
The Broadcast action sends the message to all connected WebSocket
clients. They'll receive it as JSON and update their UI in real-time.
The <now> special variable gives you the current timestamp.
ARO automatically serializes it to ISO 8601 format for JSON responses.
When clients connect and disconnect via WebSocket, ARO fires events that you
can handle with feature sets. The business activity pattern is
WebSocket Event Handler.
(Handle WebSocket Connect: WebSocket Event Handler) {
Extract the <connection-id> from the <event: id>.
Log "WebSocket client connected" to the <console>.
Return an <OK: status> for the <connection>.
}
(Handle WebSocket Disconnect: WebSocket Event Handler) {
Extract the <connection-id> from the <event: connectionId>.
Log "WebSocket client disconnected" to the <console>.
Return an <OK: status> for the <disconnection>.
}
These handlers are optional but useful for logging and tracking connected clients.
The magic happens in Broadcast which ARO handles internally.
What happens when hundreds of messages pile up? We need automatic cleanup.
Repository observers let you react when data is stored, updated,
or deleted.
We'll create an observer that triggers when the message count exceeds 40.
It keeps the 20 most recent messages and clears the rest.
(Cleanup Messages: message-repository Observer) when <message-repository: count> > 40 {
Retrieve the <all-messages> from the <message-repository>.
Extract the <keep-messages: 0-19> from the <all-messages>.
Clear the <all> from the <message-repository>.
Store the <keep-messages> into the <message-repository>.
Log "Cleaned up messages, kept last 20" to the <console>.
Return an <OK: status> for the <cleanup>.
}
The when clause is a guard condition. The observer only fires when the
condition is true. <message-repository: count> returns the current
number of items in the repository.
Without the when guard, the observer would fire on every store operation.
The condition prevents unnecessary cleanup when there are few messages.
ARO templates are HTML files with embedded expressions. Use
{{ }} for values and {{ for each }} for loops.
<!DOCTYPE html>
<html>
<head>
<title>StatusPost</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<h1>StatusPost</h1>
<div class="form">
<form id="f">
<input type="text" id="msg" placeholder="Message..." required>
<button type="submit">Post</button>
</form>
</div>
<div id="status" class="disconnected">Connecting...</div>
<div id="messages">
{{ for each <msg> in <recent-messages> { }}
<div class="message">
<div class="time">{{ <msg: createdAt> | date: "dd.MM.yyyy HH:mm" }}</div>
<div>{{ <msg: message> }}</div>
</div>
{{ } }}
</div>
<script src="/static/app.js"></script>
</body>
</html>
The template loops through recent-messages and renders each one.
The | date: filter formats the timestamp.
The client-side JavaScript connects to WebSocket and updates the page in real-time:
var m = document.getElementById('messages');
var s = document.getElementById('status');
var f = document.getElementById('f');
var i = document.getElementById('msg');
function add(msg) {
var d = document.createElement('div');
d.className = 'message';
d.innerHTML = '<div class="time">' + formatDate(msg.createdAt) + '</div>' +
'<div>' + escapeHtml(msg.message) + '</div>';
m.insertBefore(d, m.firstChild);
}
var ws;
function connect() {
var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(protocol + '//' + location.host + '/ws');
ws.onopen = function() {
s.textContent = 'Connected';
s.className = 'connected';
};
ws.onmessage = function(e) {
try { add(JSON.parse(e.data)); } catch(err) {}
};
ws.onclose = function() {
s.textContent = 'Disconnected';
s.className = 'disconnected';
setTimeout(connect, 3000);
};
}
connect();
f.onsubmit = function(e) {
e.preventDefault();
var msg = i.value.trim();
if (!msg) return;
fetch('/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'message=' + encodeURIComponent(msg)
});
i.value = '';
};
With all files in place, run your application:
$ aro run ./StatusPost
Starting StatusPost...
StatusPost ready on http://localhost:8080
WebSocket available on ws://localhost:8080/ws
Open http://localhost:8080 in your browser. Open another browser window.
Post a message. Watch it appear instantly in both windows.
Post 41 messages. Watch the console log the cleanup. The observer keeps your
repository lean automatically.
That's StatusPost. A real-time message board with HTTP, WebSocket,
repositories, observers, and templates. All in about 50 lines of ARO code.
The complete source code is available on GitHub:
arolang/example-web-chat
Running with aro run is great for development, but for production
you want a native binary. ARO compiles to LLVM IR and links against the runtime
to produce a standalone executable.
$ aro build ./StatusPost
Compiling StatusPost...
Generating LLVM IR...
Compiling to object file...
Linking with runtime...
Build complete: ./StatusPost/StatusPost
$ ./StatusPost/StatusPost
Starting StatusPost...
StatusPost ready on http://localhost:8080
WebSocket available on ws://localhost:8080/ws
The compiled binary includes everything: your ARO code, the runtime, and all
dependencies. No interpreter needed. Just copy and run.
Use aro build --optimize for release builds. This enables
LLVM optimizations for smaller binaries and faster execution.
Keep Building
You've built a real-time application. Now explore more of what ARO can do.