Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mastic

logo

license-mit repo-stars conventional-commits

ci


About

Mastic is a federated social platform fully compatible with Mastodon and the Fediverse via ActivityPub, running entirely on the Internet Computer as Rust WASM canisters.


Documentation

Architecture

System architecture, canister design, and sequence diagrams for all major flows:

ActivityPub

ActivityPub protocol reference and Mastic-specific mapping:

Candid Interfaces

Canonical Candid interface definitions for each canister:

Project Specification

  • Project Spec - User stories, milestones, and interface definitions

Mastic Architecture Documentation

Scope

This document outlines the architecture of Mastic, with a focus on the core components and their interactions with the users and the Fediverse.

Architecture Overview

block-beta
    columns 2
    Alice (("Alice")):2

    block:mastic
        columns 2
          fe["Frontend"]:2
          uc["User Canister"]
          dir["Directory Canister"]
          fed["Federation Canister"]:2
    end

    space

    mastodon("Mastodon Web2"):2

    Bob(("Bob")):2

    Alice --> mastic
    mastic --> mastodon
    mastodon --> Bob

Flows

Create Profile

sequenceDiagram
    actor A as Alice
    participant II as Internet Identity
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant IC as IC Management Canister

    A->>II: Sign In with II
    A->>DIR: Create Profile (candid)
    DIR->>DIR: Add map between user and handle
    DIR->>DIR: Start worker to create user canister
    DIR->>IC: create_canister
    IC-->>DIR: Canister ID
    DIR->>IC: install_code (User Canister WASM)
    IC-->>UC: Install Canister
    DIR->>DIR: Store User Canister Principal for Alice
    A->>DIR: Get user canister Principal
    DIR->>A: Principal of User Canister

Sign In

sequenceDiagram
    actor A as Alice
    participant II as Internet Identity
    participant DIR as Directory Canister

    A->>II: Sign In with II
    A->>DIR: Get User Canister (candid)
    DIR->>A: Return Canister ID

Update Profile

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: Update Profile (candid)
    UC->>UC: Update Profile in User Canister

Delete Profile

Note: The Federation Canister must buffer the Delete activity data before the User Canister is destroyed, since the User Canister will no longer exist to serve actor profile requests after deletion.

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant IC as IC Management Canister
    participant M as Mastodon Web2

    A->>DIR: Delete profile (candid)
    DIR->>A: Ok
    DIR->>DIR: Create tombstone for Alice
    DIR->>DIR: Start delete canister worker
    DIR->>UC: Notify Delete
    UC->>UC: Aggregate notification based on followers
    UC->>FED: Send Delete Activity
    FED->>FED: Buffer Delete activity data
    FED->>DIR: Route Delete to local followers
    DIR->>DIR: Resolve local follower User Canisters
    FED->>M: Forward Delete Activity to remote followers
    UC->>DIR: Activity Sent
    DIR->>IC: stop_canister + delete_canister
    IC-->>DIR: Canister Deleted

Follow User

Local follow (both users on Mastic)

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant BUC as Bob's User Canister

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: follow_user (candid)
    UC->>FED: Send Follow Activity
    FED->>DIR: Resolve Bob's User Canister
    DIR->>FED: Bob's User Canister Principal
    FED->>BUC: Deliver Follow activity
    BUC->>BUC: Record follower (Alice)
    BUC->>FED: Send Accept Activity
    FED->>DIR: Resolve Alice's User Canister
    FED->>UC: Deliver Accept activity
    UC->>UC: Record following (Bob)

Remote follow (target on external Fediverse instance)

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant M as Mastodon Web2

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: follow_user (candid)
    UC->>FED: Send Follow Activity
    FED->>M: Forward Follow Activity (ActivityPub / HTTP Signature)
    M->>M: Record follower (Alice)
    M->>FED: Send Accept Activity (ActivityPub)
    FED->>DIR: Resolve Alice's User Canister
    FED->>UC: Deliver Accept activity
    UC->>UC: Record following (remote user)

Unfollow User

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant BUC as Bob's User Canister (local)
    participant M as Mastodon Web2

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: unfollow_user (candid)
    UC->>UC: Remove following (Bob)
    UC->>FED: Send Undo(Follow) Activity
    alt Bob is local
        FED->>DIR: Resolve Bob's User Canister
        FED->>BUC: Deliver Undo(Follow) activity
        BUC->>BUC: Remove follower (Alice)
    else Bob is remote
        FED->>M: Forward Undo(Follow) Activity (ActivityPub)
    end

Block User

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant BUC as Bob's User Canister (local)
    participant M as Mastodon Web2

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: block_user (candid)
    UC->>UC: Record block locally
    UC->>FED: Send Block Activity
    alt Bob is local
        FED->>DIR: Resolve Bob's User Canister
        FED->>BUC: Deliver Block activity
        BUC->>BUC: Hide Alice's profile from Bob
    else Bob is remote
        FED->>M: Forward Block Activity (ActivityPub)
    end

Create Status

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant BUC as Bob's User Canister (local)
    participant M as Mastodon Web2
    actor B as Bob (remote)

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: Create Status (candid)
    UC->>UC: Store Status in Alice's Outbox
    UC->>UC: Aggregate Create activity for each follower
    UC->>FED: Forward Create Status Activities (ic)
    FED->>DIR: Resolve local followers
    DIR->>FED: Local follower User Canister principals
    FED->>BUC: Deliver Create activity to local follower inboxes
    FED->>M: Forward Create activities to remote instances (ActivityPub)
    B->>M: Get Feed
    M->>B: Return Alice's Status

Like Status

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant TUC as Target User Canister (local)
    participant M as Mastodon Web2

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: Like Status (candid)
    UC->>UC: Store Like in Alice's Outbox
    UC->>FED: Forward Like Activity (ic)
    alt Status author is local
        FED->>DIR: Resolve author's User Canister
        FED->>TUC: Deliver Like activity
    else Status author is remote
        FED->>M: Forward Like Activity (ActivityPub)
    end

Boost Status

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant TUC as Target User Canister (local)
    participant M as Mastodon Web2

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: Boost Status (candid)
    UC->>UC: Store Boost in Alice's Outbox
    UC->>FED: Forward Announce Activity (ic)
    alt Status author is local
        FED->>DIR: Resolve author's User Canister
        FED->>TUC: Deliver Announce activity
    else Status author is remote
        FED->>M: Forward Announce Activity (ActivityPub)
    end
    Note over FED: Also delivers to Alice's followers (local + remote)

Delete Status

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant M as Mastodon Web2

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: Delete Status (candid)
    UC->>UC: Remove Status from Alice's Outbox
    UC->>FED: Forward Delete Status Activity (ic)
    FED->>DIR: Resolve local followers
    FED->>FED: Deliver Delete to local follower inboxes
    FED->>M: Forward Delete Activity to remote instances (ActivityPub)

Read Feed

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister

    A->>DIR: Get Alice's User Canister (candid)
    DIR->>A: User Canister Principal
    A->>UC: Read Feed page (candid)
    UC->>UC: Aggregate feed from Alice's Inbox and Outbox
    UC->>A: Return Feed

Receive Updates from Fediverse

sequenceDiagram
    actor A as Alice
    participant UC as Alice's User Canister
    participant DIR as Directory Canister
    participant FED as Federation Canister
    participant M as Mastodon Web2
    actor B as Bob

    B->>M: Publish Status
    M->>M: Get who Follows Bob
    M->>FED: Dispatch create Status Activity for Alice
    FED->>DIR: Get User Canister for Alice
    DIR->>FED: User Canister ID
    FED->>UC: Put Status to Alice's Inbox
    A->>UC: Read feed
    UC->>A: Return Bob's Post

ActivityPub on Mastic

This module provides a technical overview with simple diagrams of the ActivityPub protocol, which is used for federated social networking.

The diagrams illustrate the flow of activities between actors in a federated network, showing how they interact with each other through various endpoints, and so what it has to be implemented in order to support ActivityPub on Mastic.

Mapping to Mastic Architecture

The following table shows how core ActivityPub concepts map to Mastic’s canister-based architecture:

ActivityPub ConceptMastic ComponentNotes
ActorUser CanisterEach Mastic user is represented by a dedicated User Canister that acts as their ActivityPub Actor.
Inbox / OutboxUser CanisterThe actor’s inbox and outbox collections are stored in the User Canister. They are exposed to the Fediverse via HTTP endpoints served by the Federation Canister.
Social API (C2S)Candid calls to User CanisterInstead of HTTP-based Client-to-Server interactions, Mastic users interact with their User Canister through authenticated Candid calls, using Internet Identity for authentication.
Federation Protocol (S2S)Federation CanisterAll Server-to-Server HTTP traffic is handled by the Federation Canister, which receives incoming activities and forwards outgoing activities to remote instances.
HTTP SignaturesUser Canister (key storage) + Federation Canister (signing/verification)Each User Canister generates and stores an RSA key pair at creation time. The Federation Canister uses the private key to sign outgoing requests and serves the public key when the actor profile is requested.
WebFingerFederation CanisterWebFinger lookups (/.well-known/webfinger) are handled by the Federation Canister’s http_request query method, which resolves account handles to actor URIs via the Directory Canister.

Federation Canister HTTP Endpoints

The Federation Canister serves the following HTTP routes to enable ActivityPub federation and discovery:

MethodRouteDescription
GET/.well-known/webfingerWebFinger lookup — resolves acct: URIs to actor profiles via the Directory Canister
GET/users/{handle}Actor profile — returns the JSON-LD representation of the actor
GET/users/{handle}/inboxActor inbox — returns the inbox as an OrderedCollection
POST/users/{handle}/inboxReceive activities from remote instances (S2S) — validates HTTP Signatures and delivers to the User Canister
GET/users/{handle}/outboxActor outbox — returns the outbox as an OrderedCollection
GET/users/{handle}/followersFollowers collection — returns the actor’s followers as an OrderedCollection
GET/users/{handle}/followingFollowing collection — returns the actors followed by this actor as an OrderedCollection
GET/users/{handle}/likedLiked collection — returns the activities liked by this actor as an OrderedCollection

All GET endpoints are served by the Federation Canister’s http_request query method. The POST /users/{handle}/inbox endpoint is handled by http_request_update since it requires state changes (delivering activities to User Canisters).

Objects

All objects in ActivityPub are represented as JSON-LD documents. The objects can be of various types, such as Person, Note, Create, Like, etc.

Each object MUST have:

  • id: The object’s unique global identifier (unless the object is transient, in which case the id MAY be omitted).
  • type: The type of the object.

and can have various properties such as to, actor, and content.

Retrieving objects

Servers MUST present the ActivityStreams object representation in response to application/ld+json; profile="https://www.w3.org/ns/activitystreams", and SHOULD also present the ActivityStreams representation in response to application/activity+json as well.

The client MUST specify an Accept header with the application/ld+json; profile="https://www.w3.org/ns/activitystreams" media type in order to retrieve the activity.

Source

The Object also contains the source attribute, which has been originally used to derive the content:

{
  "content": "<p>I <em>really</em> like strawberries!</p>",
  "source": {
    "content": "I *really* like strawberries!",
    "mediaType": "text/markdown"
    }
}

Actors

Actors are the entities that perform actions in the ActivityPub protocol. They can be users, applications, or services. Each actor has a unique identifier and can have various properties such as name, icon, and preferred language.

Actors are represented as JSON-LD documents with the type set to Person, Application, or other types defined in the ActivityStreams vocabulary.

Each actor MUST, in addition to the properties for the Objects, have the following properties:

  • inbox: (OrderedCollection) The URL of the actor’s inbox, where it receives activities.
  • outbox: (OrderedCollection) The URL of the actor’s outbox, where it sends activities.
  • following: (OrderedCollection) An Url to an ActivityStreams collection that contains the actors that this actor is following.
  • followers: (OrderedCollection) An Url to an ActivityStreams collection that contains the actors that are following this actor.
  • liked: (OrderedCollection) An Url to an ActivityStreams collection that contains the activities that this actor has liked.
{
  "@context": ["https://www.w3.org/ns/activitystreams", { "@language": "ja" }],
  "type": "Person",
  "id": "https://kenzoishii.example.com/",
  "following": "https://kenzoishii.example.com/following.json",
  "followers": "https://kenzoishii.example.com/followers.json",
  "liked": "https://kenzoishii.example.com/liked.json",
  "inbox": "https://kenzoishii.example.com/inbox.json",
  "outbox": "https://kenzoishii.example.com/feed.json",
  "preferredUsername": "kenzoishii",
  "name": "石井健蔵",
  "summary": "この方はただの例です",
  "icon": ["https://kenzoishii.example.com/image/165987aklre4"]
}

Inbox and Outbox

Every actor has both an inbox and an outbox. The inbox is where the actor receives activities from other actors, while the outbox is where the actor sends activities to other actors.

From an implementation perspective, both the Inbox and the Outbox, are OrderedCollection objects.

block-beta
    columns 7

    U (["User"])
    space:2

    block:queues
    columns 1
    IN ("Inbox")
    OUT ("Outbox")
    end

    space:2

    RO ("Rest of the world")

    U -- "POST messages to" --> OUT
    IN -- "GET messages from" --> U
    RO -- "POST messages to" --> IN
    OUT -- "GET messages from" --> RO

Actor data:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Person",
  "id": "https://social.example/alice/",
  "name": "alice P. Hacker",
  "preferredUsername": "alice",
  "summary": "Lisp enthusiast hailing from MIT",
  "inbox": "https://social.example/alice/inbox/",
  "outbox": "https://social.example/alice/outbox/",
  "followers": "https://social.example/alice/followers/",
  "following": "https://social.example/alice/following/",
  "liked": "https://social.example/alice/liked/"
}

Now let’s say Alice wants to send a message to Bob. The following diagram illustrates the flow of this activity:

block-beta
    columns 13

    A (["Alice"])
    space

    block:aliq
        columns 1
        AIN ("Inbox")
        AOUT ("Outbox")
    end

    space

    AS ("Alice's Server")

    space:2

    BS ("Bob's Server")

    space

    block:bobq
        columns 1
        BIN ("Inbox")
        BOUT ("Outbox")
    end

    space:2

    B ("Bob")

    A -- "POST message" --> AOUT
    AOUT --> AS
    AS -- "POST message to" --> BS
    BS --> BIN
    BIN -- "GET message" --> B

First Alice sends a message to her outbox:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Note",
  "to": ["https://chatty.example/ben/"],
  "attributedTo": "https://social.example/alice/",
  "content": "Say, did you finish reading that book I lent you?"
}

Then Alice’s server creates the post and forwards the message to Bob’s server:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "id": "https://social.example/alice/posts/a29a6843-9feb-4c74-a7f7-081b9c9201d3",
  "to": ["https://chatty.example/ben/"],
  "actor": "https://social.example/alice/",
  "object": {
    "type": "Note",
    "id": "https://social.example/alice/posts/49e2d03d-b53a-4c4c-a95c-94a6abf45a19",
    "attributedTo": "https://social.example/alice/",
    "to": ["https://chatty.example/ben/"],
    "content": "Say, did you finish reading that book I lent you?"
  }
}

Later after Bob has answered, Alice can fetch her inbox with a GET and see the answer to that message:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "id": "https://chatty.example/ben/p/51086",
  "to": ["https://social.example/alice/"],
  "actor": "https://chatty.example/ben/",
  "object": {
    "type": "Note",
    "id": "https://chatty.example/ben/p/51085",
    "attributedTo": "https://chatty.example/ben/",
    "to": ["https://social.example/alice/"],
    "inReplyTo": "https://social.example/alice/posts/49e2d03d-b53a-4c4c-a95c-94a6abf45a19",
    "content": "<p>Argh, yeah, sorry, I'll get it back to you tomorrow.</p><p>I was reviewing the section on register machines,since it's been a while since I wrote one.</p>"
  }
}

