One programming model,
two players.

RingScript puts the Ring VM in the browser. RingServ puts the same VM on the server. Because both sides speak the same call — service.action(payload) — a fullstack application is written once, and a separate declaration decides where each piece runs. This page shows the whole idea in three code blocks.

1 — The call is the same call, everywhere

In a RingScript page, a button asks for a price. On the server, a Ring service answers. Neither side knows where the other lives:

The page (RingScript, in the browser)
asks by name, gets an answer
aReply = serv.call("orders.price", [
    :item = "espresso",
    :qty  = 2
])

Page(:settext, [ :id = "total",
    :text = aReply[:data][:total] ])
The server (RingServ)
declares the same name, answers it
RingServ([ :services = [
    :orders = [
        :price = func aReq {
            # qty × the current price
            return Reply(:ok, [
                :total = Price(aReq) ])
        }
    ]
] ])

A JavaScript page can make the identical call with serv.call("orders.price", {item: "espresso", qty: 2}) — the model is not Ring-only, it is name-and-payload, which any client can speak.

2 — Declared once, placed separately

Where should orders.price actually run? In the page, so it works offline? On the server, where the whole dataset lives? That is a deployment decision, so it does not live in the application — it lives in the topology:

Topology([
    :app = "comptoir",

    # the menu lives in the page, and syncs back
    :data = [
        :menu = [ :store = :local, :sync = :onreconnect ]
    ],

    :services = [
        # instant in the page; the server has the last word
        :orders  = [ :site = :local, :authority = :server ],

        # needs the full dataset
        :reports = [ :site = :server ]
    ]
])

The one-word move. The day a rule outgrows the page — it needs data the page does not carry — change its :site and deploy. The rule's code does not change. The page's call does not change. Only the answer's journey does. And the server enforces the declaration: a service placed in the page is refused over the wire with a message naming the fix, so the topology is a fact, not a comment.

3 — Offline is a declaration, not an architecture

Mark a table :store = :local and the page keeps a copy, works without a network, and syncs when the connection returns. Underneath is machinery you never write: a change log on the read path, a queue on the write path that delivers each change exactly once even when the connection drops mid-send, and an honest signal when a client has been away so long it should refetch. It is the boring kind of protocol — which is precisely what you want carrying your data.

This is the shape RingScript's local-first pages already use. RingServ is the other half: the authority those pages sync back to.

Why this pairing is different

Same VM on both sides

Not "similar languages" — the same Ring VM, compiled to WebAssembly in the browser and natively on the server, both verified byte-identical to native Ring.

Same call shape

One seam, service.action(payload), on both sides of the wire. No route tables in the page, no client SDK to generate, nothing to keep in step.

Placement is enforced

Most stacks describe their deployment in a diagram that drifts. Here the topology is a declaration the server refuses to violate.

Start here Meet RingScript The topology, in depth