RFC: Native support for unique list relationships between stream types and accounts

Context

There is a unique challenge new to Web3 composable ecosystems when defining relationships between two sides that are both user authored content. This challenge does not exist in traditional Web2 systems because, in those systems, a central authority has the ability to enforce custom rules on one side of the relationship (e.g., users can only leave one review for a product, as regulated by the central authority of the Amazon marketplace). In Web3 systems, all relationship rules must be defined at the protocol level.

Ceramic currently supports two account relationship types:

  • SINGLE - A developer can define a BasicProfile schema and ensure only one document type using this definition can be created per user account (i.e., one DID can have one BasicProfile)
  • LIST - A developer can allow users within a decentralized social media platform to create as many Post instances as they want (i.e., one DID can have many Posts)

Problem

The existing relationship types do not cover all use cases. For example, in a decentralized marketplace, a developer may want to ensure that each user account (or DID) can only create only one Review for a specific Product. However, they may also want to allow users to create Reviews for as many individual Products as they want, so long as they can only leave 1 Review per Product

Ceramic does not currently natively support this use case, which means a user could hypothetically write multiple Reviews for a single Product. Developers can attempt to work around the existing limitations by consuming all the Reviews a user creates for one Product and applying a custom filter so that only one Review is displayed. However, this is inefficient and additionally introduces centralized application bias in each different data consuming application within the ecosystem.

Goals

  • Transfer decision load or relationship logic from the developer to natively within the protocol to
    • Improve the developer experience
    • Minimize centralization and bias bottlenecks in data consumption
    • Continue enabling products built around “user-controlled data” as opposed to “application-controlled” data
    • Improve composability between apps

Solution

This problem is solved by introducing a new “unique list” relationship type that is defined at the protocol level. This new relationship type is called “SET”, after the data structure known in existing computer science definitions.

With the SET feature, a developer can ensure that each user account (or DID) can only create ONE Review for a specific Product. However, users are allowed to create Reviews for as many individual Products as they want, so long as they only leave 1 review per product.

Code sample: Using SET on a subfield that defines a relation to another document

## very simple boolean-style review shown below

type Product @loadModel(id: "...") {}

type Review @createModel(accountRelation: SET, accountRelationField: "postID") {
  postID: StreamID! @documentReference(model: "Post")
  review: Boolean! 
}

Code sample: Using SET without a relation to another document

## very simple boolean-style trust signal shown below 

type TrustedResource @createModel(accountRelation: SET, accountRelationField: "uri") {
  uri: URI! 
  review: Boolean! 
}

Requirements

Feature Details Priority
New relationship type Introduce a custom syntax that allows developers to define a unique list relationship between accounts and a specific model type Must have
Relationship type defined at model schema level The relationship must be defined during model schema creation. Must have
By schema controller Can only be defined by controller of the schema Must have
Cannot be changed The relationship for a schema cannot be changed later (i.e. from SET to LIST) Must have
Relationship value defined at model level The actual value (i.e., what other thing the model has a SET relationship with) is defined at time of model schema creation Must have
Relationship can be deleted and undeleted E.g., once a user has created a review for a product, the review can be deleted and also undeleted. Must have
Compatible with other new features Allows developers to equitably use SET alongside other features (such as field locking), and in doing so, prevents “conflicting” interactions between the two features Must have

Appendix

Existing relationship types supported by ComposeDB

SINGLE

  • A developer can defines a BasicProfile schema and ensure only one document type using this definition can be created per user account
  • (i.e., one DID can have one BasicProfile)
type BasicProfile 
@createModel(accountRelation: SINGLE, description: "Very basic profile") 
{
displayName: String! @string(maxLength: 50)
}

LIST

  • A developer can allow users within a decentralized social media platform to create as many Post instances as they want
  • (i.e., one DID can have many Posts)
type Message 
@createModel(accountRelation: LIST, description: "Direct message model") 
{
recipient: DID! @accountReference
directMessage: String! @string(maxLength: 50)
}
5 Likes

Instead of

type Review @createModel(accountRelation: SET, accountRelationField: "postID") {

what about

type Review @createModel(accountRelation: UNIQUE_LIST, uniqueField: "postID") {

?

Essentially I’m wondering if the term “unique list” is more intuitive for people to understand than “set” for the account relation?

I think it’s more confusing, because it’s ambiguous if it’s the list itself that’s unique or its elements.

But indeed, understanding why it’s called SET probably requires some computer science/math background. On the other hand, I can’t think of a more correct name. I think good docs can solve this schism, this isn’t user-facing functionality anyway. :slight_smile:

@rohhan :

I’m curious on your thoughts on why this needs to be singular. I can think of cases where this should be 1-n fields. In the same example, consider one review per productID and order.

Curious on this must-have. Could you elaborate on what you mean with “delete” here? I think it’s a bit surprising to be able to mutate the unique variable whatsoever for two reasons:

  • Removing the value would mean you can indeed create a new instance, so the SET doesn’t apply historically, only at the tip. Hence, there is no way of knowing if there were indeed other instances.
  • Changing the value means that a SET stream can be unique against different things at different points in history. I think this is very confusing.

Possibly this RFC resolves this. But I’m still curious to the reasons for these cases, because I’d assume I could still “see” there is a stream XYZ here, but the content has been deleted in the latest commit. Take deleted posts on this forum for example, it’s good for transparency that the “ref” is still there, even if the content is gone. I think it’s important that history can’t just disappear at the whim of the controller, which is what happens at the indexer level if a stream ref is removed or changed.

Hey m0ar, great feedback! Sorry for the delay in getting back to you over the holidays, but our team has now had a chance to review the feedback you provided.

I’m curious on your thoughts on why this needs to be singular. I can think of cases where this should be 1-n fields. In the same example, consider one review per productID and order.

Good callout; we’ll be updating accountRelationField to accountRelationFields so it’s not limited to being singular.

Could you elaborate on what you mean with “delete” here?

As an example, there is a pretty common use case with social apps e.g., where a user may want to like and then unlike a post. There are other scenarios like in the original post where someone may want to delete their review, post, etc.

At the Ceramic network layer:
It’s worth clarifying that underneath the hood, this doesn’t actually remove a stream from the Ceramic network. After discussing with the team, we’ve decided better phrasing might be to say that the stream becomes “unindexed.” In the context of the “set” feature, “unsetting” something would be “unindexing” it. I.e., The stream becomes marked with an unindexed flag.

At the indexing layer:
Different indexers (and later applications) may choose to handle this unindexed flag in different ways. Some may choose to reflect the spirit of the flag and delete/hide data. Or there may be some use cases where the unindexed data is chosen to be retained, perhaps for historical or audit reasons as you suggested.

Hope this helps!

1 Like