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