Further interactions can be made, such as liking the reply:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Like",
  "id": "https://social.example/alice/posts/5312e10e-5110-42e5-a09b-934882b3ecec",
  "to": ["https://chatty.example/ben/"],
  "actor": "https://social.example/alice/",
  "object": "https://chatty.example/ben/p/51086"
}

And this will follow the same flow as before.

Activity Streams

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://www.w3.org/ns/activitystreams",
  "type": "Collection"
}

ActivityStreams defines the collection concept; ActivityPub defines several collections with special behavior. Note that ActivityPub makes use of ActivityStreams paging to traverse large sets of objects.

Note that some of these collections are specified to be of type OrderedCollection specifically, while others are permitted to be either a Collection or an OrderedCollection. An OrderedCollection MUST be presented consistently in reverse chronological order.

Public collections

Some collections are marked as Public

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://www.w3.org/ns/activitystreams#Public",
  "type": "Collection"
}

And MUST be accessible to anyone, regardless of whether they are authenticated or not.

Protocol

The protocol is based on HTTP and uses JSON-LD for data representation.

Two APIs are defined:

  • Social API: It’s a client-to-server API that allows clients to interact with the server, such as creating posts, following users, and liking content.
  • Federation Protocol: It’s a server-to-server API that allows servers to exchange activities with each other, such as sending posts, following users, and liking content.

Mastic is implemented as a ActivityPub conformant Federated Server, with a significant variation.

While the Federation Protocol is implemented with HTTP, the Social API is implemented using the Internet Computer’s native capabilities, such as update calls and query calls, and calls are so authenticated using the Internet Computer’s Internet Identity.

Social API

In the standard ActivityPub specification, client-to-server (C2S) interaction takes place through clients posting Activities to an actor’s outbox via HTTP POST requests. Mastic replaces this HTTP-based C2S layer with typed Candid methods on the User Canister.

Instead of discovering an outbox URL and POSTing JSON-LD payloads to it, Mastic users call specific Candid methods on their User Canister, such as publish_status, like_status, follow_user, boost_status, block_user, etc. Each method accepts a typed Candid argument struct and returns a typed response. The User Canister internally manages the actor’s outbox, appending the corresponding ActivityPub activity for each operation.

Requests MUST be authenticated using the Internet Computer’s Internet Identity — the caller’s principal must match the owner principal configured at canister install time.

The User Canister handles the same side effects that the ActivityPub spec requires for outbox operations:

  • The server (User Canister) MUST generate a new id for each Activity.
  • The server MUST remove bto and/or bcc properties before delivery, but MUST utilize the addressing originally stored on these properties for determining recipients.
  • The server MUST add the new Activity to the outbox collection.
  • Depending on the type of Activity, the server may carry out further side effects as described per individual Activity below.

Client Addressing

Clients are responsible for addressing new Activities appropriately. To some extent, this is dependent upon the particular client implementation, but clients must be aware that the server will only forward new Activities to addressees in the to, bto, cc, bcc, and audience fields.

Create Activity

The Create activity is used when posting a new object. This has the side effect that the object embedded within the Activity (in the object property) is created.

When a Create activity is posted, the actor of the activity SHOULD be copied onto the object’s attributedTo field.

For client to server posting, it is possible to submit an object for creation without a surrounding activity. The server MUST accept a valid ActivityStreams object that isn’t a subtype of Activity in the POST request to the outbox. The server then MUST attach this object as the object of a Create Activity. For non-transient objects, the server MUST attach an id to both the wrapping Create and its wrapped Object.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Note",
  "content": "This is a note",
  "published": "2015-02-10T15:04:55Z",
  "to": ["https://example.org/~john/"],
  "cc": ["https://example.com/~erik/followers",
         "https://www.w3.org/ns/activitystreams#Public"]
}

Is equivalent to this and both MUST be accepted by the server:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "id": "https://example.net/~mallory/87374",
  "actor": "https://example.net/~mallory",
  "object": {
    "id": "https://example.com/~mallory/note/72",
    "type": "Note",
    "attributedTo": "https://example.net/~mallory",
    "content": "This is a note",
    "published": "2015-02-10T15:04:55Z",
    "to": ["https://example.org/~john/"],
    "cc": ["https://example.com/~erik/followers",
           "https://www.w3.org/ns/activitystreams#Public"]
  },
  "published": "2015-02-10T15:04:55Z",
  "to": ["https://example.org/~john/"],
  "cc": ["https://example.com/~erik/followers",
         "https://www.w3.org/ns/activitystreams#Public"]
}

Update Activity

The Update activity is used when updating an already existing object. The side effect of this is that the object MUST be modified to reflect the new structure as defined in the update activity, assuming the actor has permission to update this object.

Usually updates are partial.

Delete Activity

The Delete activity is used to delete an already existing object

The side effect of this is that the server MAY replace the object with a Tombstone of the object that will be displayed in activities which reference the deleted object. If the deleted object is requested the server SHOULD respond with either the Gone status code if a Tombstone object is presented as the response body, otherwise respond with a NotFound.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://example.com/~alice/note/72",
  "type": "Tombstone",
  "published": "2015-02-10T15:04:55Z",
  "updated": "2015-02-10T15:04:55Z",
  "deleted": "2015-02-10T15:04:55Z"
}

Follow Activity

The Follow activity is used to subscribe to the activities of another actor.

Add Activity

Upon receipt of an Add activity into the outbox, the server SHOULD add the object to the collection specified in the target property, unless:

  • the target is not owned by the receiving server, and thus they are not authorized to update it.
  • the object is not allowed to be added to the target collection for some other reason, at the receiving server’s discretion.

Remove Activity

Upon receipt of a Remove activity into the outbox, the server SHOULD remove the object from the collection specified in the target property, unless:

  • the target is not owned by the receiving server, and thus they are not authorized to update it.
  • the object is not allowed to be removed from the target collection for some other reason, at the receiving server’s discretion.

Like Activity

The Like activity indicates the actor likes the object.

The side effect of receiving this in an outbox is that the server SHOULD add the object to the actor’s liked Collection.

Block Activity

The Block activity is used to indicate that the posting actor does not want another actor (defined in the object property) to be able to interact with objects posted by the actor posting the Block activity. The server SHOULD prevent the blocked user from interacting with any object posted by the actor.

Servers SHOULD NOT deliver Block Activities to their object.

Undo Activity

The Undo activity is used to undo a previous activity. See the Activity Vocabulary documentation on Inverse Activities and “Undo”. For example, Undo may be used to undo a previous Like, Follow, or Block. The undo activity and the activity being undone MUST both have the same actor. Side effects should be undone, to the extent possible. For example, if undoing a Like, any counter that had been incremented previously should be decremented appropriately.

There are some exceptions where there is an existing and explicit “inverse activity” which should be used instead. Create based activities should instead use Delete, and Add activities should use Remove.

Delivery

Federated servers MUST perform delivery on all Activities posted to the outbox according to outbox delivery.

Federation Protocol

Servers communicate with other servers and propagate information across the social graph by posting activities to actors’ inbox endpoints. An Activity sent over the network SHOULD have an id, unless it is intended to be transient (in which case it MAY omit the id).

POST requests (eg. to the inbox) MUST be made with a Content-Type of application/ld+json; profile="https://www.w3.org/ns/activitystreams" and GET requests (see also 3.2 Retrieving objects) with an Accept header of application/ld+json; profile="https://www.w3.org/ns/activitystreams".

Servers SHOULD interpret a Content-Type or Accept header of application/activity+json as equivalent to application/ld+json; profile="https://www.w3.org/ns/activitystreams“ for server-to-server interactions.

In order to propagate updates throughout the social graph, Activities are sent to the appropriate recipients. First, these recipients are determined through following the appropriate links between objects until you reach an actor, and then the Activity is inserted into the actor’s inbox (delivery). This allows recipient servers to:

  1. conduct any side effects related to the Activity (for example, notification that an actor has liked an object is used to update the object’s like count);
  2. deliver the Activity to recipients of the original object, to ensure updates are propagated to the whole social graph (see inbox delivery).

Delivery is usually triggered by, for example:

  • an Activity being created in an actor’s outbox with their Followers Collection as the recipient.
  • an Activity being created in an actor’s outbox with directly addressed recipients.
  • an Activity being created in an actors’s outbox with user-curated collections as recipients.
  • an Activity being created in an actor’s outbox or inbox which references another object.

Servers performing delivery to the inbox or sharedInbox properties of actors on other servers MUST provide the object property in the activity: Create, Update, Delete, Follow, Add, Remove, Like, Block, Undo. Additionally, servers performing server to server delivery of the following activities MUST also provide the target property: Add, Remove.

An activity is delivered to its targets (which are actors) by first looking up the targets’ inboxes and then posting the activity to those inboxes. Targets for delivery are determined by checking the ActivityStreams audience targeting; namely, the to, bto, cc, bcc, and audience fields of the activity.

Server Side Activities

Just follow 1:1 the document described here:

https://www.w3.org/TR/activitypub/#create-activity-inbox

Mastodon

Note: Not all Mastodon extensions described in this section will be implemented in the initial milestones. Features such as pinned posts (Featured Collection), Flag/reporting, and Move/account migration are documented here for completeness and future reference. See the milestone plan in docs/project.md for the implementation timeline.

Statuses Federation

In Mastodon statuses are posts, aka toots, of the type of Notes of the ActivityPub protocol.

Mastodon supports the following activities for Statuses:

  • Create: Transformed into a status and saved into database
  • Delete: Delete a Status from the database
  • Like: Favourited a Status
  • Announce: Boost a status (like rt on Twitter)
  • Undo: Undo a Like or a Boost
  • Flag: Transformed into a report to the moderation team. See the Reports extension for more information
  • QuoteRequest: Request approval for a quote post. See the Quote Posts extension

Payloads

The first-class Object types supported by Mastodon are Note and Question.

  • Notes are transformed into regular statuses.
  • Questions are transformed into a poll status. See the Polls extension for more information.

HTML Sanitization

https://docs.joinmastodon.org/spec/activitypub/#sanitization.

Status Properties

These are the properties used:

  • content: status text content
  • name: Used as status text, if content is not provided on a transformed Object type
  • summary: Used as CW (Content warning) text
  • sensitive: Used to determine whether status media or text should be hidden by default. See the Sensitive content extension section for more information about as:sensitive
  • inReplyTo: Used for threading a status as a reply to another status
  • published: status published date
  • url: status permalink
  • attributedTo: Used to determine the profile which authored the status
  • to/cc: Used to determine audience and visibility of a status, in combination with mentions. See Mentions for adddressing and notifications.
  • tag: Used to mark up mentions and hashtags.
    • type: Either Mention, Hashtag, or Emoji is currently supported. See the Hashtag and Custom emoji extension sections for more information.
    • name: The plain-text Webfinger address of a profile Mention (@user or @user@domain), or the plain-text Hashtag (#tag), or the custom Emoji shortcode (:thounking:)
    • href: The URL of the actor or tag
  • attachment: Used to include attached images, videos, or audio
    • url: Used to fetch the media attachment
    • summary: Used as media description alt
    • blurhash: Used to generate a blurred preview image corresponding to the colors used within the image. See Blurhash for more details
  • replies: A Collection of statuses that are in reply to the current status. Up to 5 replies from the same server will be fetched upon discovery of a remote status, in order to resolve threads more fully. On Mastodon’s side, the first page contains self-replies, and additional pages contain replies from other people.
  • likes: A Collection used to represent Like activities received for this status. The actual activities are not exposed by Mastodon at this time.
    • totalItems: The number of likes this status has received
  • shares: A Collection used to represent Announce activities received for this status. The actual activities are not exposed by Mastodon at this time.
    • totalItems: The number of Announce activities received for this status.

Poll specific properties

  • endTime: The timestamp for when voting will close on the poll
  • closed: The timestamp for when voting closed on the poll. The timestamp will likely match the endTime timestamp. If this property is present, the poll is assumed to be closed.
  • votersCount: How many people have voted in the poll. Distinct from how many votes have been cast (in the case of multiple-choice polls)
  • oneOf: Single-choice poll options
    • name: The poll option’s text
    • replies:
      • totalItems: The poll option’s vote count
  • anyOf: Multiple-choice poll options
    • name: The poll option’s text
    • replies:
      • totalItems: The poll option’s vote count

Profiles Federation

Profiles are represented as Person objects in ActivityPub, and they are used to represent users on the platform. Mastodon supports the following activities for profiles:

  • Follow: Indicate interest in receiving status updates from a profile.
  • Accept/Reject: Used to approve or deny Follow activities. Unlocked accounts will automatically reply with an Accept, while locked accounts can manually choose whether to approve or deny a follow request.
  • Add/Remove: Manage pinned posts and featured collections.
  • Update: Refresh account details
  • Delete: Remove an account from the database, as well as all of their statuses.
  • Undo: Undo a previous Follow, Accept Follow, or Block.
  • Block: Signal to a remote server that they should hide your profile from that user. Not guaranteed.
  • Flag: Report a user to their moderation team. See the Reports extension for more information
  • Move: Migrate followers from one account to another. Requires alsoKnownAs to be set on the new account pointing to the old account

Profile Properties

  • preferredUsername: Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI.
  • name: Used as profile display name.
  • summary: Used as profile bio.
  • type: Assumed to be Person. If type is Application or Service, it will be interpreted as a bot flag.
  • url: Used as profile link.
  • icon: Used as profile avatar.
  • image: Used as profile header.
  • manuallyApprovesFollowers: Will be shown as a locked account.
  • discoverable: Will be shown in the profile directory.
  • indexable: Posts by this account can be indexed for full-text search
  • publicKey: Required for signatures. See Public Key for more information.
  • featured: Pinned posts. See Featured collection
  • attachment: Used for profile fields. See Profile metadata.
  • alsoKnownAs: Required for Move activity
  • published: When the profile was created.
  • memorial: Whether the account is a memorial account. See Memorial Flag for more information.
  • suspended: Whether the account is currently suspended. See Suspended Flag for more information.
  • attributionDomains: Domains allowed to use fediverse:creator for this actor in published articles.

Reports Extension

To report profiles and/or posts on remote servers, Mastodon will send a Flag activity from the instance actor. The object of this activity contains the user being reported, as well as any posts attached to the report. If a comment is attached to the report, it will be used as the content of the activity.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://mastodon.example/ccb4f39a-506a-490e-9a8c-71831c7713a4",
  "type": "Flag",
  "actor": "https://mastodon.example/actor",
  "content": "Please take a look at this user and their posts",
  "object": [
    "https://example.com/users/1",
    "https://example.com/posts/380590",
    "https://example.com/posts/380591"
  ],
  "to": "https://example.com/users/1"
}

Sensitive Extension

Mastodon uses the as:sensitive extension property to mark certain posts as sensitive. When a post is marked as sensitive, any media attached to it will be hidden by default, and if a summary is present, the status content will be collapsed behind this summary. In Mastodon, this is known as a content warning.

Hashtag

Similar to the Mention subtype of Link already defined in ActivityStreams, Mastodon will use Hashtag as a subtype of Link in order to surface posts referencing some common topic identified by a string key. The Hashtag has a name containing the #hashtag microsyntax – a # followed by a string sequence representing a topic. This is similar to the @mention microsyntax, where an @ is followed by some string sequence representing a resource (where in Mastodon’s case, this resource is expected to be an account). Mastodon will also normalize hashtags to be case-insensitive lowercase strings, performing ASCII folding and removing invalid characters.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "Hashtag": "https://www.w3.org/ns/activitystreams#Hashtag"
    }
  ],
  "id": "https://example.com/some-post",
  "type": "Note",
  "attributedTo": "https://example.com",
  "content": "I love #cats",
  "tag": [
    {
      "type": "Hashtag",
      "name": "#cats",
      "href": "https://example.com/tagged/cats"
    }
  ]
}

