Abstract
Methods for creating, reading, updating, and deleting Graffiti objects.
Methods that retrieve or accumulate information about multiple Graffiti objects at a time.
Methods and properties for logging in and out of a Graffiti implementation.
Methods for for converting Graffiti objects to and from URIs.
Abstract
putCreates a new object or replaces an existing object.
An object can only be replaced by the same actor
that created it.
Replacement occurs when the GraffitiLocation properties of the supplied object
(name
, actor
,
and source
) exactly match the location of an existing object.
The object to be put. This object is statically type-checked against the JSON schema that can be optionally provided as the generic type parameter. We highly recommend providing a schema to ensure that the PUT object matches subsequent get or discover methods.
An implementation-specific object with information to authenticate the
actor
.
The object that was replaced if one exists or an object with
with a null
value
if this method
created a new object.
The object will have a tombstone
field set to true
and a lastModified
field updated to the time of replacement/creation.
Abstract
getRetrieves an object from a given location.
The retrieved object is type-checked against the provided JSON schema otherwise a GraffitiErrorSchemaMismatch is thrown.
If the object existed but has since been deleted,
or the retrieving actor
was allowed
to access
the object but now isn't, this method will return the latest
version of the object that the actor
was allowed to access with its tombstone
set to true
, so long as that version is still cached.
Otherwise, if the object never existed, or the
retrieving actor
was never
allowed
to access it, or if
the object was changed long enough ago that its history has been
purged from the cache, a GraffitiErrorNotFound is thrown.
The rate at which the cache is purged is implementation dependent.
See the tombstoneReturn
property returned by discover.
The location of the object to get.
The JSON schema to validate the retrieved object against.
Optional
session: null | GraffitiSessionAbstract
patchPatches an existing object at a given location.
The patching actor
must be the same as the
actor
that created the object.
A collection of JSON Patch operations to apply to the object. See GraffitiPatch for more information.
The location of the object to patch.
An implementation-specific object with information to authenticate the
actor
.
The object that was deleted if one exists or an object with
with a null
value
otherwise.
The object will have a tombstone
field set to true
and a lastModified
field updated to the time of deletion.
Abstract
deleteDeletes an object from a given location.
The deleting actor
must be the same as the
actor
that created the object.
If the object does not exist or has already been deleted, GraffitiErrorNotFound is thrown.
The location of the object to delete.
An implementation-specific object with information to authenticate the
actor
.
The object that was deleted if one exists or an object with
with a null
value
otherwise.
The object will have a tombstone
field set to true
and a lastModified
field updated to the time of deletion.
Abstract
discoverDiscovers objects created by any user that are contained
in at least one of the given channels
and match the given JSON Schema.
Objects are returned asynchronously as they are discovered but the stream will end once all leads have been exhausted. The method must be polled again for new objects.
discover
will not return objects that the actor
is not allowed
to access.
If the actor is not the creator of a discovered object,
the allowed list will be masked to only contain the querying actor if the
allowed list is not undefined
(public). Additionally, if the actor is not the
creator of a discovered object, any channels
not specified by the discover
method will not be revealed. This masking happens
before the supplied schema is applied.
discover can be used in conjunction with synchronizeDiscover to provide a responsive and consistent user experience.
Since different implementations may fetch data from multiple sources there is
no guarentee on the order that objects are returned in. Additionally, the method
will return objects that have been deleted but with a
tombstone
field set to true
for
cache invalidation purposes.
The final return()
value of the stream includes a tombstoneRetention
property that represents the minimum amount of time,
in milliseconds, that an application will retain and return tombstones for objects that
have been deleted.
When repolling, the lastModified
field can be queried via the schema to
only fetch objects that have been modified since the last poll.
Such queries should only be done if the time since the last poll
is less than the tombstoneRetention
value of that poll, otherwise the tombstones
for objects that have been deleted may not be returned.
{
"properties": {
"lastModified": {
"minimum": LAST_RETRIEVED_TIME
}
}
}
discover
needs to be polled for new data because live updates to
an application can be visually distracting or lead to toxic engagement.
If and when an application wants real-time updates, such as in a chat
application, application authors must be intentional about their polling.
Implementers should be aware that some users may applications may try to poll
discover repetitively. You can deal with this by rate limiting or
preemptively fetching data via a bidirectional channel, like a WebSocket.
Additionally, implementers should probably index the lastModified
field
to speed up responses to schemas like the one above.
The channels
that objects must be associated with.
A JSON Schema that objects must satisfy.
Optional
session: null | GraffitiSessionA stream of objects that match the given channels
and JSON Schema.
Abstract
recoverDiscovers objects not contained in any
channels
that were created by the querying actor
and match the given JSON Schema.
Unlike discover, this method will not return objects created by other users.
This method is not useful for most applications, but necessary for getting a global view of all a user's Graffiti data or debugging channel usage.
It's return value is the same as discover.
A JSON Schema that orphaned objects must satisfy.
An implementation-specific object with information to authenticate the
actor
.
Abstract
channelReturns statistics about all the channels
that an actor
has posted to.
This is not very useful for most applications, but
necessary for certain applications where a user wants a
global view of all their Graffiti data or to debug
channel usage.
An implementation-specific object with information to authenticate the
actor
.
A stream of statistics for each channel
that the actor
has posted to.
Abstract
loginBegins the login process. Depending on the implementation, this may involve redirecting the user to a login page or opening a popup, so it should always be called in response to a user action.
The session object is returned
asynchronously via sessionEvents
as a GraffitiLoginEvent with event type login
.
Optional
proposal: { actor?: string; scope?: {} }Suggestions for the permissions that the login process should grant. The login process may not provide the exact proposed permissions.
Optional
actor?: stringA suggested actor to login as. For example, if a user tries to edit a post but are not logged in, the interface can infer that they might want to log in as the actor who created the post they are attempting to edit.
Even if provided, the implementation should allow the user to log in as a different actor if they choose.
Optional
scope?: {}A yet to be defined permissions scope. An application may use this to indicate the minimum necessary scope needed to operate. For example, it may need to be able read private messages from a certain set of channels, or write messages that follow a particular schema.
The login process should make it clear what scope an application is requesting and allow the user to enhance or reduce that scope as necessary.
Abstract
logoutBegins the logout process. Depending on the implementation, this may involve redirecting the user to a logout page or opening a popup, so it should always be called in response to a user action.
A confirmation will be returned asynchronously via
sessionEvents
as a GraffitiLogoutEvent as event type logout
.
The session object to logout.
Abstract
Readonly
sessionAn event target that can be used to listen for the following events and they're corresponding event types:
login
- GraffitiLoginEventlogout
- GraffitiLogoutEventinitialized
- GraffitiSessionInitializedEventAbstract
synchronizeThis method has the same signature as discover but listens for changes made via put, patch, and delete or fetched from get, discover, and recoverOrphans and then streams appropriate changes to provide a responsive and consistent user experience.
Unlike discover, this method continuously listens for changes
and will not terminate unless the user calls the return
method on the iterator
or break
s out of the loop.
Example 1: Suppose a user publishes a post using put. If the feed displaying that user's posts is using synchronizeDiscover to listen for changes, then the user's new post will instantly appear in their feed, giving the UI a responsive feel.
Example 2: Suppose one of a user's friends changes their name. As soon as the user's application receives one notice of that change (using get or discover), then synchronizeDiscover listeners can be used to update all instance's of that friend's name in the user's application instantly, providing a consistent user experience.
The channels
that the objects must be associated with.
A JSON Schema that objects must satisfy.
Optional
session: null | GraffitiSessionAbstract
synchronizeThis method has the same signature as get but, like synchronizeDiscover, it listens for changes made via put, patch, and delete or fetched from get, discover, and recoverOrphans and then streams appropriate changes to provide a responsive and consistent user experience.
Unlike get, which returns a single result, this method continuously listens for changes which are output as an asynchronous GraffitiStream.
The location of the object to get.
The JSON schema to validate the retrieved object against.
Optional
session: null | GraffitiSessionAbstract
synchronizeThis method has the same signature as recoverOrphans but, like synchronizeDiscover, it listens for changes made via put, patch, and delete or fetched from get, discover, and recoverOrphans and then streams appropriate changes to provide a responsive and consistent user experience.
Unlike recoverOrphans, this method continuously listens for changes
and will not terminate unless the user calls the return
method on the iterator
or break
s out of the loop.
A JSON Schema that orphaned objects must satisfy.
An implementation-specific object with information to authenticate the
actor
.
Abstract
locationConverts a GraffitiLocation object containing a
name
, actor
,
and source
into a globally unique URI.
The form of this URI is implementation dependent.
Its exact inverse is uriToLocation.
Abstract
uriParses a globally unique Graffiti URI into a GraffitiLocation
object containing a name
,
actor
, and source
.
Its exact inverse is locationToUri.
An alias of locationToUri
This API describes a small but powerful set of methods that can be used to create many different kinds of social media applications, all of which can interoperate. These methods should satisfy all of an application's needs for the communication, storage, and access management of social data. The rest of the application can be built with standard client-side user interface tools to present and interact with the data — no server code necessary. The Typescript source for this API is available at graffiti-garden/api.
There are several different implementations of this Graffiti API available, including a federated implementation, that lets users choose where their data is stored, and a local implementation that can be used for testing and development. In our design of Graffiti, this API is our primary focus as it is the layer that shapes the experience of developing applications. While different implementations can provide tradeoffs between other important properties (e.g. privacy, security, scalability), those properties are useless if the system as a whole doesn't expose useful functionality to developers.
On the other side of the stack, there is Vue plugin that wraps around this API to provide reactivity. Other high-level libraries will be available in the future.
Overview
Graffiti provides applications with methods to create and store data on behalf of their users using standard CRUD operations: put, get, patch, and delete. This data can represent both social artifacts (e.g. posts, profiles) and activities (e.g. likes, follows) and is stored as JSON.
The social aspect of Graffiti comes from the discover method which allows applications to find objects that other users made. It is a lot like a traditional query operation, but it only returns objects that have been placed in particular
channels
specified by the discovering application.Graffiti builds on well known concepts and standards wherever possible. JSON Objects can be typed with JSON Schema and patches can be applied with JSON Patch. For interoperability between Graffiti applications, we recommend that objects use established properties from the Activity Vocabulary when available, however it is always possible to create additional properties, contributing to the broader folksonomy.
channels
are one of the major concepts unique to Graffiti along with interaction relativity. Channels create boundaries between public spaces and work to prevent context collapse even in a highly interoperable environment. Interaction relativity means that all interactions between users are actually atomic single-user operations that can be interpreted in different ways, which also supports interoperability and pluralism.Channels
channels
are a way for the creators of social data to express the intended audience of their data. When a user creates data using the put method, they can place their data in one or more channels. Content consumers using the discover method will only see data contained in one of the channels they specify.While many channels may be public, they partition the public into different "contexts", mitigating the phenomenon of context collapse or the "flattening of multiple audiences." Any URI can be used as a channel, and so channels can represent people, comment threads, topics, places (real or virtual), pieces of media, and more.
For example, consider a comment on a post. If we place that comment in the channel represented by the post's URI, then only people viewing the post will know to look in that channel, giving it visibility akin to a comment on a blog post or comment on Instagram (since 2019). If we also place the comment in the channel represented by the commenter's URI (their
actor
URI), then people viewing the commenter's profile will also see the comment, giving it more visibility, like a reply on Twitter. If we only place the comment in the channel represented by the commenter's URI, then it becomes like a quote tweet (prior to 2020), where the comment is only visible to the commenter's followers but not the audience of the original post.The channel model differs from other models of communication such as the actor model used by ActivityPub, the protocol underlying Mastodon, or the firehose model used by the AT Protocol, the protocol underlying BlueSky. The actor model is a fusion of direct messaging (like Email) and broadcasting (like RSS) and works well for follow-based communication but struggles to pass information via other rendez-vous. In the actor model, even something as simple as comments can be very tricky and require server "side effects". The firehose model dumps all user data into one public database, which doesn't allow for the carving out of different contexts that we did in our comment example above. In the firehose model a comment will always be visible to both the original post's audience and the commenter's followers.
In some sense, channels provide a sort of "social access control" by forming expectations about the audiences of different online spaces. As a real world analogy, oftentimes support groups, such as alcoholics anonymous, are open to the public but people in those spaces feel comfortable sharing intimate details because they have expectations about the other people attending. If someone malicious went to support groups just to spread people's secrets, they would be shamed for violating these norms. Similarly, in Graffiti, while you could spider public channels like a search engine to find content about a person, revealing that you've done such a thing would be shameful.
Still, social access control is not perfect and so in situations where privacy is important, objects can also be given an
allowed
list. For example, to send someone a direct message you should put an object representing that message in the channel that represents them (theiractor
URI), so they can find it, and set theallowed
field to only include the recipient, so only they can read it.Interaction relativity
Interaction relativity posits that "interaction between two individuals only exists relative to an observer," or equivalently, all interaction is reified. For example, if one user creates a post and another user wants to "like" that post, their like is not modifying the original post, it is simply another data object that points to the post being liked, via its URI.
In Graffiti, all interactions including moderation and collaboration are relative. This means that applications can freely choose which interactions they want to express to their users and how. For example, one application could have a single fixed moderator, another could allow users to choose which moderators they would like filter their content like Bluesky's stackable moderation, and another could implement a fully democratic system like PolicyKit. Each of these applications is one interpretation of the underlying refieid user interactions and users can freely switch between them.
Interaction relativy also allows applications to introduce new sorts of interactions without having to coordinate with all the other existing applications, keeping the ecosystem flexible and interoperable. For example, an application could add a "Trust" button to posts and use it assess the truthfulness of posts made on applications across Graffiti. New sorts of interactions like these can be smoothly absorbed by the broader ecosystem as a folksonomy.
Interactivy relativity is realized in Graffiti through two design decisions:
disableReplies
. Applications could then filter out any content from the replies channel that the original poster has not specifically approved.Implementing the API
To implement the API, first install it:
Then create a class that extends the
Graffiti
class and implement the abstract methods.Testing
We have written a number of unit tests written with vitest that can be used to verify implementations of the API. To use them, create a test file in that ends in
*.spec.ts
and format it as follows:Then run the tests in the root of your directory with:
Building the Documentation
To build the TypeDoc documentation, run the following commands:
Then run a local server to view the documentation:
TODO