Puck — Protocol Specification GitHub issue

vibecode
{"vibecode": {
    "doc": "puck-protocol",
    "role": "formal spec for the JSON expression of Puck — the message structures and behaviors a Puck server and client agree on when speaking JSON over HTTP",
    "key_concepts": ["json_wire_format", "class_definition_json",
        "shared_shape_with_caspian_and_mikobase",
        "remote_methods_block", "method_params_and_returns",
        "dynamic_objects_are_pucks_on_the_wire",
        "stored_objects_live_on_servers_referenced_by_dynamic_objects",
        "method_dispatch_via_url", "instance_body_is_class_plus_bucket"],
    "audience": ["puck_protocol_designers", "puck_server_implementors",
        "puck_client_implementors_in_any_language"],
    "example_universe": "Star Trek",
    "example_language": "JSON"
}}

This doc defines the Puck wire protocol expressed in JSON — the message structures and behaviors that a Puck server and client agree on when speaking JSON over HTTP.

The conceptual overview is in puck.md; a Python client sketch is in python.md; this doc is the formal spec.

JSON is the primary wire format.


Dynamic and stored objects GitHub issue

Two terms used throughout this doc:

The Geo example in §2–3 below is a dynamic object whose entire state is its fields (lat, lon) — nothing behind it on any server. The Starship example in §4 is a dynamic object that holds just a primary key (registry) and is a reference to a stored object — the actual ship data on the Starfleet server.


Class definition GitHub issue

A Puck class is defined in JSON, using the same shape Caspian and Mikobase use (see class-definition.md for the full Mikobase/Caspian spec). Here's https://puck.uno/geo with two fields and three remote methods:

json
{
    "name": "https://puck.uno/geo",
    "fields": {
        "lat": {"class": "number", "required": true},
        "lon": {"class": "number", "required": true}
    },
    "methods": {
        "weather": {
            "returns": {"class": "https://puck.uno/weather_report"}
        },
        "congressional_district": {
            "returns": {"class": "https://puck.uno/congressional_district"}
        },
        "map_image": {
            "params": {
                "zoom": {"class": "number", "integer_only": true, "default": 14}
            },
            "returns": {"class": "https://puck.uno/image"}
        }
    }
}

Every method in this block is callable by a Puck client over the wire. The server side of the class implements them; the JSON definition is what a client needs to know to dispatch a call correctly.


Invoking a method GitHub issue

A method call is typically a POST to a URL that encodes the class and the method, with a body that carries the instance's class and bucket (the field-value hash):

For example, calling weather on a Geo instance pointing at Starfleet HQ in San Francisco:

POST https://puck.uno/geo/weather

with body:

json
{
    "class": "https://puck.uno/geo",
    "bucket": {
        "lat": 37.7980,
        "lon": -122.4626
    }
}

And calling map_image(zoom=14) on the same instance:

POST https://puck.uno/geo/map_image

with body:

json
{
    "class": "https://puck.uno/geo",
    "bucket": {
        "lat": 37.7980,
        "lon": -122.4626
    },
    "params": {
        "zoom": 14
    }
}

The class in the body looks redundant with the URL, but it's the canonical declaration of what the bucket conforms to — useful for validation, for forwarding the call to another handler without re-parsing the URL, and for logs and replay.

Response shape GitHub issue

The response body is the method's return value, encoded as JSON with no envelope. The HTTP status code carries success/failure (200 OK for normal returns, error codes for the error catalog — TBD here).

For a primitive return, the body is just the primitive. For example, the response from a hypothetical hq.name (returning a string) is:

json
"Starfleet HQ"

For an object return, the body is a dynamic object — class plus bucket. The response from congressional_district on the Geo instance from §3 returns a dynamic object that references the stored CongressionalDistrict for that location:

json
{
    "class": "https://puck.uno/congressional_district",
    "bucket": {
        "code": "CA-11"
    }
}