Custom Emoji

Mastodon supports arbitrary emojis by including a tag of the Emoji type. Handling of custom emojis is similar to handling of mentions and hashtags, where the name of the tagged entity is found as a substring of the natural language properties (name, summary, content) and then linked to the local representation of some resource or topic. In the case of emoji shortcodes, the name is replaced by the HTML for an inline image represented by the icon property (where icon.url links to the image resource).

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "Emoji": "http://joinmastodon.org/ns#Emoji",
    }
  ],

  "id": "https://example.com/@alice/hello-world",
  "type": "Note",
  "content": "Hello world :kappa:",
  "tag": [
    {
      "id": "https://example.com/emoji/123",
      "type": "Emoji",
      "name": ":kappa:",
      "icon": {
        "type": "Image",
        "mediaType": "image/png",
        "url": "https://example.com/files/kappa.png"
      }
    }
  ]
}

Focal Points

Mastodon supports setting a focal point on uploaded images, so that wherever that image is displayed, the focal point stays in view. This is implemented using an extra property focalPoint on Image objects. The property is an array of two floating points between -1.0 and 1.0, with 0,0 being the center of the image, the first value being x (-1.0 is the left edge, +1.0 is the right edge) and the second value being y (-1.0 is the bottom edge, +1.0 is the top edge).

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "focalPoint": {
        "@container": "@list",
        "@id": "http://joinmastodon.org/ns#focalPoint"
      }
    }
  ],

  "id": "https://example.com/@alice/hello-world",
  "type": "Note",
  "content": "A picture attached!",
  "attachment": [
    {
      "type": "Image",
      "mediaType": "image/png",
      "url": "https://example.com/files/cats.png",
      "focalPoint": [
        -0.55,
        0.43
      ]
    }
  ]
}

Quote Posts

Mastodon implements experimental support for handling remote quote posts according to FEP-044f. Additionally, it understands quoteUri, quoteUrl and _misskey_quote for compatibility.

Should a post contain multiple quotes, Mastodon only accepts the first one.

Furthermore, Mastodon does not handle the full range of interaction policies, but instead converts the authorized followers to a combination of “public”, “followers” and “unknown”, defaulting to “nobody”.

At this time, Mastodon does not offer authoring quotes, nor does it expose a quote policy, or produce stamps for incoming quote requests.

Discoverability Flag

Mastodon allows users to opt-in or opt-out of discoverability features like the profile directory. This flag may also be used as an indicator of the user’s preferences toward being included in external discovery services. If you are implementing such a tool, it is recommended that you respect this property if it is present. This is implemented using an extra property discoverable on objects mapping to profiles.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "discoverable": "http://joinmastodon.org/ns#discoverable"
    }
  ],
  "id": "https://mastodon.social/users/Gargron",
  "type": "Person",
  "discoverable": true
}

Indexable Flag

Mastodon allows users to opt-in or opt-out of indexing features like full-text search of public statuses. If you are implementing such a tool, it is recommended that you respect this property if it is present. This is implemented using an extra property indexable on objects mapping to profiles.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "indexable": "http://joinmastodon.org/ns#indexable"
    }
  ],
  "id": "https://mastodon.social/users/Gargron",
  "type": "Person",
  "indexable": true
}

Suspended Flag

Mastodon reports whether a user was locally suspended, for better handling of these accounts. Suspended accounts in Mastodon return empty data. If a remote account is marked as suspended, it cannot be unsuspended locally. Suspended accounts can be targeted by activities such as Update, Undo, Reject, and Delete. This functionality is implemented using an extra property suspended on objects.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "suspended": "http://joinmastodon.org/ns#suspended"
    }
  ],
  "id": "https://example.com/@eve",
  "type": "Person",
  "suspended": true
}

Memorial Flag

Mastodon reports whether a user’s profile was memorialized, for better handling of these accounts. Memorial accounts in Mastodon return normal data, but are rendered with a header indicating that the account is a memorial account. This functionality is implemented using an extra property memorial on objects.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "memorial": "http://joinmastodon.org/ns#memorial"
    }
  ],
  "id": "https://example.com/@alice",
  "type": "Person",
  "memorial": true
}

Polls

The ActivityStreams Vocabulary specification describes loosely (non-normatively) how a question might be represented. Mastodon’s implementation of polls is somewhat inspired by this section. The following implementation details can be observed:

Question is used as an Object type instead of as an IntransitiveActivity; rather than being sent directly, it is wrapped in a Create just like any other status.

Poll options are serialized using oneOf or anyOf as an array.

Each item in this array has no id, has a type of Note, and has a name representing the text of the poll option.

Each item in this array also has a replies property, representing the responses to this particular poll option. This node has no id, has a type of Collection, and has a totalItems property representing the total number of votes received for this option.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "votersCount": "http://joinmastodon.org/ns#votersCount"
    }
  ],
  "id": "https://mastodon.example/users/alice/statuses/1009947848598745",
  "type": "Question",
  "content": "What should I eat for breakfast today?",
  "published": "2023-03-05T07:40:13Z",
  "endTime": "2023-03-06T07:40:13Z",
  "votersCount": 7,
  "anyOf": [
    {
      "type": "Note",
      "name": "apple",
      "replies": {
        "type": "Collection",
        "totalItems": 3
      }
    },
    {
      "type": "Note",
      "name": "orange",
      "replies": {
        "type": "Collection",
        "totalItems": 7
      }
    },
    {
      "type": "Note",
      "name": "banana",
      "replies": {
        "type": "Collection",
        "totalItems": 6
      }
    }
  ]
}

Poll votes are serialized as Create activities, where the object is a Note with a name that exactly matches the name of the poll option. The Note.inReplyTo points to the URI of the Question object.

For multiple-choice polls, multiple activities may be sent. Votes will be counted if you have not previously voted for that option.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://mastodon.example/users/bob#votes/827163/activity",
  "to": "https://mastodon.example/users/alice",
  "actor": "https://mastodon.example/users/bob",
  "type": "Create",
  "object": {
    "id": "https://mastodon.example/users/bob#votes/827163",
    "type": "Note",
    "name": "orange",
    "attributedTo": "https://mastodon.example/users/bob",
    "to": "https://mastodon.example/users/alice",
    "inReplyTo": "https://mastodon.example/users/alice/statuses/1009947848598745"
  }
}
{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://mastodon.example/users/bob#votes/827164/activity",
  "to": "https://mastodon.example/users/alice",
  "actor": "https://mastodon.example/users/bob",
  "type": "Create",
  "object": {
    "id": "https://mastodon.example/users/bob#votes/827164",
    "type": "Note",
    "name": "banana",
    "attributedTo": "https://mastodon.example/users/bob",
    "to": "https://mastodon.example/users/alice",
    "inReplyTo": "https://mastodon.example/users/alice/statuses/1009947848598745"
  }
}

Mentions

In the ActivityStreams Vocabulary, Mention is a subtype of Link that is intended to represent the microsyntax of @mentions. The tag property is intended to add references to other Objects or Links. For Link tags, the name of the Link should be a substring of the natural language properties (name, summary, content) on that object. Wherever such a substring is found, it can be transformed into a hyperlink reference to the href.

However, Mastodon also uses Mention tags for addressing in some cases. Based on the presence or exclusion of Mention tags, and compared to the explicitly declared audiences in to and cc, Mastodon will calculate a visibility level for the post. Additionally, Mastodon requires Mention tags in order to generate a notification. (The mentioned actor must still be included within to or cc explicitly in order to receive the post.)

  • public: Public statuses have the as:Public magic collection in to
  • unlisted: Unlisted statuses have the as:Public magic collection in cc
  • private: Followers-only statuses have an actor’s follower collection in to or cc, but do not include the as:Public magic collection
  • limited: Limited-audience statuses have actors in to or cc, at least one of which is not Mentioned in tag
  • direct: Mentions-only statuses have actors in to or cc, all of which are Mentioned in tag

Public Key

Public keys are used for HTTP Signatures and Linked Data Signatures. This is implemented using an extra property publicKey on actor objects. See HTTP Signatures for more information.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1"
  ],
  "id": "https://mastodon.social/users/Gargron",
  "type": "Person",
  "publicKey": {
    "id": "https://mastodon.social/users/Gargron#main-key",
    "owner": "https://mastodon.social/users/Gargron",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXc4vkECU2/CeuSo1wtn\nFoim94Ne1jBMYxTZ9wm2YTdJq1oiZKif06I2fOqDzY/4q/S9uccrE9Bkajv1dnkO\nVm31QjWlhVpSKynVxEWjVBO5Ienue8gND0xvHIuXf87o61poqjEoepvsQFElA5ym\novljWGSA/jpj7ozygUZhCXtaS2W5AD5tnBQUpcO0lhItYPYTjnmzcc4y2NbJV8hz\n2s2G8qKv8fyimE23gY1XrPJg+cRF+g4PqFXujjlJ7MihD9oqtLGxbu7o1cifTn3x\nBfIdPythWu5b4cujNsB3m3awJjVmx+MHQ9SugkSIYXV0Ina77cTNS0M2PYiH1PFR\nTwIDAQAB\n-----END PUBLIC KEY-----\n"
  }
}

Blurhash

Mastodon generates colorful preview thumbnails for attachments. This is implemented using an extra property blurhash on Image objects. The property is a string generated by the BlurHash algorithm.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "blurhash": "http://joinmastodon.org/ns#blurhash"
    }
  ],

  "id": "https://example.com/@alice/hello-world",
  "type": "Note",
  "content": "A picture attached!",
  "attachment": [
    {
      "type": "Image",
      "mediaType": "image/png",
      "url": "https://example.com/files/cats.png",
      "blurhash": "UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH"
    }
  ]
}

What is known in Mastodon as “pinned statuses”, or statuses that are always featured at the top of people’s profiles, is implemented using an extra property featured on the actor object that points to a Collection of objects.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "featured": {
        "@id": "http://joinmastodon.org/ns#featured",
        "@type": "@id"
      }
    }
  ],

  "id": "https://example.com/@alice",
  "type": "Person",
  "featured": "https://example.com/@alice/collections/featured"
}

Mastodon allows users to feature specific hashtags on their profile for easy browsing, as a discoverability mechanism. This is implemented using an extra property featuredTags on the actor object that points to a Collection of Hashtag objects specifically.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "featuredTags": {
        "@id": "http://joinmastodon.org/ns#featuredTags",
        "@type": "@id"
      }
    }
  ],

  "id": "https://example.com/@alice",
  "type": "Person",
  "featuredTags": "https://example.com/@alice/collections/tags"
}

Profile Metadata

Mastodon supports arbitrary profile fields containing name-value pairs. This is implemented using the attachment property on actor objects, with objects in the array having a type of PropertyValue and a value property, both from the schema.org namespace.

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "schema": "http://schema.org#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value"
    }
  ],
  "id": "https://mastodon.social/users/Gargron",
  "type": "Person",
  "attachment": [
    {
      "type": "PropertyValue",
      "name": "Patreon",
      "value": "<a href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span}"
    },
    {
      "type": "PropertyValue",
      "name": "Homepage",
      "value": "<a href=\"https://zeonfederated.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">zeonfederated.com</span><span class=\"invisible\"></span}"
    }
  ]
}

Account Migration

Mastodon uses the Move activity to signal that an account has migrated to a different account. For the migration to be considered valid, Mastodon checks that the new account has defined an alias pointing to the old account (via the alsoKnownAs property).

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://mastodon.example/users/alice#moves/1",
  "actor": "https://mastodon.example/users/alice",
  "type": "Move",
  "object": "https://mastodon.example/users/alice",
  "target": "https://alice.com/users/109835986274379",
  "to": "https://mastodon.example/users/alice/followers"
}

Remote Blocking

ActivityPub defines the Block activity for client-to-server (C2S) use-cases, but not for server-to-server (S2S) – it recommends that servers SHOULD NOT deliver Block activities to their object. However, Mastodon will send this activity when a local user blocks a remote user. When Mastodon receives a Block activity where the object is an actor on the local domain, it will interpret this as a signal to hide the actor’s profile and posts from the local user, as well as disallowing mentions of that actor by that local user.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://mastodon.example/bd06bb61-01e0-447a-9dc8-95915db9aec8",
  "type": "Block",
  "actor": "https://mastodon.example/users/alice",
  "object": "https://example.com/~mallory",
  "to": "https://example.com/~mallory"
}

HTTP Signatures

HTTP Signatures are used to authenticate requests between servers (S2S) part of the Federation Protocol.

In particular, in the Public data for each user there is a publicKey property that contains the public key of the user. This public key is used to verify the signature of the request.

When a user is created on the server, the server generates and stores securely the private key of the user.

When the server sends an activity to the inbox of another server, the request MUST be signed with the private key of the user.

The server receiving the request MUST verify the signature using the public key of the user.

Mastic implementation: In Mastic, each User Canister generates an RSA key pair at creation time. The private key is stored securely within the User Canister and is used by the Federation Canister to sign outgoing HTTP requests on behalf of the user. The public key is served by the Federation Canister when the actor profile is requested by a remote instance (e.g. to verify a signature).

For any HTTP request incoming to Mastodon for the Federation Protocol, the Signature header MUST be present and contain the signature of the request:

Signature: keyId="https://my.example.com/username#main-key",headers="(request-target) host date",signature="Y2FiYW...IxNGRiZDk4ZA=="

The three parts of the Signature: header can be broken down like so:

Signature:
  keyId="https://my.example.com/username#main-key",
  headers="(request-target) host date",
  signature="Y2FiYW...IxNGRiZDk4ZA=="

The keyId should correspond to the actor and the key being used to generate the signature, whose value is equal to all parameters in headers concatenated together and signed by the key, then Base64-encoded. See Public key for more information on actor keys.

An example key looks like this:

{
  "publicKey": {
    "id": "https://my.example.com/username#main-key",
    "owner": "https://my.example.com/username",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXc4vkECU2/CeuSo1wtn\nFoim94Ne1jBMYxTZ9wm2YTdJq1oiZKif06I2fOqDzY/4q/S9uccrE9Bkajv1dnkO\nVm31QjWlhVpSKynVxEWjVBO5Ienue8gND0xvHIuXf87o61poqjEoepvsQFElA5ym\novljWGSA/jpj7ozygUZhCXtaS2W5AD5tnBQUpcO0lhItYPYTjnmzcc4y2NbJV8hz\n2s2G8qKv8fyimE23gY1XrPJg+cRF+g4PqFXujjlJ7MihD9oqtLGxbu7o1cifTn3x\nBfIdPythWu5b4cujNsB3m3awJjVmx+MHQ9SugkSIYXV0Ina77cTNS0M2PYiH1PFR\nTwIDAQAB\n-----END PUBLIC KEY-----\n"
 }
}

Signing POST requests

When making a POST request to Mastodon, you must calculate the RSA-SHA256 digest hash of your request’s body and include this hash (in base64 encoding) within the Digest: header. The Digest: header must also be included within the headers parameter of the Signature: header. For example:

POST /users/username/inbox HTTP/1.1
HOST: mastodon.example
Date: 18 Dec 2019 10:08:46 GMT
Digest: sha-256=hcK0GZB1BM4R0eenYrj9clYBuyXs/lemt5iWRYmIX0A=
Signature: keyId="https://my.example.com/actor#main-key",headers="(request-target) host date digest",signature="Y2FiYW...IxNGRiZDk4ZA=="
Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "actor": "https://my.example.com/actor",
  "type": "Create",
  "object": {
    "type": "Note",
    "content": "Hello!"
  },
  "to": "https://mastodon.example/users/username"
}

Verifying Signatures

