Mandy: ActivityPub on Goblins

birdculture1 pts0 comments

Mandy: ActivityPub on Goblins — Spritely InstituteMandy: ActivityPub on Goblins<br>Jessica Tallon — January 6, 2026

ActivityPub is the protocol that powers<br>the Fediverse. Not only does it allow different instances of the same app to<br>federate with one another, it also allows different apps to federate. For<br>example, a post on a video hosting app could federate to a microblogging app.<br>ActivityPub does a good job of this and is a leap forward from what<br>came before it.

For those unfamiliar, ActivityPub is a decentralized social networking protocol<br>standardized under the W3C. Both Spritely’s Executive Director,<br>Christine Lemmer-Webber and myself (Jessica Tallon) worked on standardizing<br>ActivityPub. The ActivityPub specification left holes in for identity,<br>distributed storage, and more. Since then Spritely has been a continuation of<br>this work, researching and developing the next generation of social web<br>infrastructure.

But where does this leave ActivityPub? Has it been abandoned by Spritely as a<br>stepping stone? No! We’ve long had a project (codenamed Mandy) on our roadmap to<br>implement ActivityPub on top of Goblins. If you open up the ActivityPub<br>specification you’ll actually see<br>mention of actors. The protocol<br>itself is designed with the actor model in mind. Since Goblins is an<br>implementation of the actor model, they should be a natural fit.

The source code for the prototype this blog post is based off can be found<br>here.

Goblins actors over HTTP

ActivityPub is a protocol on top of HTTP, but Goblins doesn’t use HTTP. So, the<br>first step was to make Goblins actors available over HTTP. Fortunately, hooking<br>this up was quite easy. There are many different ways we could do this, but for<br>this prototype I took a fairly simple approach.

Guile has a<br>built in web server.<br>not only that but fibers (the concurrency<br>system Goblins uses) has a backend for this. It means we can pretty<br>quickly start handling requests.

The webserver can be started using the run-server procedure. It takes in a<br>symbol which specifies an implementation ('http would be the built in HTTP<br>server, 'fibers is the one provided by Fibers):

(run-server handler 'fibers)

The handler is a procedure which takes a request and a body and expects the<br>HTTP response as the return value. When writing a typical HTTP server in Fibers<br>we’d suspend the fiber until the response is ready. However, Goblins code is<br>built around sending messages and waiting for promises to resolve. To bridge the<br>API between these two different philosophies, we use a channel.

Channels allow two fibers to send a message to one another. Reading or<br>writing to a channel causes the fiber to suspend until a message is available.<br>We can then send a message to one of our actors and use on to listen to the<br>response, once we have the response we can write our response to our channel.

Goblins vats are event loops which run on a fiber. These event loops manage a<br>queue of messages sent to actors spawned within that vat and are processed<br>sequentially. If we were to just write to the channel, we would suspend<br>underlying fiber for the vat. When the vat’s fiber suspends, it stops it from<br>processing other messages within the queue. To ensure that we’re not blocking<br>the vat by suspending it, we’ll use the helper procedure syscaller-free-fiber<br>which gives us a new fiber outside the vat which can be safely suspended.

(define vat (spawn-vat))<br>(define (^web-server bcom router)<br>(define (handler . args)<br>(define response-ch (make-channel))<br>(with-vat vat<br>(on (apply router args)<br>(lambda (response)<br>(syscaller-free-fiber<br>(lambda ()<br>(put-message response-ch (vector 'ok response))))<br>*unspecified*)<br>#:catch<br>(lambda (err)<br>(syscaller-free-fiber<br>(lambda ()<br>(put-message response-ch (vector 'err err))))<br>*unspecified*)))<br>(match (get-message response-ch)<br>[#(ok (content-type response))<br>(values `((content-type . (,content-type))) response)]<br>[#(ok (content-type response) headers)<br>(values `((content-type . ,content-type) ,@headers) response)]<br>[#(ok response)<br>(values '((content-type . (text/plain))) response)]<br>[#(err err) (error "Oh no!")]))<br>(syscaller-free-fiber<br>(lambda ()<br>(run-server handler 'fibers `(#:addr ,(inet-pton AF_INET "0.0.0.0")))))<br>(lambda () 'running))

(define web-server (with-vat vat (spawn ^web-server (spawn ^router))))

This is a slightly simpler version than the one used in the prototype, but it<br>shows how we’re making asynchronous actors which can return promises accessible<br>to HTTP requests. From the code above, we’ve already bridged into our<br>Goblins actors. This is a pretty flexible bridge as this ^router actor just<br>takes in a request and provides a response, we could dispatch this request in<br>any number of ways. For our prototype, this is the approach we took:

(define-values (registry locator)<br>(call-with-vat vat spawn-nonce-registry-and-locator))

(define (^router bcom)<br>(lambda (request body)<br>(define request-url (request-uri request))<br>(match (string-split (uri-path (request-uri request)) #\/)<br>[("" "static"...

response activitypub goblins fiber http server

Related Articles