Mastic

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:
- Architecture Overview - Canister architecture, flows, and sequence diagrams
ActivityPub
ActivityPub protocol reference and Mastic-specific mapping:
- ActivityPub on Mastic - Protocol mapping, objects, actors, activities
Candid Interfaces
Canonical Candid interface definitions for each canister:
- Interface Definitions - Directory, Federation, and User canister interfaces
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
- ActivityPub on Mastic
- Mapping to Mastic Architecture
- Objects
- Actors
- Activity Streams
- Protocol
- Social API
- Federation Protocol
- Mastodon
- Statuses Federation
- Profiles Federation
- Reports Extension
- Sensitive Extension
- Hashtag
- Custom Emoji
- Focal Points
- Quote Posts
- Discoverability Flag
- Indexable Flag
- Suspended Flag
- Memorial Flag
- Polls
- Mentions
- Public Key
- Blurhash
- Featured Collection
- Featured Tags
- Profile Metadata
- Account Migration
- Remote Blocking
- HTTP Signatures
- WebFinger
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 Concept | Mastic Component | Notes |
|---|---|---|
| Actor | User Canister | Each Mastic user is represented by a dedicated User Canister that acts as their ActivityPub Actor. |
| Inbox / Outbox | User Canister | The 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 Canister | Instead 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 Canister | All Server-to-Server HTTP traffic is handled by the Federation Canister, which receives incoming activities and forwards outgoing activities to remote instances. |
| HTTP Signatures | User 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. |
| WebFinger | Federation Canister | WebFinger 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:
| Method | Route | Description |
|---|---|---|
GET | /.well-known/webfinger | WebFinger 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}/inbox | Actor inbox — returns the inbox as an OrderedCollection |
POST | /users/{handle}/inbox | Receive activities from remote instances (S2S) — validates HTTP Signatures and delivers to the User Canister |
GET | /users/{handle}/outbox | Actor outbox — returns the outbox as an OrderedCollection |
GET | /users/{handle}/followers | Followers collection — returns the actor’s followers as an OrderedCollection |
GET | /users/{handle}/following | Following collection — returns the actors followed by this actor as an OrderedCollection |
GET | /users/{handle}/liked | Liked 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
OrderedCollectionspecifically, while others are permitted to be either aCollectionor anOrderedCollection. AnOrderedCollectionMUST 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
idfor each Activity. - The server MUST remove
btoand/orbccproperties 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
targetis not owned by the receiving server, and thus they are not authorized to update it. - the
objectis not allowed to be added to thetargetcollection 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
targetis not owned by the receiving server, and thus they are not authorized to update it. - the
objectis not allowed to be removed from thetargetcollection 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:
- 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);
- 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.mdfor 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 databaseDelete: Delete a Status from the databaseLike: Favourited a StatusAnnounce: Boost a status (like rt on Twitter)Undo: Undo a Like or a BoostFlag: Transformed into a report to the moderation team. See the Reports extension for more informationQuoteRequest: Request approval for a quote post. See the Quote Posts extension
Payloads
The first-class Object types supported by Mastodon are Note and Question.
Notesare transformed into regular statuses.Questionsare 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 contentname: Used as status text, if content is not provided on a transformed Object typesummary: Used as CW (Content warning) textsensitive: Used to determine whether status media or text should be hidden by default. See the Sensitive content extension section for more information about as:sensitiveinReplyTo: Used for threading a status as a reply to another statuspublished: status published dateurl: status permalinkattributedTo: Used to determine the profile which authored the statusto/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 (@useror@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 audiourl: Used to fetch the media attachmentsummary: Used as media descriptionaltblurhash: Used to generate a blurred preview image corresponding to the colors used within the image. See Blurhash for more details
replies: A Collection ofstatusesthat 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 pollclosed: 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 optionsname: The poll option’s textreplies:totalItems: The poll option’s vote count
anyOf: Multiple-choice poll optionsname: The poll option’s textreplies: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 detailsDelete: 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 informationMove: Migrate followers from one account to another. RequiresalsoKnownAsto 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 Webfingeracct:URI.name: Used as profile display name.summary: Used as profile bio.type: Assumed to bePerson. If type isApplicationorService, 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 searchpublicKey: Required for signatures. See Public Key for more information.featured: Pinned posts. See Featured collectionattachment: Used for profile fields. See Profile metadata.alsoKnownAs: Required forMoveactivitypublished: 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 usefediverse:creatorfor 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 intounlisted: Unlisted statuses have the as:Public magic collection inccprivate: Followers-only statuses have an actor’s follower collection intoorcc, but do not include theas:Publicmagic collectionlimited: Limited-audience statuses have actors intoorcc, at least one of which is not Mentioned in tagdirect: Mentions-only statuses have actors intoorcc, 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"
}
]
}
Featured Collection
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"
}
Featured Tags
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:
- Split Signature: into its separate parameters.
- Construct the signature string from the value of headers.
- Fetch the
keyIdand resolve to an actor’spublicKey. - RSA-SHA256 hash the signature string and compare to the Base64-decoded signature as decrypted by
publicKey[publicKeyPem]. - Use the
Date:header to check that the signed request was made within the past12 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.
-
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 } -
service : (FederationInstallArgs) -> { http_request : (HttpRequest) -> (HttpResponse) query; http_request_update : (HttpRequest) -> (HttpResponse); send_activity : (SendActivityArgs) -> (SendActivityResponse) } -
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
- ActivityPub: https://www.w3.org/TR/activitypub/
- ActivityStreams: https://www.w3.org/TR/activitystreams-core/
- Mastodon ActivityPub Spec: https://docs.joinmastodon.org/spec/activitypub/
- ActivityPub Federation framework implemented with Rust: https://docs.rs/activitypub_federation/0.6.5/activitypub_federation/
- Webfinger: https://docs.joinmastodon.org/spec/webfinger/
Milestones
Implementation plans for each Mastic development milestone.
- Milestone 0 - Proof of Concept
- Milestone 1 - Standalone Mastic Node
- Milestone 2 - Frontend
- Milestone 3 - Integrating the Fediverse
- Milestone 4 - SNS Launch
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 withid,type,content,name,summary,published,updated,url,to,cc,bto,bcc,audience,attributed_to,in_reply_to,source,tag,attachment,replies,likes,shares,sensitiveSource—content+media_typeTombstone—id,type,published,updated,deletedObjectTypeenum —Note,Question,Image,Tombstone, etc.
- Actor types (
activitypub::actor):Actor— extends Object withinbox,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,imageActorTypeenum —Person,Application,Service,Group,OrganizationPublicKey—id,owner,public_key_pemEndpoints—shared_inbox
- Activity types (
activitypub::activity):Activity— extends Object withactor,object,target,result,origin,instrumentActivityTypeenum —Create,Update,Delete,Follow,Accept,Reject,Like,Announce,Undo,Block,Add,Remove,Flag,Move
- Collection types (
activitypub::collection):Collection—id,type,total_items,first,last,current,itemsOrderedCollection— same as Collection withordered_itemsCollectionPage/OrderedCollectionPage—part_of,next,prev,items/ordered_items
- Link types (
activitypub::link):Link—href,rel,media_type,name,hreflang,height,widthMention— subtype of LinkHashtag— subtype of Link
- Tag types (
activitypub::tag):Tagenum —Mention,Hashtag,EmojiEmoji—id,name,icon(Image withurlandmedia_type)
- Mastodon extensions (
activitypub::mastodon):PropertyValue—name,value(for profile metadata fields)- Poll support on
Questionobjects:end_time,closed,voters_count,one_of/any_ofwithname+replies.total_items - Attachment properties:
blurhash,focal_point
- WebFinger types (
activitypub::webfinger):WebFingerResponse—subject,aliases,linksWebFingerLink—rel,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 namespacehttp://joinmastodon.org/ns#) Contexttype for@contextserialization (single URI, array, or map)
- Constants for standard context URIs
(
- 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
activitypubcrate serde_jsonround-trip tests for every top-level type- Deserialization tests using real-world Mastodon JSON-LD payloads from
docs/src/activitypub.mdexamples cargo clippypasses 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,SendActivityResponsefor the Federation canister - Define
ReceiveActivityArgs,ReceiveActivityResponsefor the User canister - Ensure all types derive
CandidType,Deserialize,Serialize,Clone,Debug
Acceptance Criteria:
- All types compile and are exported from the
didcrate - Types match the
.didinterface files indocs/interface/ cargo clippypasses 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-canisteras a workspace dependency - Create
crates/libs/db-utilscrate:- Define
SettingKeyas au32newtype with named constants per canister (e.g.,FEDERATION_PRINCIPAL,OWNER_PRINCIPAL,DOMAIN_NAME) - Define
SettingValueenum wrappingic-dbms-canisterValuevariants (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
- Define
- Shared
settingstable (both canisters):settingstable:key(INTEGER PK),value(depends on key — TEXT, INTEGER, or BLOB)- Uses
SettingKeyconstants fromdb-utilsto identify entries
- Directory Canister schema:
settingstable — storesfederation_principalfromDirectoryInstallArgs- The initial moderator from
DirectoryInstallArgsis inserted as the first row in themoderatorstable duringinit userstable: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)moderatorstable:principal(PRINCIPAL PK),added_at(INTEGER NOT NULL)- Index on
users.handlefor fast lookups
- User Canister schema:
settingstable — storesowner_principal,federation_principalfromUserInstallArgsprofiletable (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)statusestable:id(TEXT PK),content(TEXT NOT NULL),visibility(TEXT NOT NULL DEFAULT ‘public’),created_at(INTEGER NOT NULL)inboxtable:id(TEXT PK),activity_type(TEXT NOT NULL),actor_uri(TEXT NOT NULL),object_json(TEXT NOT NULL),created_at(INTEGER NOT NULL)followerstable:actor_uri(TEXT PK),created_at(INTEGER NOT NULL)followingtable:actor_uri(TEXT PK),status(TEXT NOT NULL DEFAULT ‘pending’),created_at(INTEGER NOT NULL)keypairtable (single-row):public_key_pem(TEXT NOT NULL),private_key_pem(TEXT NOT NULL)- Indexes on
statuses.created_at,inbox.created_atfor feed ordering
- Initialize the schema in each canister’s
initfunction and persist init args into thesettingstable - Data survives canister upgrades via
wasm-dbmsstable memory management
Acceptance Criteria:
- All tables are created on canister initialization
- Init args are persisted in the
settingstable and retrievable after upgrade db-utilscrate 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,settingstables) -
Implement
initto acceptDirectoryInstallArgs, create the schema, and persist init args into thesettingstable -
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
SignUpResponsewith the canister ID Acceptance Criteria:
-
Calling
sign_upwith 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’sUserRecord(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 ifNone) - Implement
get_user(GetUserArgs)query: look up a user by handle, return their public info (handle, canister ID)
Acceptance Criteria:
whoamireturns the correct record for a registered userwhoamireturns an error for an unregistered calleruser_canister(None)returns the caller’s canisteruser_canister(Some(p))returns the canister for principalpget_userreturns the correct user for a valid handleget_userreturns 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,keypairtables) -
Implement
initto acceptUserInstallArgs, create the schema, and persist init args into thesettingstable -
Generate an RSA keypair for HTTP Signatures (store in
keypairtable) -
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_profilereturns 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
Statusrecords in thestatusestable, keyed by a unique status ID (e.g., ULID or timestamp-based) - Implement
publish_status(PublishStatusArgs):- Authorize the caller (owner only)
- Create a
Statusrecord 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
PublishStatusResponsewith 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
Followactivity 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
Followactivities: 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_usersends 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_followersreturns the correct follower listget_followingreturns 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
ReadFeedResponsewith the page ofFeedItemrecords
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 usewasm-dbms) - Implement
initto acceptFederationInstallArgsand 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_activityon 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
whoamiand verify the correct canister ID is returned - Test UC7 (View Profile): After sign-up, call
get_useron the Directory, thenget_profileon 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:
likedtable:status_uri(TEXT PK),created_at(INTEGER NOT NULL)blockstable:actor_uri(TEXT PK),created_at(INTEGER NOT NULL)- Add
like_count(INTEGER DEFAULT 0) andboost_count(INTEGER DEFAULT 0) columns to thestatusestable - Add
is_boost(INTEGER DEFAULT 0) andoriginal_status_uri(TEXT) columns to theinboxtable for boost tracking
- Directory Canister schema additions:
tombstonestable:handle(TEXT PK),deleted_at(INTEGER NOT NULL),expires_at(INTEGER NOT NULL)- Add index on
tombstones.expires_atfor 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_activityso followers (local for now) receive the updated profile
- Define
UpdateProfileArgsandUpdateProfileResponsein thedidcrate 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
Updateactivity is sent to the Federation Canister - Integration test: update profile, verify
get_profilereturns 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
DeleteProfileResponsein thedidcrate
Acceptance Criteria:
- Calling
delete_profileon the Directory removes the user record - The User Canister is stopped and deleted via the IC management canister
- A
Deleteactivity is delivered to local followers - The deleted user’s handle cannot be reused immediately (tombstone)
whoamireturns an error after deletionget_userreturns 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_activityhandler: handle incomingUndo(Follow):- Remove the requester from the followers list
- Define
UnfollowUserArgs,UnfollowUserResponsein thedidcrate - Add
UndotoActivityTypein thedidcrate
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,SearchProfilesResponsein thedidcrate
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
Likeactivity 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_activityhandler: handle incomingLike:- 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,GetLikedResponsein thedidcrate - Add
LiketoActivityType
Acceptance Criteria:
- Liking a status records it in the liked collection
- A
Likeactivity is sent to the status author - The author’s status like count is incremented
get_likedreturns 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
Announceactivity - 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_activityhandler: handle incomingAnnounce:- 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,UndoBoostResponsein thedidcrate - Add
AnnouncetoActivityType
Acceptance Criteria:
- Boosting a status records it in the outbox
- An
Announceactivity 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,DeleteStatusResponsein thedidcrate - Add
DeletetoActivityTypeif 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
Deleteactivity to followers - Suspended users cannot call any methods on their User Canister
- Define
AddModeratorArgs,AddModeratorResponse,RemoveModeratorArgs,RemoveModeratorResponse,SuspendArgs,SuspendResponsein thedidcrate
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
Deleteactivity is sent to the suspended user’s followers search_profilesexcludes 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
Blockactivity to the Federation Canister
- User Canister
receive_activityhandler: handle incomingBlock:- Hide the blocking user’s content from the blocked user
- Activities from blocked users should be silently dropped in
receive_activity - Define
BlockUserArgs,BlockUserResponsein thedidcrate - Add
BlocktoActivityType
Acceptance Criteria:
- Blocking a user removes mutual follow relationships
- Activities from a blocked user are dropped
- A
Blockactivity 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.jsonwith the frontend asset canister - Set up
@dfinity/agentwith actor factories generated from the.didfiles for Directory, Federation, and User canisters - Integrate
@dfinity/auth-clientfor Internet Identity sign-in/sign-out - Implement sign-up page: handle input + call
sign_upon the Directory Canister - Post-auth routing: call
whoamito 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_feedon 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_statuson 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_useron the Directory Canister to resolve the User Canister - Call
get_profileon the target User Canister - Display: handle, display name, bio, avatar, header image, follower/following counts
- Call
- Own profile: show an edit form for display name, bio, avatar URL, and
header URL that calls
update_profileon the User Canister - Delete account flow: confirmation dialog that calls
delete_profileon 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_useron the User Canister
- Followers and following lists on the profile page:
- Call
get_followers/get_followingon the User Canister - Render paginated lists of user cards linking to their profiles
- Call
- Like button on status cards:
- Toggle like state, call
like_status/undo_like - Update like count optimistically
- Toggle like state, call
- Boost button on status cards:
- Toggle boost state, call
boost_status/undo_boost - Update boost count optimistically
- Toggle boost state, call
- Liked statuses page: call
get_likedand 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
WI-2.5: User search
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_profileson 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_statuson the author’s User Canister - Remove the status from the feed on success
- Call
- Show a suspend button on user profiles when the user is a moderator:
- Confirmation dialog explaining the action
- Call
suspendon 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_useron the User Canister
- Call
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_frontendcommand: runs the Vite production build and outputs to the asset canister directory - Add
just dfx_deploy_frontendcommand: deploys the asset canister locally - Update
just build_all_canistersto include the frontend build - Update
just dfx_deploy_localto include the frontend canister - Ensure the frontend build works in CI (install Node.js dependencies, run build)
- Add
just test_frontendcommand for running frontend unit tests
Acceptance Criteria:
just build_frontendproduces a production build without errorsjust dfx_deploy_localdeploys 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_actorstable: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_queuetable: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_canisterstable:canister_id(TEXT PK),registered_at(INTEGER NOT NULL)- Index on
delivery_queue.statusfor pending delivery lookup - Index on
remote_actors.expires_atfor cache eviction
- User Canister schema additions:
- Add
actor_uri(TEXT) column tofollowersandfollowingtables to distinguish local vs remote actors
- Add
- 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_actorstable - 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/webfingerinhttp_request(query) - Parse the
resourcequery parameter (e.g.,acct:alice@mastic.social) - Extract the handle, resolve it via the Directory Canister
- Return a JSON Resource Descriptor (JRD) with:
subject: theacct:URIlinks: aselflink pointing to the actor’s ActivityPub profile URL withtype: application/activity+json
- Return 404 for unknown handles
- Return 400 for malformed requests
Acceptance Criteria:
GET /.well-known/webfinger?resource=acct:alice@mastic.socialreturns a valid JRD with the correct actor URL- Unknown handles return 404
- Malformed
resourceparameters 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}inhttp_request(query) whenAcceptheader includesapplication/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
Personobject with:id,url,preferredUsername,name,summaryinbox,outbox,followers,followingcollection URLspublicKeyblock (key ID, owner, PEM-encoded RSA public key)iconandimageif avatar/header are set
- Return the JSON-LD response
Acceptance Criteria:
GET /users/alicewith the correct Accept header returns a valid ActivityPub Person object- The
publicKeyblock 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}/outboxinhttp_request:- Return an
OrderedCollectionwithtotalItemsand paginatedOrderedCollectionPageitems - Fetch outbox items from the User Canister
- Return an
- Handle
GET /users/{handle}/followersinhttp_request:- Return an
OrderedCollectionof follower actor URIs
- Return an
- Handle
GET /users/{handle}/followinginhttp_request:- Return an
OrderedCollectionof following actor URIs
- Return an
- Support pagination via
pagequery 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
Signatureheader string
- Sign headers:
- Add the
SignatureandDigestheaders 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
Signatureheader - The
Digestheader 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_updatehandler, before processing any incoming activity:- Parse the
Signatureheader to extractkeyId,headers,signature - Fetch the remote actor’s profile from the
keyIdURL (viaic_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
- Parse the
- 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 /inboxinhttp_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 inboxFollow: deliver to the target user for acceptanceAccept(Follow): deliver to the original requesterReject(Follow): deliver to the original requesterUndo(Follow): deliver to the target userLike: deliver to the status authorUndo(Like): deliver to the status authorAnnounce: deliver to the target userUndo(Announce): deliver to the target userDelete: deliver to affected usersUpdate(Person): update cached remote actor infoBlock: 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_activityhandler, 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/nodeinfoinhttp_request:- Return a JSON document with a link to the NodeInfo 2.0 schema URL
- Handle
GET /nodeinfo/2.0inhttp_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/nodeinforeturns a valid link to the NodeInfo endpointGET /nodeinfo/2.0returns 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.yamlwith 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.yamlpassesdfx 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_upgradeandpost_upgradehooks correctly serialize and deserialize all state for Directory and Federation canisters - For User Canisters: verify
wasm-dbmsstable memory survives upgrades - Implement
set_sns_governanceon 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_canistersmethod 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_codewith modeUpgradefor 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_governancecan 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 listRemoveModerator: remove a principal from the moderator listSuspendUser: suspend a user by handleUnsuspendUser: reactivate a suspended userUpdatePolicy: update instance moderation policies (e.g., content rules text)
- Restrict existing direct
add_moderator,remove_moderator, andsuspendmethods 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
unsuspendmethod 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)orUpdate(Person)activity to re-announce the user to followers
- Define
UnsuspendArgs,UnsuspendResponsein thedidcrate
Acceptance Criteria:
- A suspended user can be reactivated via the
unsuspendmethod - Only the SNS governance canister can call
unsuspend - After unsuspension, the user can interact with their User Canister again
- The user reappears in
search_profilesresults - 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-testingrepo 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
- Deploy NNS canisters locally using
- 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 usingdfx sns prepare-canisters add-nns-root - Final validation of
sns_init.yaml - Call
set_sns_governanceon the Directory Canister with the expected SNS governance canister ID (set after SNS-W deploys it)
- Add NNS Root (
- 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