Mastodon verifies the signature using the following algorithm:

  1. Split Signature: into its separate parameters.
  2. Construct the signature string from the value of headers.
  3. Fetch the keyId and resolve to an actor’s publicKey.
  4. RSA-SHA256 hash the signature string and compare to the Base64-decoded signature as decrypted by publicKey[publicKeyPem].
  5. Use the Date: header to check that the signed request was made within the past 12 hours.

WebFinger

For fully-featured Mastodon support, Mastic also implements the WebFinger protocol, which is used to discover information about users and their profiles on the Fediverse. WebFinger is a protocol that allows clients to discover information about a user based on their account name or email address.

WebFinger Simple Flow

Suppose we want to lookup the user @Gargron hosted on the mastodon.social website.

Just make a request to that domain’s /.well-known/webfinger endpoint, with the resource query parameter set to an acct: URI (e.g. acct:veeso_dev@hachyderm.io).

For instance: https://hachyderm.io/.well-known/webfinger?resource=acct%3Aveeso_dev%40hachyderm.io

{
  "subject": "acct:veeso_dev@hachyderm.io",
  "aliases": [
    "https://hachyderm.io/@veeso_dev",
    "https://hachyderm.io/users/veeso_dev"
  ],
  "links": [
    {
      "rel": "http://webfinger.net/rel/profile-page",
      "type": "text/html",
      "href": "https://hachyderm.io/@veeso_dev"
    },
    {
      "rel": "self",
      "type": "application/activity+json",
      "href": "https://hachyderm.io/users/veeso_dev"
    },
    {
      "rel": "http://ostatus.org/schema/1.0/subscribe",
      "template": "https://hachyderm.io/authorize_interaction?uri={uri}"
    },
    {
      "rel": "http://webfinger.net/rel/avatar",
      "type": "image/png",
      "href": "https://media.hachyderm.io/accounts/avatars/114/410/957/328/747/476/original/1cc6bed1aa3ad81e.png"
    }
  ]
}

Interface

This directory contains the Candid interface definitions for the various canisters in the Mastic project.

  • Directory

    service : (DirectoryInstallArgs) -> {
      add_moderator : (AddModeratorArgs) -> (AddModeratorResponse);
      delete_profile : () -> (DeleteProfileResponse);
      get_user : (GetUserArgs) -> (GetUserResponse) query;
      remove_moderator : (RemoveModeratorArgs) -> (RemoveModeratorResponse);
      search_profiles : (SearchProfilesArgs) -> (SearchProfilesResponse) query;
      sign_up : (text) -> (SignUpResponse);
      suspend : (SuspendArgs) -> (SuspendResponse);
      user_canister : (opt Principal) -> (UserCanisterResponse) query;
      whoami : () -> (WhoAmIResponse) query
    }
    
  • Federation

    service : (FederationInstallArgs) -> {
      http_request : (HttpRequest) -> (HttpResponse) query;
      http_request_update : (HttpRequest) -> (HttpResponse);
      send_activity : (SendActivityArgs) -> (SendActivityResponse)
    }
    
  • User

    service : (UserInstallArgs) -> {
      accept_follow : (AcceptFollowArgs) -> (AcceptFollowResponse);
      block_user : (BlockUserArgs) -> (BlockUserResponse);
      boost_status : (BoostStatusArgs) -> (BoostStatusResponse);
      delete_profile : () -> (DeleteProfileResponse);
      delete_status : (DeleteStatusArgs) -> (DeleteStatusResponse);
      follow_user : (FollowUserArgs) -> (FollowUserResponse);
      get_followers : (GetFollowersArgs) -> (GetFollowersResponse) query;
      get_following : (GetFollowingArgs) -> (GetFollowingResponse) query;
      get_liked : (GetLikedArgs) -> (GetLikedResponse) query;
      get_profile : () -> (GetProfileResponse) query;
      like_status : (LikeStatusArgs) -> (LikeStatusResponse);
      publish_status : (PublishStatusArgs) -> (PublishStatusResponse);
      read_feed : (ReadFeedArgs) -> (ReadFeedResponse) query;
      receive_activity : (ReceiveActivityArgs) -> (ReceiveActivityResponse);
      reject_follow : (RejectFollowArgs) -> (RejectFollowResponse);
      undo_boost : (UndoBoostArgs) -> (UndoBoostResponse);
      undo_like : (UndoLikeArgs) -> (UndoLikeResponse);
      unfollow_user : (UnfollowUserArgs) -> (UnfollowUserResponse);
      update_profile : (UpdateProfileArgs) -> (UpdateProfileResponse)
    }
    

Mastic

Unleashing the power of IC on the Fediverse

Introduction

Mastic aims to bring the Fediverse — a decentralised network of interconnected social platforms — natively onto the Internet Computer ecosystem.

The Fediverse operates through the ActivityPub protocol, enabling independent instances of the social platform on the network to communicate with one another, thereby creating a distributed social network.

The Mastic project brings the possibility both to have a node dedicated to the Internet Computer community that can finally have a dedicated fully decentralised social network, but also any people that wish to set up their own Mastic node on the Internet Computer, empowering the decentralisation of the Fediverse, bringing web3 functionalities on it, and with the ease the IC brings in spinning up network services.

Mission

Our mission is to build a fully federated, ActivityPub-compatible social platform running entirely on the Internet Computer protocol, enabling seamless participation in the Fediverse.

This project aims to bring a truly decentralised social experience to the IC ecosystem by integrating with an already thriving network of over 1.4 million monthly active users across the Fediverse.

In particular, it will help engage the vibrant community of Rust developers, many of whom are active on Mastodon, by bridging their preferred social environment with the technical and philosophical foundations of the Internet Computer.

Impact

Mastic has the potential to reshape the future of federated social platforms by demonstrating how a modern, scalable, and fully on-chain implementation of ActivityPub can be achieved using Rust and the Internet Computer ecosystem.

While Mastodon has played a central role in popularising the Fediverse, its current architecture — based on Ruby on Rails and multiple supporting services (PostgreSQL, Redis, Sidekiq, Nginx, etc.) — presents significant barriers to contribution and scalability.

The complexity of deploying and maintaining Mastodon discourages smaller communities and individual developers, and its ageing technology stack makes it less attractive to modern developers, particularly those in the Web3 and Rust ecosystems.

By reimagining an ActivityPub-compatible server as a set of autonomous, composable canisters written in Rust, this project removes those barriers: no external infrastructure, no ops burden, and a far more approachable codebase for developers already aligned with the values of decentralisation.

Mastic can not only attract a new wave of Rust and Web3 contributors to the Internet Computer but also act as a reference architecture for building scalable, open social infrastructure in the post-cloud era.

Additionally, this would finally bring authentication to the Internet Identity on the Fediverse.

Governance & Sustainability Model

Mastic is not just about a technical implementation of a decentralised social platform - it is a long-term vision for how open social platforms can be sustainably governed and evolved without relying on centralised control or ad revenue models.

To ensure sustainability and a decentralised governance model, Mastic will be deployed under a **Service Nervous System ** (SNS), making Mastic a fully on-chain DAO governed by its community.

DAO-Based Governance

We propose giving the DAO community governance for upgrades, feature proposals, federation decisions and moderation policies.

Token holders will be able to vote on proposals and allocate funds to the treasury.

Participants in the DAO will be able to apply for moderation and will be elected by the community.

Market Analysis

Total Addressable Market

Currently, Mastodon has 1.4 million active users per month.

Most of these are developers, journalists, artists and niche communities. In particular, Mastodon has experienced significant growth following Elon Musk’s acquisition of Twitter.

Of course, some users have left Twitter but have not joined Mastodon, either because they are using BlueSky or other platforms, such as Reddit or Discord groups.

Serviceable Available Market

Above 10-15% of the TAM

Our first target is:

  • Rust Developers: 2.3 million developers in 2024
  • Web3 Developers: about 25k developers in 2024
  • IC developers: ~3k

Serviceable Obtainable Market

A realistic target within the first year could be around 20,000 users, if we primarily focus on developers and web3 developers.

Differentiation from Traditional Mastodon Instances

Mastic introduces a fundamentally new paradigm for federated social platforms by reimagining the Mastodon experience as a fully on-chain, modular architecture built natively on the Internet Computer. While Mastodon has pioneered the Fediverse movement, its traditional deployment model — based on a monolithic Ruby on Rails stack supported by PostgreSQL, Redis, Sidekiq, and other infrastructure — imposes significant operational overhead and limits scalability.

By contrast, Mastic offers the following distinct advantages:

  • Fully on-chain architecture: Every component of Mastic runs within canisters on the Internet Computer, eliminating the need for traditional DevOps, hosting, and external databases.
  • Modularity and scalability: Each user operates within their User Canister, ensuring composability, privacy, and scalability by design.
  • Internet Identity integration: Users can sign in with Internet Identity, bringing native IC authentication to the Fediverse.
  • Decentralised governance: Instead of relying on a central instance admin, Mastic is governed by a DAO through the Service Nervous System (SNS), enabling transparent, community-driven decisions around moderation and feature development.
  • Developer-friendly technology stack: Mastic is built entirely in Rust, offering a modern, secure, and performant alternative that resonates with a growing community of Rust and Web3 developers.

These innovations make Mastic not only more accessible for developers and small communities but also more aligned with the philosophical foundations of the decentralised web.

User Acquisition Strategy

Mastic’s adoption strategy is designed to activate key communities aligned with its technical foundation and decentralised vision, and to establish a sustainable user base through targeted outreach and integrations within the Web3 ecosystem.

Our multi-phase growth approach includes:

  • Developer-first onboarding: We will bootstrap the platform by targeting Rust and Internet Computer developers, communities that already value decentralisation, composability, and performance. Early access, bounties, and contributor incentives will be designed to attract this group.
  • Fediverse-native advocacy: As a fully compatible ActivityPub implementation, Mastic will be promoted within the Fediverse itself. We aim to attract privacy-conscious users and instance operators looking for a lower-maintenance, modern alternative to traditional Mastodon deployments.

Architecture Overview

The architecture of Mastic consists of 3 components, each of which will be a standalone canister.

Frontend

The frontend of Mastic provides an interface where each user can sign in to Mastic and interact with the Fediverse by publishing Statuses and interacting with other users, including those from the Mastodon Fediverse on the web2. The User will be able to sign in with Internet Identity on the frontend and interact with the backend canisters using Candid.

User Canister

Each user has one User Canister. The User canister is created by the Directory Canister via the IC management canister every time a user signs up on Mastic, and is deleted whenever they delete their account or migrate to another Fediverse instance. The User Canister provides the interface for users to interact with the Fediverse through the Inbox and Outbox, implementing the core functionality of the ActivityPub protocol.

The User canister will use wasm-dbms to store data inside of a relational database.

Directory Canister

The Directory Canister provides an index for all existing users on Mastic by creating a Map between a user’s identity and their handle (e.g., @veeso@mastic.social) and their User Canister instance.

Federation Canister

The Federation canister implements the HTTP Web server to handle both incoming and outgoing requests of the Federation Protocol[1], which is used to communicate with the other instances on the Fediverse. The Federation canister MUST also implement the WebFinger[2] protocol to search for users.

Authorization Model

Mastic uses principal-based authorization, where each canister checks the caller’s principal against an expected set of principals configured at install time via init args.

  • User → User Canister: The caller’s principal must match the owner principal that was set when the User Canister was installed. This ensures only the canister owner can call methods like publish_status, update_profile, like_status, etc.
  • Federation Canister → User Canister: The User Canister stores the Federation Canister’s principal at install time (passed via UserInstallArgs). Only the Federation Canister can call receive_activity to deliver incoming activities from the Fediverse.
  • User Canister → Federation Canister: The Federation Canister stores the Directory Canister’s principal at install time. The Directory Canister registers each new User Canister principal with the Federation Canister, so the Federation Canister maintains a list of all authorised User Canister principals that can call send_activity.
  • Directory Canister: Moderator actions (add_moderator, remove_moderator, suspend) require the caller’s principal to be present in the moderator list. The initial moderator is set at install time via DirectoryInstallArgs.

Interface

Here’s a description of the Candid interface of the Directory, Federation and User canisters. Types are not yet defined, but only calls are provided to provide an overview of the flows that will need to be implemented. For more information on the flows, see the User Stories.

Directory Interface

service : (DirectoryInstallArgs) -> {

add_moderator : (AddModeratorArgs) -> (AddModeratorResponse);

delete_profile : () -> (DeleteProfileResponse);

get_user : (GetUserArgs) -> (GetUserResponse) query;

remove_moderator : (RemoveModeratorArgs) -> (RemoveModeratorResponse);

search_profiles : (SearchProfilesArgs) -> (SearchProfilesResponse) query;

sign_up : (text) -> (SignUpResponse);

suspend : (SuspendArgs) -> (SuspendResponse);

user_canister : (opt Principal) -> (UserCanisterResponse) query;

whoami : () -> (WhoAmIResponse) query

}

Federation Interface

service : (FederationInstallArgs) -> {

http_request : (HttpRequest) -> (HttpResponse) query;

http_request_update : (HttpRequest) -> (HttpResponse);

send_activity : (SendActivityArgs) -> (SendActivityResponse)

}

User Interface

service : (UserInstallArgs) -> {

accept_follow : (AcceptFollowArgs) -> (AcceptFollowResponse);

block_user : (BlockUserArgs) -> (BlockUserResponse);

boost_status : (BoostStatusArgs) -> (BoostStatusResponse);

delete_profile : () -> (DeleteProfileResponse);

delete_status : (DeleteStatusArgs) -> (DeleteStatusResponse);

follow_user : (FollowUserArgs) -> (FollowUserResponse);

get_followers : (GetFollowersArgs) -> (GetFollowersResponse) query;

get_following : (GetFollowingArgs) -> (GetFollowingResponse) query;

get_liked : (GetLikedArgs) -> (GetLikedResponse) query;

get_profile : () -> (GetProfileResponse) query;

like_status : (LikeStatusArgs) -> (LikeStatusResponse);

publish_status : (PublishStatusArgs) -> (PublishStatusResponse);

read_feed : (ReadFeedArgs) -> (ReadFeedResponse) query;

receive_activity : (ReceiveActivityArgs) -> (ReceiveActivityResponse);

reject_follow : (RejectFollowArgs) -> (RejectFollowResponse);

undo_boost : (UndoBoostArgs) -> (UndoBoostResponse);

undo_like : (UndoLikeArgs) -> (UndoLikeResponse);

unfollow_user : (UnfollowUserArgs) -> (UnfollowUserResponse);

update_profile : (UpdateProfileArgs) -> (UpdateProfileResponse)

}

User Stories

UC1: As a User, I should be able to create a Profile

  • Alice lands on Mastic Frontend
  • Alice signs in with her Internet Identity
  • Alice sends a sign_up call
  • The Directory Canister establishes a connection between Alice’s identity and her handle
  • The Directory Canister starts a worker to create her User Canister
  • The Directory Canister creates Alice’s User Canister via the IC management canister
  • The Directory Canister installs Alice’s User Canister via the IC management canister
  • The Directory Canister stores Alice’s User Canister ID for Alice’s Identity
  • Alice queries her User Canister ID with user_canister
  • The Directory Canister returns the ID to Alice

UC2: As a User, I should be able to Sign In

  • Alice lands on Mastic Frontend
  • Alice signs in with her Internet Identity
  • Alice queries the Directory Canister to get her user_canister with whoami
  • The Directory Canister returns her User Canister’s principal

UC3: As a User, I should be able to update my Profile

  • Alice signs in with her Internet Identity
  • Alice queries the Directory Canister with whoami to obtain her User Canister principal
  • Alice calls update_profile on her User Canister with the updated fields (display name, bio, avatar, etc.)
  • The User Canister persists the changes
  • The User Canister sends an Update activity to the Federation Canister so that remote followers receive the updated profile