The client can use that returned object as the body of further calls. To know what methods the returned object exposes, the client should fetch the class definition from https://puck.uno/congressional_district — the class's URL doubles as the URL where its JSON definition lives. To get the representative, the client then takes the returned object verbatim and POSTs it to the representative method's URL:

POST https://puck.uno/congressional_district/representative

with the same body it just received as the response.

The server is stateless with respect to instance identity. It keeps no handles, no sessions, no per-instance state between calls. Each request carries the object it operates on. Subsequent calls on the "same" instance from the client's perspective each re-send the full instance — the server reconstructs whatever it needs each time. Field values that a particular method doesn't actually use are still sent; the protocol doesn't try to optimize that away.

In practice the cost is small. Most Puck objects are tiny — a Geo is two numbers, a CongressionalDistrict reference is one URL string, a Color is one hex code. The wire payload is mostly HTTP/JSON envelope; the object data itself rarely amounts to much.


Stored objects GitHub issue

The Geo example in §2–3 is a dynamic object that carries all of its state in fields (lat and lon) — there's nothing behind it on any server. Every call ships those numbers and that's it.

But many objects only make sense server-side: a row in a database, a record in a registry, a long-lived account. These are stored objects — they live on a server, and they're too big or too sensitive or too database-bound to ship in full. For these, a Puck class defines a dynamic object that carries just enough to reference the stored one — typically a single primary-key field — and lets the server resolve everything else.

Example: a Starfleet starship class keyed by registry number. The dynamic object has one field; the stored object (the actual ship record, with name, captain, mission logs, crew list) lives in the Starfleet database.

json
{
    "name": "https://starfleet.com/starship",
    "fields": {
        "registry": {"class": "string", "required": true}
    },
    "methods": {
        "name": {"returns": {"class": "string"}},
        "captain": {"returns": {"class": "https://starfleet.com/officer"}},
        "current_location": {"returns": {"class": "https://puck.uno/geo"}}
    }
}

The dynamic class declares one field — registry, the primary key — and three methods. The server holds the stored object. A client asking for the ship's captain hits the captain method's URL and sends just the dynamic object (class + one-key bucket):

POST https://starfleet.com/starship/captain

with body:

json
{
    "class": "https://starfleet.com/starship",
    "bucket": {
        "registry": "NCC-1701-D"
    }
}

The server resolves NCC-1701-D against its database, finds the Enterprise-D, and returns the captain as a https://starfleet.com/officer reference — itself probably another PK-only object the client can make further calls on.

Same wire shape, different sized bucket. From the protocol's perspective, the Geo and Starship examples are identical: a URL that names class + method, and a body with class, bucket, and optional params. The only difference is what the dynamic object represents:

Most Puck classes pick one shape or the other. Mixed forms (some fields carried, some server-resolved) are allowed; the protocol doesn't care.


Client experience: Python (sketch) GitHub issue

A Python client wraps everything above. The developer doesn't see URLs, body envelopes, or marshaling — they get a Python class that behaves like any other Python class:

python
import puck

# Fetches the class definition from https://puck.uno/geo
# and returns a Python class wrapping it.
Geo = puck.lookup('https://puck.uno/geo')

# Instantiating produces a Python wrapper around the dynamic object
# (class + bucket). No remote call yet — just an object in memory.
hq = Geo(lat=37.7980, lon=-122.4626)

# A method call becomes the POST we specced above. The wrapper
# builds the URL, serializes the body, sends it, and unmarshals
# the response.
report = hq.weather

# Returns that are themselves dynamic objects come back as Python
# wrappers you can keep calling methods on. The chained shape
# reads like a local-object chain.
representative = hq.congressional_district.representative

The Python wrapper does three things behind that surface:

That's the feel from Python. For the full client spec — module layout, configuration, error mapping, properties-vs-methods, sync/async, open questions — see python.md.


Versioning GitHub issue

The Puck protocol supports versioning only loosely. Two recommended paths:

These two conventions cover the common cases. If the community wants a more fine-grained approach then let's have that discussion.

Versioning in Puck is distinct from Caspian's blockchain-signed versioning model.

© 2026 Puck.uno