UC4: As a User, I should be able to delete my Profile

  • Alice signs in with her Internet Identity
  • Alice calls delete_profile on the Directory Canister
  • The Directory Canister creates a tombstone for Alice and starts a delete worker
  • The Directory Canister notifies Alice’s User Canister, which aggregates a Delete activity for all of Alice’s followers
  • The User Canister sends the Delete activity to the Federation Canister
  • The Federation Canister buffers the activity data, then forwards it to all remote followers
  • The Directory Canister deletes Alice’s User Canister via the IC management canister

UC5: As a User, I should be able to follow another Profile

  • Alice signs in and obtains her User Canister principal via the Directory Canister
  • Alice calls follow_user on her User Canister with the target user’s handle
  • If the target is local: the User Canister resolves the handle via the Directory Canister, then sends a Follow activity to the target User Canister through the Federation Canister
  • If the target is remote: the User Canister sends a Follow activity to the Federation Canister, which forwards it to the remote instance via HTTP
  • When the target accepts, an Accept activity is delivered back, and Alice’s User Canister records the follow relationship

UC6: As a User, I should be able to remove a Following

  • Alice signs in and obtains her User Canister principal via the Directory Canister
  • Alice calls unfollow_user on her User Canister with the target user’s handle
  • The User Canister removes the follow relationship locally
  • The User Canister sends an Undo(Follow) activity through the Federation Canister to notify the target (local or remote) that Alice has unfollowed them

UC7: As a User, I should be able to see a user’s profile

  • Alice signs in and obtains her User Canister principal via the Directory Canister
  • Alice calls get_user on the Directory Canister with the target handle
  • The Directory Canister returns the target user’s User Canister principal
  • Alice calls get_profile on the target User Canister to retrieve their public profile information

UC8: As a User, I should be able to search for other users

  • Alice signs in and obtains her User Canister principal via the Directory Canister
  • Alice calls search_profiles on the Directory Canister with a search query
  • The Directory Canister returns a list of matching user handles and their User Canister principals

UC9: As a User, I should be able to create a Status

  • Alice signs in and obtains her User Canister principal via the Directory Canister
  • Alice calls publish_status on her User Canister with the status content
  • The User Canister stores the status in Alice’s outbox
  • The User Canister aggregates a Create(Note) activity for each follower of Alice
  • The User Canister sends the activities to the Federation Canister
  • The Federation Canister routes local activities through the Directory Canister to each local follower’s User Canister inbox
  • The Federation Canister forwards remote activities via HTTP to external Fediverse instances

UC10: As a User, I should be able to like a Status

  • Alice signs in and obtains her User Canister principal via the Directory Canister
  • Alice calls like_status on her User Canister with the status ID
  • The User Canister records the like in Alice’s outbox
  • The User Canister sends a Like activity to the Federation Canister
  • The Federation Canister routes the activity to the status author’s User Canister (local) or forwards it via HTTP (remote)

UC11: As a User, I should be able to boost a Status

  • Alice signs in and obtains her User Canister principal via the Directory Canister
  • Alice calls boost_status on her User Canister with the status ID
  • The User Canister records the boost in Alice’s outbox
  • The User Canister sends an Announce activity to the Federation Canister
  • The Federation Canister routes the activity to the status author and Alice’s followers, handling both local delivery (through the Directory Canister) and remote forwarding via HTTP

UC12: As a User, I should be able to read my Feed

  • Alice signs in and obtains her User Canister principal via the Directory Canister
  • Alice calls read_feed on her User Canister with pagination parameters
  • The User Canister aggregates the feed from Alice’s inbox (statuses from followed users) and outbox (Alice’s own statuses)
  • The User Canister returns the paginated feed to Alice

UC13: As a User, I should be able to receive updates from users I follow on other Fediverse instances

  • Bob publishes a status on a remote Mastodon instance
  • The remote instance dispatches a Create(Note) activity to the Federation Canister via HTTP
  • The Federation Canister verifies the HTTP Signature and resolves the target user via the Directory Canister
  • The Federation Canister calls receive_activity on Alice’s User Canister to deliver the status to her inbox
  • Alice reads her feed and sees Bob’s status

UC14: As a User on a Web2 Mastodon Instance, I should be able to receive updates and interact with users on Mastic

  • Alice publishes a status on Mastic
  • The Federation Canister forwards the Create(Note) activity via HTTP to Bob’s remote Mastodon instance
  • Bob sees Alice’s status in his feed
  • Bob likes or replies to Alice’s status
  • The remote instance sends the corresponding activity (Like, Create(Note) reply) to the Federation Canister via HTTP
  • The Federation Canister routes the activity to Alice’s User Canister

UC15: As a Moderator, I should be able to remove a Status violating the policy

  • A Moderator signs in with their Internet Identity
  • The Moderator identifies a status that violates the instance policy
  • The Moderator calls delete_status on the offending User Canister (authorised by the moderator principal stored in the Directory Canister)
  • The User Canister removes the status from the user’s outbox
  • The User Canister sends a Delete activity through the Federation Canister to notify followers

UC16: As a Moderator, I should be able to suspend a Profile violating the policy

  • A Moderator signs in with their Internet Identity
  • The Moderator calls suspend on the Directory Canister with the offending user’s handle
  • The Directory Canister marks the user as suspended, preventing further API calls
  • The Directory Canister notifies the User Canister, which sends a Delete activity through the Federation Canister to notify remote followers that the account is no longer active

Milestones

These are the milestones we plan to achieve during the first year of Mastic’s development cycle.

Milestone 0 - Proof of concept

Duration: 1.5 months

First implementation to demo the social platform.

Only basic functionalities are implemented, such as signing up, posting statuses, and reading the feed.

User stories:

  • UC1
  • UC2
  • UC9
  • UC5
  • UC12
  • UC7

Milestone 1 - Standalone Mastic Node

Duration: 3 months

Mastic is set up; we build all the data structures required by the user to sign up, operate on statuses (Create, Delete, like, and Boost), and find and follow other users on the Mastic node.

This Milestone won’t include the integration with the Fediverse yet, but it will provide an already usable Social Network, ready to be integrated onto the Fediverse soon.

These stories must be implemented during this phase:

  • UC3
  • UC4
  • UC6
  • UC8
  • UC10
  • UC11
  • UC15
  • UC16

Milestone 2 - Frontend

Duration: 2 months

Build the Mastic web frontend as a React application deployed as an IC asset canister. The frontend provides Internet Identity authentication and a Mastodon-like interface covering all user stories from Milestones 0 and 1.

These stories must be implemented during this phase:

  • UC1
  • UC2
  • UC3
  • UC4
  • UC5
  • UC6
  • UC7
  • UC8
  • UC9
  • UC10
  • UC11
  • UC12
  • UC15
  • UC16

Milestone 3 - Integrating the Fediverse

Duration: 2 months

This step requires implementing the Federation Canister using the Federation Protocol.

This will allow Mastic to be fully integrated with the Fediverse ecosystem.

These user stories must be implemented during this phase:

  • UC13
  • UC14

Milestone 4 - SNS Launch

Duration: 1 month

During this phase, we plan to launch Mastic on the SNS to add a fully decentralised governance to Mastic.

We need to implement a comprehensive integration for voting, specifically for adding and removing moderators and updating policies.

Reference

  1. ActivityPub: https://www.w3.org/TR/activitypub/
  2. ActivityStreams: https://www.w3.org/TR/activitystreams-core/
  3. Mastodon ActivityPub Spec: https://docs.joinmastodon.org/spec/activitypub/
  4. ActivityPub Federation framework implemented with Rust: https://docs.rs/activitypub_federation/0.6.5/activitypub_federation/
  5. Webfinger: https://docs.joinmastodon.org/spec/webfinger/

Milestones

Implementation plans for each Mastic development milestone.

Milestone 0 - Proof of Concept

Duration: 1.5 months

Goal: First implementation to demo the social platform with basic functionalities: signing up, posting statuses, following users, and reading the feed.

User Stories: UC1, UC2, UC5, UC7, UC9, UC12

Work Items

WI-0.1: Implement ActivityPub types in the activitypub crate

Description: Implement all ActivityPub and ActivityStreams protocol types in the activitypub crate (crates/libs/activitypub). This crate provides the canonical Rust representation of the ActivityPub protocol used by the Federation Canister for S2S communication and JSON-LD serialization/deserialization. Types must round-trip correctly through serde_json and match the JSON-LD payloads documented in docs/src/activitypub.md.

What should be done:

  • Core types (activitypub::object):
    • Object — base type with id, type, content, name, summary, published, updated, url, to, cc, bto, bcc, audience, attributed_to, in_reply_to, source, tag, attachment, replies, likes, shares, sensitive
    • Sourcecontent + media_type
    • Tombstoneid, type, published, updated, deleted
    • ObjectType enum — Note, Question, Image, Tombstone, etc.
  • Actor types (activitypub::actor):
    • Actor — extends Object with inbox, outbox, following, followers, liked, preferred_username, public_key, endpoints, manually_approves_followers, discoverable, indexable, suspended, memorial, featured, featured_tags, also_known_as, attribution_domains, icon, image
    • ActorType enum — Person, Application, Service, Group, Organization
    • PublicKeyid, owner, public_key_pem
    • Endpointsshared_inbox
  • Activity types (activitypub::activity):
    • Activity — extends Object with actor, object, target, result, origin, instrument
    • ActivityType enum — Create, Update, Delete, Follow, Accept, Reject, Like, Announce, Undo, Block, Add, Remove, Flag, Move
  • Collection types (activitypub::collection):
    • Collectionid, type, total_items, first, last, current, items
    • OrderedCollection — same as Collection with ordered_items
    • CollectionPage / OrderedCollectionPagepart_of, next, prev, items/ordered_items
  • Link types (activitypub::link):
    • Linkhref, rel, media_type, name, hreflang, height, width
    • Mention — subtype of Link
    • Hashtag — subtype of Link
  • Tag types (activitypub::tag):
    • Tag enum — Mention, Hashtag, Emoji
    • Emojiid, name, icon (Image with url and media_type)
  • Mastodon extensions (activitypub::mastodon):
    • PropertyValuename, value (for profile metadata fields)
    • Poll support on Question objects: end_time, closed, voters_count, one_of/any_of with name + replies.total_items
    • Attachment properties: blurhash, focal_point
  • WebFinger types (activitypub::webfinger):
    • WebFingerResponsesubject, aliases, links
    • WebFingerLinkrel, type, href, template
  • JSON-LD context (activitypub::context):
    • Constants for standard context URIs (https://www.w3.org/ns/activitystreams, https://w3id.org/security/v1, Mastodon namespace http://joinmastodon.org/ns#)
    • Context type for @context serialization (single URI, array, or map)
  • All types derive serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq
  • Use #[serde(rename_all = "camelCase")] to match JSON-LD field naming
  • Use #[serde(skip_serializing_if = "Option::is_none")] for optional fields
  • Use #[serde(rename = "@context")] for the context field

Acceptance Criteria:

  • All types compile and are exported from the activitypub crate
  • serde_json round-trip tests for every top-level type
  • Deserialization tests using real-world Mastodon JSON-LD payloads from docs/src/activitypub.md examples
  • cargo clippy passes with zero warnings
  • Unit tests cover: Object, Actor, each ActivityType, Collection, OrderedCollection, CollectionPage, WebFinger, Mention, Hashtag, Emoji

WI-0.2: Define shared Candid types in the did crate

Description: Define all shared Candid types required by Milestone 0 in the did crate. These types are used across the Directory, Federation, and User canisters.

What should be done:

  • Define DirectoryInstallArgs (initial moderator principal, federation canister principal)
  • Define UserInstallArgs (owner principal, federation canister principal)
  • Define FederationInstallArgs (directory canister principal, domain name)
  • Define sign-up types: SignUpResponse
  • Define whoami types: WhoAmIResponse
  • Define user canister query types: UserCanisterResponse
  • Define get-user types: GetUserArgs, GetUserResponse
  • Define profile types: GetProfileResponse, UserProfile (handle, display name, bio, avatar URL, created at)
  • Define follow types: FollowUserArgs, FollowUserResponse, AcceptFollowArgs, AcceptFollowResponse, RejectFollowArgs, RejectFollowResponse
  • Define status types: PublishStatusArgs, PublishStatusResponse, Status (id, content, author, created at, visibility)
  • Define feed types: ReadFeedArgs, ReadFeedResponse, FeedItem
  • Define get-followers/following types: GetFollowersArgs, GetFollowersResponse, GetFollowingArgs, GetFollowingResponse
  • Define SendActivityArgs, SendActivityResponse for the Federation canister
  • Define ReceiveActivityArgs, ReceiveActivityResponse for the User canister
  • Ensure all types derive CandidType, Deserialize, Serialize, Clone, Debug

Acceptance Criteria:

  • All types compile and are exported from the did crate
  • Types match the .did interface files in docs/interface/
  • cargo clippy passes with zero warnings
  • Unit tests verify serialization/deserialization round-trips

WI-0.3: Design and implement database schema for Milestone 0

Description: Design the relational database schema using wasm-dbms for all entities required by Milestone 0 in the Directory and User canisters. Since wasm-dbms manages its own stable memory, ic-stable-structures cannot be used alongside it in these canisters. Canister init arguments and runtime configuration are persisted in a settings key-value table instead. The Federation Canister does not use wasm-dbms and uses ic-stable-structures directly (see WI-0.10).

What should be done:

  • Add ic-dbms-canister as a workspace dependency
  • Create crates/libs/db-utils crate:
    • Define SettingKey as a u32 newtype with named constants per canister (e.g., FEDERATION_PRINCIPAL, OWNER_PRINCIPAL, DOMAIN_NAME)
    • Define SettingValue enum wrapping ic-dbms-canister Value variants (Text, Integer, Blob) with typed accessor methods (as_text(), as_principal(), etc.)
    • Provide helper functions for reading/writing settings rows
    • Add the crate to the workspace in root Cargo.toml
  • Shared settings table (both canisters):
    • settings table: key (INTEGER PK), value (depends on key — TEXT, INTEGER, or BLOB)
    • Uses SettingKey constants from db-utils to identify entries
  • Directory Canister schema:
    • settings table — stores federation_principal from DirectoryInstallArgs
    • The initial moderator from DirectoryInstallArgs is inserted as the first row in the moderators table during init
    • users table: principal (PRINCIPAL PK), handle (TEXT UNIQUE NOT NULL), user_canister_id (PRINCIPAL NOT NULL), status (TEXT NOT NULL DEFAULT ‘active’), created_at (INTEGER NOT NULL)
    • moderators table: principal (PRINCIPAL PK), added_at (INTEGER NOT NULL)
    • Index on users.handle for fast lookups
  • User Canister schema:
    • settings table — stores owner_principal, federation_principal from UserInstallArgs
    • profile table (single-row): handle (TEXT NOT NULL), display_name (TEXT), bio (TEXT), avatar_url (TEXT), header_url (TEXT), created_at (INTEGER NOT NULL), updated_at (INTEGER NOT NULL)
    • statuses table: id (TEXT PK), content (TEXT NOT NULL), visibility (TEXT NOT NULL DEFAULT ‘public’), created_at (INTEGER NOT NULL)
    • inbox table: id (TEXT PK), activity_type (TEXT NOT NULL), actor_uri (TEXT NOT NULL), object_json (TEXT NOT NULL), created_at (INTEGER NOT NULL)
    • followers table: actor_uri (TEXT PK), created_at (INTEGER NOT NULL)
    • following table: actor_uri (TEXT PK), status (TEXT NOT NULL DEFAULT ‘pending’), created_at (INTEGER NOT NULL)
    • keypair table (single-row): public_key_pem (TEXT NOT NULL), private_key_pem (TEXT NOT NULL)
    • Indexes on statuses.created_at, inbox.created_at for feed ordering
  • Initialize the schema in each canister’s init function and persist init args into the settings table
  • Data survives canister upgrades via wasm-dbms stable memory management

Acceptance Criteria:

  • All tables are created on canister initialization
  • Init args are persisted in the settings table and retrievable after upgrade
  • db-utils crate compiles and is usable from both canisters
  • Schema supports all queries needed by Milestone 0 work items
  • Data persists across canister upgrades
  • Unit tests verify table creation and basic CRUD operations

WI-0.4: Implement Directory Canister - sign-up flow

Description: Implement the sign_up method on the Directory Canister, which creates a new User Canister for the caller and maps their principal to a handle and canister ID.

What should be done:

  • Use the database schema from WI-0.3 (users, moderators, settings tables)

  • Implement init to accept DirectoryInstallArgs, create the schema, and persist init args into the settings table

  • Implement sign_up(handle):

    • Validate handle format (alphanumeric, lowercase, 1-30 chars)
    • Check handle uniqueness
    • Create a new User Canister via the IC management canister (ic_cdk::api::management_canister::main::create_canister)
    • Install the User Canister WASM via ic_cdk::api::management_canister::main::install_code
    • Store the mapping (principal -> handle, canister ID)
    • Register the new User Canister with the Federation Canister
    • Return SignUpResponse with the canister ID Acceptance Criteria:
  • Calling sign_up with a valid handle creates a User Canister and returns its principal

  • Duplicate handles are rejected

  • Duplicate sign-ups from the same principal are rejected

  • Invalid handles are rejected with a descriptive error

  • The user record is persisted across canister upgrades

  • Integration test: sign up, then verify the canister exists and is callable

WI-0.5: Implement Directory Canister - query methods

Description: Implement the read-only query methods on the Directory Canister that allow users to discover their canister and look up other users.

What should be done:

  • Implement whoami() query: return the caller’s UserRecord (handle + canister ID) or an error if not registered
  • Implement user_canister(opt principal) query: return the User Canister ID for the given principal (or the caller if None)
  • Implement get_user(GetUserArgs) query: look up a user by handle, return their public info (handle, canister ID)

Acceptance Criteria:

  • whoami returns the correct record for a registered user
  • whoami returns an error for an unregistered caller
  • user_canister(None) returns the caller’s canister
  • user_canister(Some(p)) returns the canister for principal p
  • get_user returns the correct user for a valid handle
  • get_user returns an error for a non-existent handle

WI-0.6: Implement User Canister - profile and state management

Description: Implement the User Canister’s internal state, initialization, and profile query method.

What should be done:

  • Use the database schema from WI-0.3 (settings, profile, statuses, inbox, followers, following, keypair tables)

  • Implement init to accept UserInstallArgs, create the schema, and persist init args into the settings table

  • Generate an RSA keypair for HTTP Signatures (store in keypair table)

  • Implement get_profile() query: return the user’s profile (handle, display name, bio, avatar, created at)

  • Implement authorization guard: reject calls from non-owner principals for owner-only methods Acceptance Criteria:

  • The User Canister initializes correctly with the provided args

  • get_profile returns the profile for any caller (public data)

  • Owner-only methods reject unauthorized callers

  • State survives canister upgrades

WI-0.7: Implement User Canister - publish status

Description: Implement the publish_status method, which stores a status in the user’s outbox and sends Create activities to followers via the Federation Canister.

What should be done:

  • Define status storage: a collection of Status records in the statuses table, keyed by a unique status ID (e.g., ULID or timestamp-based)
  • Implement publish_status(PublishStatusArgs):
    • Authorize the caller (owner only)
    • Create a Status record with unique ID, content, timestamp, visibility
    • Store the status in the outbox
    • For each follower, build a Create(Note) activity
    • Send activities to the Federation Canister via send_activity
  • Return PublishStatusResponse with the new status ID

Acceptance Criteria:

  • Only the owner can publish a status
  • The status is stored in the outbox with a unique ID
  • A Create(Note) activity is sent for each follower
  • The status ID is returned to the caller
  • Statuses persist across upgrades

WI-0.8: Implement User Canister - follow user

Description: Implement the follow_user, accept_follow, and reject_follow methods for managing follow relationships.

What should be done:

  • Implement follow_user(FollowUserArgs):
    • Authorize the caller (owner only)
    • Build a Follow activity targeting the given handle/actor URI
    • Send the activity to the Federation Canister via send_activity
    • Store a pending follow request locally
  • Implement accept_follow(AcceptFollowArgs):
    • Called by the Federation Canister when the target accepts
    • Add the requester to the followers list
    • Send an Accept(Follow) activity back via the Federation Canister
  • Implement reject_follow(RejectFollowArgs):
    • Called by the Federation Canister when the target rejects
    • Remove the pending follow request
    • Send a Reject(Follow) activity back via the Federation Canister
  • Implement receive_activity(ReceiveActivityArgs):
    • Authorize the caller (federation canister only)
    • Handle incoming Follow activities: auto-accept (for M0) and add to followers
    • Handle incoming Accept(Follow): add to following list
    • Handle incoming Create(Note): store in inbox

Acceptance Criteria:

  • follow_user sends a Follow activity and records a pending request
  • When an Accept is received, the target is added to the following list
  • When a Follow is received, the requester is added to the followers list
  • get_followers returns the correct follower list
  • get_following returns the correct following list
  • Only the Federation Canister can call receive_activity

WI-0.9: Implement User Canister - read feed

Description: Implement the read_feed method, which aggregates the user’s inbox and outbox into a chronological, paginated feed.

What should be done:

  • Implement read_feed(ReadFeedArgs):
    • Authorize the caller (owner only)
    • Merge inbox items (statuses from followed users) and outbox items (own statuses)
    • Sort by timestamp descending
    • Apply pagination (cursor-based or offset-based as defined in ReadFeedArgs)
    • Return ReadFeedResponse with the page of FeedItem records

Acceptance Criteria:

  • Feed contains both inbox and outbox items
  • Items are sorted by timestamp (newest first)
  • Pagination works correctly (returns the requested page size, provides a cursor/offset for the next page)
  • An empty feed returns an empty list (no error)
  • Only the owner can read their own feed

WI-0.10: Implement Federation Canister - activity routing

Description: Implement the Federation Canister’s send_activity method, which routes activities between local User Canisters via the Directory Canister. Remote HTTP delivery is out of scope for Milestone 0.

What should be done:

  • Define canister state using ic-stable-structures: directory canister principal, domain name, set of authorized User Canister principals (the Federation Canister does not use wasm-dbms)
  • Implement init to accept FederationInstallArgs and persist state in stable memory
  • Implement a method to register User Canister principals (called by the Directory Canister during sign-up)
  • Implement send_activity(SendActivityArgs):
    • Authorize the caller (must be a registered User Canister)
    • Parse the activity to determine the target actor(s)
    • For local targets: resolve the target User Canister via the Directory Canister, then call receive_activity on it
    • For remote targets: log/skip (federation is Milestone 2)
  • Return SendActivityResponse

Acceptance Criteria:

  • Only registered User Canisters can call send_activity
  • Local activities are correctly routed to the target User Canister
  • The Federation Canister resolves local handles via the Directory Canister
  • Remote targets are gracefully skipped (no crash)
  • Integration test: Alice follows Bob (both local), Bob sees Alice in followers

WI-0.11: Integration tests for Milestone 0 flows

Description: Write end-to-end integration tests using pocket-ic that exercise the complete Milestone 0 user flows.

What should be done:

  • Test UC1 (Create Profile): Deploy Directory + Federation canisters, call sign_up, verify the User Canister is created and callable
  • Test UC2 (Sign In): After sign-up, call whoami and verify the correct canister ID is returned
  • Test UC7 (View Profile): After sign-up, call get_user on the Directory, then get_profile on the User Canister
  • Test UC5 (Follow User): Two users sign up, Alice follows Bob, verify follower/following lists
  • Test UC9 (Create Status): Publish a status, verify it appears in the author’s outbox and is delivered to followers’ inboxes
  • Test UC12 (Read Feed): Publish multiple statuses from different users, verify the feed is correctly aggregated and paginated

Acceptance Criteria:

  • All six user story flows pass as integration tests
  • Tests run in CI via just integration_test
  • Tests use pocket-ic with realistic canister deployment
  • Each test is independent and can run in isolation

Milestone 1 - Standalone Mastic Node

Duration: 3 months

Goal: Build all remaining local-node features: profile management (update, delete), status interactions (like, boost, delete), user search, and moderation. After this milestone, Mastic is a fully usable social network within a single node, ready for Fediverse integration.

User Stories: UC3, UC4, UC6, UC8, UC10, UC11, UC15, UC16

Prerequisites: Milestone 0 completed.

Work Items

WI-1.1: Extend database schema for Milestone 1

Description: Extend the wasm-dbms schema to support all new entities introduced in Milestone 1: likes, boosts, blocks, tombstones, and status deletion tracking.

What should be done:

  • User Canister schema additions:
    • liked table: status_uri (TEXT PK), created_at (INTEGER NOT NULL)
    • blocks table: actor_uri (TEXT PK), created_at (INTEGER NOT NULL)
    • Add like_count (INTEGER DEFAULT 0) and boost_count (INTEGER DEFAULT 0) columns to the statuses table
    • Add is_boost (INTEGER DEFAULT 0) and original_status_uri (TEXT) columns to the inbox table for boost tracking
  • Directory Canister schema additions:
    • tombstones table: handle (TEXT PK), deleted_at (INTEGER NOT NULL), expires_at (INTEGER NOT NULL)
    • Add index on tombstones.expires_at for cleanup
  • Run schema migrations on canister upgrade (add new tables/columns without losing existing data)

Acceptance Criteria:

  • New tables and columns are created on upgrade from Milestone 0 schema
  • Existing data is preserved during migration
  • Unit tests verify migration from M0 schema to M1 schema
  • All new queries needed by M1 work items are supported

WI-1.2: Implement User Canister - update profile (UC3)

Description: Allow the user to update their profile fields and propagate the change to followers via an Update activity.

What should be done:

  • Implement update_profile(UpdateProfileArgs):
    • Authorize the caller (owner only)
    • Accept optional fields: display name, bio, avatar URL, header URL
    • Update the profile in stable memory (only fields that are Some)
    • Build an Update(Person) activity
    • Send the activity to the Federation Canister via send_activity so followers (local for now) receive the updated profile
  • Define UpdateProfileArgs and UpdateProfileResponse in the did crate if not already present

Acceptance Criteria:

  • Only the owner can update their profile
  • Partial updates work (e.g., updating only the bio leaves other fields unchanged)
  • The updated profile is returned by get_profile
  • An Update activity is sent to the Federation Canister
  • Integration test: update profile, verify get_profile returns new values

WI-1.3: Implement delete profile flow (UC4)

Description: Implement account deletion across Directory, User, and Federation canisters.

What should be done:

  • Directory Canister: Implement delete_profile():
    • Authorize the caller (must be a registered user)
    • Create a tombstone record for the user (prevents handle reuse for a grace period)
    • Notify the User Canister to aggregate Delete activities
    • After activities are sent, delete the User Canister via the IC management canister (stop_canister + delete_canister)
    • Remove the user record from the directory
  • User Canister: Implement delete_profile():
    • Authorize the caller (owner only)
    • Aggregate a Delete(Person) activity for all followers
    • Send activities to the Federation Canister
    • Return success
  • Federation Canister: Handle Delete(Person) activities:
    • Buffer the activity data before forwarding (the User Canister will be destroyed)
    • Route to local followers via the Directory Canister
    • For remote: skip (Milestone 2)
  • Define DeleteProfileResponse in the did crate

Acceptance Criteria:

  • Calling delete_profile on the Directory removes the user record
  • The User Canister is stopped and deleted via the IC management canister
  • A Delete activity is delivered to local followers
  • The deleted user’s handle cannot be reused immediately (tombstone)
  • whoami returns an error after deletion
  • get_user returns an error for the deleted handle
  • Integration test: create user, delete, verify canister is gone

WI-1.4: Implement User Canister - unfollow user (UC6)

Description: Allow a user to unfollow another user and notify the target via an Undo(Follow) activity.

What should be done:

  • Implement unfollow_user(UnfollowUserArgs):
    • Authorize the caller (owner only)
    • Remove the target from the following list
    • Build an Undo(Follow) activity
    • Send the activity to the Federation Canister
  • User Canister receive_activity handler: handle incoming Undo(Follow):
    • Remove the requester from the followers list
  • Define UnfollowUserArgs, UnfollowUserResponse in the did crate
  • Add Undo to ActivityType in the did crate

Acceptance Criteria:

  • After unfollowing, the target is removed from the following list
  • The target’s follower list no longer contains the caller
  • An Undo(Follow) activity is delivered to the target
  • Unfollowing a user you don’t follow returns a descriptive error
  • Integration test: follow, then unfollow, verify lists are updated

WI-1.5: Implement Directory Canister - search profiles (UC8)

Description: Implement the search_profiles method for user discovery.

What should be done:

  • Implement search_profiles(SearchProfilesArgs) query:
    • Accept a search query string and pagination parameters
    • Search by handle prefix or substring match
    • Return a paginated list of matching users (handle + canister ID)
  • Define SearchProfilesArgs, SearchProfilesResponse in the did crate

Acceptance Criteria:

  • Searching by exact handle returns the correct user
  • Searching by prefix returns all matching users
  • Empty query returns a paginated list of all users
  • Pagination works correctly
  • Results do not include suspended or deleted users
  • Integration test: create multiple users, search, verify results

WI-1.6: Implement User Canister - like status (UC10)

Description: Allow a user to like a status and notify the author.

What should be done:

  • Implement like_status(LikeStatusArgs):
    • Authorize the caller (owner only)
    • Record the like in the user’s liked collection (stable memory)
    • Build a Like activity targeting the status
    • Send the activity to the Federation Canister
  • Implement get_liked(GetLikedArgs) query:
    • Return the paginated list of statuses liked by the user
  • Implement undo_like(UndoLikeArgs):
    • Remove the like from the liked collection
    • Send an Undo(Like) activity to the Federation Canister
  • User Canister receive_activity handler: handle incoming Like:
    • Increment the like count on the target status
  • Handle incoming Undo(Like):
    • Decrement the like count on the target status
  • Define LikeStatusArgs, LikeStatusResponse, UndoLikeArgs, UndoLikeResponse, GetLikedArgs, GetLikedResponse in the did crate
  • Add Like to ActivityType

Acceptance Criteria:

  • Liking a status records it in the liked collection
  • A Like activity is sent to the status author
  • The author’s status like count is incremented
  • get_liked returns the correct list
  • Undoing a like removes it and sends an Undo(Like) activity
  • Cannot like the same status twice
  • Integration test: Alice likes Bob’s status, verify like count and liked list

WI-1.7: Implement User Canister - boost status (UC11)

Description: Allow a user to boost (reblog) a status and notify both the author and the user’s followers.

What should be done:

  • Implement boost_status(BoostStatusArgs):
    • Authorize the caller (owner only)
    • Record the boost in the user’s outbox
    • Build an Announce activity
    • Send the activity to the Federation Canister (targets: status author + all of the booster’s followers)
  • Implement undo_boost(UndoBoostArgs):
    • Remove the boost from the outbox
    • Send an Undo(Announce) activity
  • User Canister receive_activity handler: handle incoming Announce:
    • Store the boosted status in the inbox (as a boost, not a new status)
  • Handle incoming Undo(Announce):
    • Remove the boosted status from the inbox
  • Define BoostStatusArgs, BoostStatusResponse, UndoBoostArgs, UndoBoostResponse in the did crate
  • Add Announce to ActivityType

Acceptance Criteria:

  • Boosting a status records it in the outbox
  • An Announce activity is sent to the author and the booster’s followers
  • Followers see the boost in their feed
  • Undoing a boost removes it and sends an Undo(Announce) activity
  • Cannot boost the same status twice
  • Integration test: Alice boosts Bob’s status, Charlie (Alice’s follower) sees it in their feed

WI-1.8: Implement User Canister - delete status (UC15)

Description: Allow both the status owner and moderators to delete a status.

What should be done:

  • Implement delete_status(DeleteStatusArgs):
    • Authorize the caller: must be the owner or a moderator (the moderator list is resolved from the Directory Canister)
    • Remove the status from the outbox
    • Build a Delete(Note) activity
    • Send the activity to the Federation Canister to notify followers
  • Define DeleteStatusArgs, DeleteStatusResponse in the did crate
  • Add Delete to ActivityType if not already present

Acceptance Criteria:

  • The owner can delete their own status
  • A moderator can delete any user’s status
  • Non-owner, non-moderator callers are rejected
  • A Delete(Note) activity is sent to followers
  • The status no longer appears in feeds after deletion
  • Integration test: publish status, delete it, verify it’s gone from feeds

WI-1.9: Implement Directory Canister - moderation (UC16)

Description: Implement moderator management and user suspension on the Directory Canister.

What should be done:

  • Implement add_moderator(AddModeratorArgs):
    • Authorize the caller (must be an existing moderator)
    • Add the target principal to the moderator list
  • Implement remove_moderator(RemoveModeratorArgs):
    • Authorize the caller (must be an existing moderator)
    • Prevent removing the last moderator
    • Remove the target principal from the moderator list
  • Implement suspend(SuspendArgs):
    • Authorize the caller (must be a moderator)
    • Mark the user as suspended in the directory
    • Notify the User Canister to send a Delete activity to followers
    • Suspended users cannot call any methods on their User Canister
  • Define AddModeratorArgs, AddModeratorResponse, RemoveModeratorArgs, RemoveModeratorResponse, SuspendArgs, SuspendResponse in the did crate

Acceptance Criteria:

  • Only moderators can add/remove moderators
  • The last moderator cannot be removed
  • Suspending a user marks them as inactive in the directory
  • Suspended users cannot interact with their User Canister
  • A Delete activity is sent to the suspended user’s followers
  • search_profiles excludes suspended users
  • Integration test: add moderator, suspend user, verify user is locked out

WI-1.10: Implement User Canister - block user

Description: Allow a user to block another user, preventing interactions.

What should be done:

  • Implement block_user(BlockUserArgs):
    • Authorize the caller (owner only)
    • Record the block locally (block list in stable memory)
    • If the blocked user is a follower, remove them from the followers list
    • If the owner follows the blocked user, remove from following list
    • Send a Block activity to the Federation Canister
  • User Canister receive_activity handler: handle incoming Block:
    • Hide the blocking user’s content from the blocked user
  • Activities from blocked users should be silently dropped in receive_activity
  • Define BlockUserArgs, BlockUserResponse in the did crate
  • Add Block to ActivityType

Acceptance Criteria:

  • Blocking a user removes mutual follow relationships
  • Activities from a blocked user are dropped
  • A Block activity is sent via the Federation Canister
  • The blocked user does not appear in the blocker’s feeds
  • Integration test: Alice blocks Bob, verify follow removed and activities dropped

WI-1.11: Integration tests for Milestone 1 flows

Description: Write end-to-end integration tests for all Milestone 1 user stories.

What should be done:

  • Test UC3 (Update Profile): Update profile fields, verify changes
  • Test UC4 (Delete Profile): Delete account, verify canister removed
  • Test UC6 (Unfollow): Follow then unfollow, verify lists
  • Test UC8 (Search): Create users, search by prefix, verify results
  • Test UC10 (Like): Like a status, verify like count and liked list; undo like
  • Test UC11 (Boost): Boost a status, verify followers see it; undo boost
  • Test UC15 (Delete Status): Publish and delete status, verify removal
  • Test UC16 (Moderation): Add moderator, suspend user, verify lockout
  • Test Block: Block user, verify follow removal and activity filtering

Acceptance Criteria:

  • All user story flows pass as integration tests
  • Tests run in CI via just integration_test
  • Each test is independent and can run in isolation
  • Tests cover both success and error paths

Milestone 2 - Frontend

Duration: 2 months

Goal: Build the Mastic web frontend as a React application deployed as an IC asset canister. The frontend provides Internet Identity authentication and a Mastodon-like interface covering all user stories from Milestones 0 and 1. After this milestone, users can interact with the Mastic node entirely through a browser.

User Stories: UC1, UC2, UC3, UC4, UC5, UC6, UC7, UC8, UC9, UC10, UC11, UC12, UC15, UC16

Prerequisites: Milestone 1 completed.

Work Items

WI-2.1: Frontend project scaffold & Internet Identity authentication

Description: Set up the React project as an IC asset canister with Internet Identity sign-in, agent configuration, and basic app shell.

What should be done:

  • Initialize a React project (Vite + TypeScript) under crates/canisters/frontend/
  • Configure dfx.json with the frontend asset canister
  • Set up @dfinity/agent with actor factories generated from the .did files for Directory, Federation, and User canisters
  • Integrate @dfinity/auth-client for Internet Identity sign-in/sign-out
  • Implement sign-up page: handle input + call sign_up on the Directory Canister
  • Post-auth routing: call whoami to resolve the User Canister principal, store in app state
  • Basic app shell: navigation bar with auth status, client-side routing skeleton

Acceptance Criteria:

  • The frontend deploys as an asset canister via dfx deploy
  • Users can sign in with Internet Identity and sign out
  • New users can sign up by choosing a handle
  • After sign-in, the app resolves the User Canister principal and stores it in state
  • The navigation bar shows the authenticated user’s handle
  • Routing works for at least /, /sign-up, and a placeholder home page

WI-2.2: Feed view & status composer

Description: Build the main timeline view with paginated feed and a status composer for publishing new statuses.

What should be done:

  • Implement a feed page that calls read_feed on the User Canister and renders a paginated list of statuses
  • Build a status card component displaying: author handle, display name, avatar, content, timestamp, like count, boost count
  • Implement infinite scroll or “load more” pagination using the cursor returned by read_feed
  • Build a compose form: text input with character count + call publish_status on the User Canister
  • New statuses appear at the top of the feed after publishing

Acceptance Criteria:

  • The feed displays statuses from followed users and the user’s own statuses
  • Pagination loads additional statuses without reloading the page
  • Status cards show all required fields (author, content, timestamp, counts)
  • Publishing a status adds it to the feed
  • Empty feed shows a meaningful placeholder message

WI-2.3: Profile view & management

Description: Display user profiles and allow users to edit or delete their own profile.

What should be done:

  • Implement a profile page at /users/{handle}:
    • Call get_user on the Directory Canister to resolve the User Canister
    • Call get_profile on the target User Canister
    • Display: handle, display name, bio, avatar, header image, follower/following counts
  • Own profile: show an edit form for display name, bio, avatar URL, and header URL that calls update_profile on the User Canister
  • Delete account flow: confirmation dialog that calls delete_profile on the Directory Canister, then redirects to the landing page
  • Make author names/avatars in status cards clickable to navigate to the author’s profile

Acceptance Criteria:

  • Any user’s profile can be viewed by navigating to /users/{handle}
  • The own profile displays an edit button that opens the edit form
  • Updating a field persists the change (verified by reloading the profile)
  • Account deletion requires confirmation and redirects after success
  • Author links in the feed navigate to the correct profile page

WI-2.4: Follow, like & boost interactions

Description: Implement follow/unfollow, like/unlike, and boost/unboost UI interactions.

What should be done:

  • Follow/unfollow button on profile pages:
    • Show “Follow” or “Unfollow” based on current relationship
    • Call follow_user / unfollow_user on the User Canister
  • Followers and following lists on the profile page:
    • Call get_followers / get_following on the User Canister
    • Render paginated lists of user cards linking to their profiles
  • Like button on status cards:
    • Toggle like state, call like_status / undo_like
    • Update like count optimistically
  • Boost button on status cards:
    • Toggle boost state, call boost_status / undo_boost
    • Update boost count optimistically
  • Liked statuses page: call get_liked and render the list

Acceptance Criteria:

  • Follow/unfollow toggles correctly and updates the button state
  • Followers and following lists display correct users with pagination
  • Like and boost buttons toggle state and update counts immediately
  • Undoing a like or boost reverses the action
  • The liked statuses page shows all statuses the user has liked

Description: Implement a search interface for discovering users on the Mastic node.

What should be done:

  • Add a search bar in the navigation or a dedicated search page
  • Call search_profiles on the Directory Canister with the query string
  • Display results as user cards (avatar, handle, display name) linking to the user’s profile
  • Implement pagination for search results
  • Debounce input to avoid excessive queries

Acceptance Criteria:

  • Typing a query returns matching users
  • Results link to the correct user profiles
  • Pagination works when there are many results
  • Empty or whitespace-only queries are handled gracefully
  • No excessive API calls while the user is still typing

WI-2.6: Moderation tools

Description: Build moderator-specific UI for content and user moderation. These controls are only visible to users who are moderators.

What should be done:

  • Detect moderator status (e.g., by checking with the Directory Canister whether the current principal is a moderator)
  • Show a delete button on any status card when the user is a moderator:
    • Call delete_status on the author’s User Canister
    • Remove the status from the feed on success
  • Show a suspend button on user profiles when the user is a moderator:
    • Confirmation dialog explaining the action
    • Call suspend on the Directory Canister
  • Moderator management page (accessible from settings or nav):
    • List current moderators
    • Add moderator by principal: call add_moderator
    • Remove moderator: call remove_moderator (with safeguard against removing the last moderator)
  • Block user button on profile pages (available to all users):
    • Call block_user on the User Canister

Acceptance Criteria:

  • Non-moderators do not see moderation controls (delete on others’ statuses, suspend, moderator management)
  • Moderators can delete any status and it disappears from the feed
  • Moderators can suspend a user, who then cannot interact with the platform
  • The moderator list is displayed correctly
  • Adding and removing moderators works, with a safeguard against removing the last one
  • Any user can block another user from their profile page

WI-2.7: Frontend build pipeline & deployment

Description: Integrate the frontend build into the existing just command workflow and CI pipeline.

What should be done:

  • Add just build_frontend command: runs the Vite production build and outputs to the asset canister directory
  • Add just dfx_deploy_frontend command: deploys the asset canister locally
  • Update just build_all_canisters to include the frontend build
  • Update just dfx_deploy_local to include the frontend canister
  • Ensure the frontend build works in CI (install Node.js dependencies, run build)
  • Add just test_frontend command for running frontend unit tests

Acceptance Criteria:

  • just build_frontend produces a production build without errors
  • just dfx_deploy_local deploys all canisters including the frontend
  • The deployed frontend is accessible at the asset canister URL
  • CI can build and deploy the frontend
  • Frontend unit tests run via just test_frontend

Milestone 3 - Integrating the Fediverse

Duration: 2 months

Goal: Implement the Federation Protocol to make Mastic fully compatible with the Fediverse. Remote Mastodon instances can discover Mastic users via WebFinger, fetch actor profiles, and exchange activities over HTTP with ActivityPub and HTTP Signatures.

User Stories: UC13, UC14

Prerequisites: Milestone 2 completed.

Work Items

WI-3.1: Extend database schema for Milestone 3

Description: Extend the wasm-dbms schema to support federation-specific data: remote actor cache, delivery queue, and HTTP signature key references.

What should be done:

  • Federation Canister schema:
    • remote_actors table: actor_uri (TEXT PK), inbox_url (TEXT NOT NULL), shared_inbox_url (TEXT), public_key_pem (TEXT NOT NULL), display_name (TEXT), summary (TEXT), icon_url (TEXT), fetched_at (INTEGER NOT NULL), expires_at (INTEGER NOT NULL)
    • delivery_queue table: id (TEXT PK), activity_json (TEXT NOT NULL), target_inbox_url (TEXT NOT NULL), sender_canister_id (TEXT NOT NULL), attempts (INTEGER DEFAULT 0), last_attempt_at (INTEGER), status (TEXT NOT NULL DEFAULT ‘pending’), created_at (INTEGER NOT NULL)
    • authorized_canisters table: canister_id (TEXT PK), registered_at (INTEGER NOT NULL)
    • Index on delivery_queue.status for pending delivery lookup
    • Index on remote_actors.expires_at for cache eviction
  • User Canister schema additions:
    • Add actor_uri (TEXT) column to followers and following tables to distinguish local vs remote actors
  • Run schema migrations on canister upgrade

Acceptance Criteria:

  • New tables and columns are created on upgrade from M2 schema
  • Existing data is preserved during migration
  • Cache eviction queries work on the remote_actors table
  • Delivery queue supports retry queries (find pending with attempts < max)

WI-3.2: Implement WebFinger endpoint

Description: Serve WebFinger responses so remote instances can discover Mastic users by their acct: URI.

What should be done:

  • In the Federation Canister, handle GET /.well-known/webfinger in http_request (query)
  • Parse the resource query parameter (e.g., acct:alice@mastic.social)
  • Extract the handle, resolve it via the Directory Canister
  • Return a JSON Resource Descriptor (JRD) with:
    • subject: the acct: URI
    • links: a self link pointing to the actor’s ActivityPub profile URL with type: application/activity+json
  • Return 404 for unknown handles
  • Return 400 for malformed requests

Acceptance Criteria:

  • GET /.well-known/webfinger?resource=acct:alice@mastic.social returns a valid JRD with the correct actor URL
  • Unknown handles return 404
  • Malformed resource parameters return 400
  • Response has Content-Type: application/jrd+json
  • Integration test: create user, query WebFinger, verify JRD

WI-3.3: Serve ActivityPub actor profiles

Description: Serve actor profile JSON for remote instances that look up Mastic users.

What should be done:

  • In the Federation Canister, handle GET /users/{handle} in http_request (query) when Accept header includes application/activity+json
  • Resolve the handle via the Directory Canister
  • Fetch the user’s profile from their User Canister
  • Fetch the user’s RSA public key from their User Canister
  • Build an ActivityPub Person object with:
    • id, url, preferredUsername, name, summary
    • inbox, outbox, followers, following collection URLs
    • publicKey block (key ID, owner, PEM-encoded RSA public key)
    • icon and image if avatar/header are set
  • Return the JSON-LD response

Acceptance Criteria:

  • GET /users/alice with the correct Accept header returns a valid ActivityPub Person object
  • The publicKey block contains the correct RSA public key
  • Collection URLs are well-formed
  • Unknown handles return 404
  • Integration test: create user, fetch actor profile, verify all fields

WI-3.4: Serve ActivityPub collections

Description: Serve the outbox, followers, and following OrderedCollection endpoints for remote instances.

What should be done:

  • Handle GET /users/{handle}/outbox in http_request:
    • Return an OrderedCollection with totalItems and paginated OrderedCollectionPage items
    • Fetch outbox items from the User Canister
  • Handle GET /users/{handle}/followers in http_request:
    • Return an OrderedCollection of follower actor URIs
  • Handle GET /users/{handle}/following in http_request:
    • Return an OrderedCollection of following actor URIs
  • Support pagination via page query parameter

Acceptance Criteria:

  • Each collection endpoint returns valid ActivityPub OrderedCollection JSON
  • Pagination works correctly
  • Empty collections return totalItems: 0
  • Unknown handles return 404
  • Integration test: create user with statuses and follows, verify collections

WI-3.5: Implement HTTP Signatures for outgoing requests

Description: Sign all outgoing HTTP requests from the Federation Canister using the sender’s RSA private key, per the HTTP Signatures spec used by Mastodon.

What should be done:

  • Implement HTTP Signature generation:
    • Sign headers: (request-target), host, date, digest, content-type
    • Use RSA-SHA256 algorithm
    • Fetch the sender’s private key from their User Canister
    • Build the Signature header string
  • Add the Signature and Digest headers to all outgoing ActivityPub requests
  • Implement a helper to compute SHA-256 digest of the request body

Acceptance Criteria:

  • All outgoing ActivityPub requests include a valid Signature header
  • The Digest header matches the SHA-256 hash of the body
  • The signature can be verified using the sender’s public key
  • Unit test: sign a request, verify the signature with the public key

WI-3.6: Implement HTTP Signature verification for incoming requests

Description: Verify HTTP Signatures on incoming ActivityPub requests to ensure authenticity.

What should be done:

  • In the Federation Canister http_request_update handler, before processing any incoming activity:
    • Parse the Signature header to extract keyId, headers, signature
    • Fetch the remote actor’s profile from the keyId URL (via ic_cdk::api::management_canister::http_request)
    • Extract the remote actor’s RSA public key
    • Reconstruct the signing string from the specified headers
    • Verify the signature using the remote public key
  • Cache remote actor public keys to avoid repeated fetches (with TTL)
  • Reject requests with invalid or missing signatures

Acceptance Criteria:

  • Incoming requests with valid signatures are accepted
  • Incoming requests with invalid signatures are rejected with 401
  • Incoming requests with missing signatures are rejected with 401
  • Remote public keys are cached with a reasonable TTL
  • Unit test: construct a signed request, verify it passes validation

WI-3.7: Implement incoming activity processing (inbox)

Description: Process incoming ActivityPub activities received via HTTP POST to the shared inbox.

What should be done:

  • In the Federation Canister, handle POST /inbox in http_request_update:
    • Verify HTTP Signature (WI-3.5)
    • Parse the activity JSON
    • Determine the activity type and target
    • Route to the appropriate User Canister(s) via receive_activity
  • Handle the following incoming activity types:
    • Create(Note): deliver to the target user’s inbox
    • Follow: deliver to the target user for acceptance
    • Accept(Follow): deliver to the original requester
    • Reject(Follow): deliver to the original requester
    • Undo(Follow): deliver to the target user
    • Like: deliver to the status author
    • Undo(Like): deliver to the status author
    • Announce: deliver to the target user
    • Undo(Announce): deliver to the target user
    • Delete: deliver to affected users
    • Update(Person): update cached remote actor info
    • Block: deliver to the blocked user

Acceptance Criteria:

  • All listed activity types are correctly parsed and routed
  • Invalid JSON returns 400
  • Unknown activity types are gracefully ignored (return 202)
  • Activities targeting non-existent local users return 404
  • Integration test: simulate an incoming Create(Note) from a remote instance

WI-3.8: Implement outgoing activity delivery (HTTP POST)

Description: Deliver activities to remote Fediverse instances via signed HTTP POST requests.

What should be done:

  • In the Federation Canister send_activity handler, when the target is a remote actor:
    • Resolve the remote actor’s inbox URL (fetch actor profile if not cached)
    • Serialize the activity as JSON-LD
    • Sign the request using the sender’s RSA key (WI-3.5)
    • Send the HTTP POST via ic_cdk::api::management_canister::http_request
    • Handle retries for transient failures (e.g., 5xx responses)
  • Implement delivery to shared inboxes when multiple recipients share the same instance
  • Handle delivery failures gracefully (log, do not block the caller)

Acceptance Criteria:

  • Activities are delivered to remote inboxes via signed HTTP POST
  • Shared inbox optimization works (one request per remote instance)
  • Transient failures are retried (up to a configurable limit)
  • Permanent failures (4xx) are not retried
  • The caller is not blocked by slow remote deliveries

WI-3.9: Implement remote actor resolution and caching

Description: Fetch and cache remote actor profiles for use in activity routing and display.

What should be done:

  • Implement a remote actor resolver in the Federation Canister:
    • Given a remote actor URI, perform WebFinger lookup to find the actor URL
    • Fetch the actor profile via HTTP GET with Accept: application/activity+json
    • Parse the actor profile to extract: display name, summary, public key, inbox URL, followers/following URLs, icon
    • Cache the actor profile in stable memory with a TTL (e.g., 24 hours)
  • Provide a method for User Canisters to request remote actor info (for display in feeds)

Acceptance Criteria:

  • Remote actor profiles are fetched and cached
  • Cached entries expire after the TTL
  • Invalid actor URIs return a descriptive error
  • The resolver handles redirects and content negotiation
  • Unit test: mock a remote actor endpoint, verify parsing

WI-3.10: Implement NodeInfo endpoint

Description: Serve the NodeInfo endpoint so remote instances and monitoring tools can discover Mastic’s software and protocol information.

What should be done:

  • Handle GET /.well-known/nodeinfo in http_request:
    • Return a JSON document with a link to the NodeInfo 2.0 schema URL
  • Handle GET /nodeinfo/2.0 in http_request:
    • Return NodeInfo 2.0 JSON with: software name (“mastic”), version, protocols ([“activitypub”]), open registrations status, usage statistics (total users, active users, local posts)
    • Fetch statistics from the Directory Canister

Acceptance Criteria:

  • GET /.well-known/nodeinfo returns a valid link to the NodeInfo endpoint
  • GET /nodeinfo/2.0 returns valid NodeInfo 2.0 JSON
  • Statistics reflect actual counts from the Directory Canister
  • Integration test: deploy canisters, query NodeInfo, verify response

WI-3.11: Integration tests for federation flows

Description: Write integration tests that exercise the full federation flows, verifying interoperability with the ActivityPub protocol.

What should be done:

  • Test UC13 (Receive Updates from Fediverse): Simulate a remote instance sending a Create(Note) activity, verify it appears in the local user’s feed
  • Test UC14 (Interact with Mastic from Web2): Simulate a local user publishing a status, verify the Federation Canister produces a correctly signed HTTP request with the right ActivityPub payload
  • Test WebFinger: Query WebFinger for a local user, verify the JRD
  • Test Actor Profile: Fetch a local user’s actor profile, verify the Person object
  • Test Collections: Fetch outbox/followers/following collections, verify pagination
  • Test HTTP Signature round-trip: Sign a request, verify it passes validation
  • Test incoming Follow from remote: Simulate a remote Follow, verify the local user gets a new follower

Acceptance Criteria:

  • All federation flows pass as integration tests
  • Tests run in CI via just integration_test
  • Tests simulate remote instances by crafting raw HTTP requests with valid signatures
  • Each test is independent and can run in isolation

Milestone 4 - SNS Launch

Duration: 1 month

Goal: Launch Mastic on the Service Nervous System (SNS) to establish fully decentralised, community-driven governance. Token holders can vote on proposals for moderation, policy changes, canister upgrades, and treasury management.

User Stories: None (infrastructure milestone)

Prerequisites: Milestone 3 completed, all canisters deployed and stable on mainnet.

Work Items

WI-4.1: Prepare SNS configuration (sns_init.yaml)

Description: Define the SNS initialization parameters, token distribution, and governance model for the Mastic DAO in the canonical sns_init.yaml configuration file.

What should be done:

  • Create sns_init.yaml with all required parameters:
    • Project metadata: name, description, logo, URL
    • NNS proposal text: title, forum URL, summary
    • Fallback controllers: principal IDs that regain control if the swap fails (critical — without these the dapp becomes uncontrollable)
    • Dapp canisters: Directory and Federation canister IDs to decentralize (User Canisters are managed by Directory, not directly by SNS Root)
    • Token configuration: name, symbol, transaction fee, logo
    • Governance parameters:
      • Proposal rejection fee
      • Initial voting period (>= 4 days recommended)
      • Maximum wait-for-quiet deadline extension
      • Minimum neuron creation stake
      • Minimum dissolve delay for voting (>= 1 month)
      • Dissolve delay bonus (duration + percentage)
      • Age bonus (duration + percentage)
      • Reward rate (initial, final, transition duration)
    • Token distribution:
      • Developer neurons with dissolve delay (>= 6 months) and vesting period (12-48 months) to signal long-term commitment
      • Seed investor neurons (if any) with vesting
      • Treasury allocation (DAO-controlled)
      • Swap allocation (sold during decentralization swap)
      • Total supply must equal the sum of all allocations
    • Decentralization swap parameters:
      • minimum_participants (100-200 recommended, not too high)
      • Minimum/maximum direct participation ICP
      • Per-participant minimum/maximum ICP
      • Duration (3-7 days recommended)
      • Neurons fund participation (true/false)
      • Vesting schedule for swap neurons (events + interval)
      • Confirmation text (legal disclaimer)
      • Restricted countries list
  • Validate the configuration with dfx sns init-config-file validate
  • Document the governance model and tokenomics rationale in docs/src/governance.md
  • Study successful SNS launches (OpenChat, Hot or Not, Kinic) for parameter ranges the NNS community accepts

Acceptance Criteria:

  • sns_init.yaml passes dfx sns init-config-file validate
  • Token distribution adds up to the total supply exactly
  • Developer neurons have non-zero dissolve delay and vesting period
  • Governance parameters are reasonable (voting period >= 4 days, quorum defined, rejection fee set)
  • Fallback controller principals are defined
  • Documentation explains tokenomics and governance model clearly

WI-4.2: Implement SNS-compatible canister upgrade path

Description: Ensure the Directory and Federation canisters can be upgraded through SNS proposals, and that User Canisters (dynamically created) can be batch-upgraded by the Directory Canister.

What should be done:

  • Verify pre_upgrade and post_upgrade hooks correctly serialize and deserialize all state for Directory and Federation canisters
  • For User Canisters: verify wasm-dbms stable memory survives upgrades
  • Implement set_sns_governance on the Directory Canister:
    • Accept a principal ID for the SNS governance canister
    • Only callable by canister controllers (before SNS launch) or by the already-set governance principal
    • Can only be set once (trap on second call)
  • Implement a require_governance(caller) guard for governance-gated methods
  • Implement upgrade_user_canisters method on the Directory Canister:
    • Accept new User Canister WASM as argument
    • Callable only by SNS governance (via proposal)
    • Iterate over all registered User Canisters
    • Call install_code with mode Upgrade for each
    • Track progress and report failures (individual failures must not block the batch)
  • Test upgrade paths with state preservation

Acceptance Criteria:

  • All canister state survives an upgrade cycle
  • set_sns_governance can only be called once by a controller
  • Governance-gated methods reject unauthorized callers
  • The Directory Canister can batch-upgrade all User Canisters
  • Upgrade failures for individual User Canisters do not block the batch
  • Integration test: deploy, populate state, upgrade, verify state preserved

WI-4.3: Implement SNS-governed moderation proposals

Description: Transition moderation actions from direct moderator calls to SNS proposal-based governance. The SNS governance canister becomes the sole authority for moderation.

What should be done:

  • Implement a generic proposal execution interface on the Directory Canister:
    • Accept proposals from the SNS governance canister
    • Parse proposal payloads to determine the action
  • Supported proposal types:
    • AddModerator: add a principal to the moderator list
    • RemoveModerator: remove a principal from the moderator list
    • SuspendUser: suspend a user by handle
    • UnsuspendUser: reactivate a suspended user
    • UpdatePolicy: update instance moderation policies (e.g., content rules text)
  • Restrict existing direct add_moderator, remove_moderator, and suspend methods to the SNS governance canister principal only (no longer callable by individual moderators directly)

Acceptance Criteria:

  • Moderation actions can only be executed via SNS proposals
  • The Directory Canister correctly parses and executes each proposal type
  • Invalid proposal payloads are rejected with a descriptive error
  • The SNS governance canister principal is the only authorized caller for moderation methods
  • Integration test: simulate a proposal execution, verify the action is applied

WI-4.4: Implement UnsuspendUser flow

Description: Add the ability to reactivate a suspended user account via SNS governance.

What should be done:

  • Implement unsuspend method on the Directory Canister:
    • Authorize the caller (SNS governance canister only)
    • Remove the suspended flag from the user record
    • Notify the User Canister to resume operations
    • Optionally send an Undo(Delete) or Update(Person) activity to re-announce the user to followers
  • Define UnsuspendArgs, UnsuspendResponse in the did crate

Acceptance Criteria:

  • A suspended user can be reactivated via the unsuspend method
  • Only the SNS governance canister can call unsuspend
  • After unsuspension, the user can interact with their User Canister again
  • The user reappears in search_profiles results
  • Integration test: suspend, then unsuspend, verify the user is active

WI-4.5: SNS testflight on local replica and mainnet

Description: Deploy a testflight (mock) SNS to validate the full governance flow before submitting the real NNS proposal. This catches configuration issues early, before they are visible to the NNS community.

What should be done:

  • Local testflight:
    • Deploy NNS canisters locally using sns-testing repo tooling
    • Deploy Mastic canisters locally
    • Deploy a local testflight SNS using sns_init.yaml
    • Test: submit a proposal to upgrade the Directory Canister, vote, verify upgrade succeeds
    • Test: submit a moderation proposal, vote, verify action is applied
    • Test: batch-upgrade User Canisters via proposal
  • Mainnet testflight:
    • Deploy a mock SNS on mainnet (does not run a real swap)
    • Verify governance flows: proposal submission, voting, execution
    • Verify canister upgrade path end-to-end
    • Verify User Canister batch upgrade

Acceptance Criteria:

  • Local testflight passes all governance flow tests
  • Mainnet testflight demonstrates working proposal → vote → execute cycle
  • No issues discovered that would block the real launch

WI-4.6: SNS deployment and decentralization swap

Description: Submit the NNS proposal, transfer canister control to SNS Root, and execute the decentralization swap. This follows the 11-stage SNS launch process.

What should be done:

  • Pre-submission:
    • Add NNS Root (r7inp-6aaaa-aaaaa-aaabq-cai) as co-controller of the Directory and Federation canisters using dfx sns prepare-canisters add-nns-root
    • Final validation of sns_init.yaml
    • Call set_sns_governance on the Directory Canister with the expected SNS governance canister ID (set after SNS-W deploys it)
  • Submit NNS proposal:
    • dfx sns propose --network ic --neuron $NEURON_ID sns_init.yaml
    • This is irreversible once submitted — double-check all parameters
  • During swap (3-7 days):
    • Monitor swap participation and ICP raised
    • Note: six governance proposal types are restricted during the swap (ManageNervousSystemParameters, TransferSnsTreasuryFunds, MintSnsTokens, UpgradeSnsControlledCanister, RegisterDappCanisters, DeregisterDappCanisters) — do not plan operations requiring these
  • Post-swap finalization:
    • Verify all canisters are controlled solely by SNS Root
    • Verify token holders can submit and vote on proposals
    • Document the post-swap governance workflow (how to submit proposals, vote, and execute upgrades)

Acceptance Criteria:

  • The NNS proposal is submitted and adopted by the community
  • SNS-W deploys all SNS canisters (Governance, Ledger, Root, Swap, Index, Archive)
  • SNS Root becomes sole controller of Directory and Federation canisters
  • The decentralization swap completes successfully (meets minimum participants and ICP thresholds)
  • Token holders can submit and vote on proposals
  • Post-swap documentation is published

WI-4.7: Integration tests for SNS governance flows

Description: Write integration tests that validate the SNS governance integration.

What should be done:

  • Test proposal execution: Simulate an SNS proposal to add a moderator, verify the moderator is added
  • Test canister upgrade via SNS: Simulate an upgrade proposal, verify state is preserved
  • Test User Canister batch upgrade: Upgrade all User Canisters via the Directory, verify state
  • Test suspend/unsuspend via proposal: Simulate suspend and unsuspend proposals
  • Test unauthorized access: Verify that direct moderation calls (not from SNS governance) are rejected
  • Test set_sns_governance: Verify it can only be set once and only by controllers

Acceptance Criteria:

  • All governance flows pass as integration tests
  • Tests simulate SNS governance canister calls
  • Each test is independent and can run in isolation
  • Tests run in CI via just integration_test