Skip to content

Instantly share code, notes, and snippets.

@cyio
Last active December 21, 2025 12:44
Show Gist options
  • Select an option

  • Save cyio/77fbd26a1ef46e8f1fe8d80997615e71 to your computer and use it in GitHub Desktop.

Select an option

Save cyio/77fbd26a1ef46e8f1fe8d80997615e71 to your computer and use it in GitHub Desktop.
Core Data Design for Metanet Microblog Posts

Core Data Design for Metanet Microblog

Author: @cloudsay | Website: microblog.bitspv.com | Published at: Gist

This document outlines the core data design for Metanet Microblog, an open protocol for decentralized social media. It enables developers to build diverse applications on a shared, on-chain data layer where users retain full data ownership. This text details the protocol's design and the trade-offs involved, aiming to accelerate innovation on BSV.

1. Core Architecture: On-Chain Key-Value Store (KVStore)

  • Technology: Core data is stored on the BSV blockchain using the GlobalKVStore component from @bsv/sdk.
  • Data Model: A simple Key-Value model where the key is a unique string and the value is a JSON string.
  • Benefits: Data is public, verifiable, allowing anyone to build applications on the protocol.

2. Data Isolation: Protocol IDs

  • Mechanism: A unique PROTOCOL_ID is defined for each data type (posts, likes, etc.) to create separate namespaces.
  • Implementation: The primary protocol IDs include:
    • [0, 'Microblog Posts v1']: For posts.
    • [0, 'Microblog Likes v1']: For likes.
    • [0, 'Microblog Reposts v1']: For reposts.
    • [0, 'Microblog Comments v1']: For comments.
    • [0, 'Microblog Tips v1']: For tips.
    • [0, 'Microblog Chat v1']: For chat messages.
  • Note: For development, a DEV prefix is added to the string identifier (e.g., [0, 'DEV Microblog Posts v1']) to isolate test data.
  • Advantages:
    • Namespace: Effectively prevents key collisions between different data types.
    • Extensibility: New data types (e.g., bookmarks, polls) can be added in the future by simply defining a new PROTOCOL_ID without affecting existing data.
    • Query Efficiency: Allows for efficient, categorized data queries based on the PROTOCOL_ID.

3. Key Data Structures and Relational Design

a. Posts

  • Key: post_{uuid}
    • uuid: A unique, randomly generated Base62 string (randomBase62()) that serves as a stable identifier for the post. This replaces the previous post_{authorPublicKey}_{randomNonce} format.
  • Value Format (JSON):
    • Core Fields: { "content": "...", "timestamp": 167..., "uuid": "...", "format": "text" | "markdown" }
      • uuid: The same unique identifier found in the key. Storing it in the value allows for easier data retrieval and management.
      • format: Specifies the content format, either plain text or markdown.
    • Repost: { "content": "...", "timestamp": 167..., "uuid": "...", "isRepost": true, "originalAuthorPublicKey": "...", "originalUuid": "..." }
      • isRepost: true: Explicitly flags the entry as a repost.
      • originalAuthorPublicKey and originalUuid: Link to the original post being reposted.
  • Tag: ["post_id_{uuid}"]
    • Purpose: Provides a consistent way to query or reference the post.

b. Relational Data: Likes and Comments

Relational data is managed through strategic use of Keys and Tags for efficient linking.

  • Likes:

    • Key: like_{post.uuid}
      • post.uuid: Links the like to the original post's stable uuid. The liker's identity is the UTXO controller, making a separate likerPublicKey in the key redundant for uniqueness.
    • Value Format (JSON): { "likedAt": 167... }
    • Tag: ["post_id_{post.uuid}"]
      • Purpose: Allows efficient querying of all likes for a specific post using its stable uuid.
  • Comments:

    • Key: comment_{post.uuid}_{comment_uuid}. This provides a unique key for each comment while linking it to the parent post.
    • Tag: ["post_id_{post.uuid}"]
      • Purpose: Allows efficient querying of all comments for a post using the post's stable uuid. This is consistent with how likes are tagged.

c. Repost Relations

  • Tags: ["repost_of_{originalPost.key}", "reposted_by_{reposterPublicKey}", "post_id_{newUuid}"]
    • repost_of_{originalPost.key}: Links to the original post's key to find all its reposts.
    • reposted_by_{reposterPublicKey}: Identifies the reposter to find all their reposts.
    • post_id_{newUuid}: The new repost itself is a post, so it gets its own post_id tag.

d. Atomic Tips with a Single Transaction

To provide a seamless user experience for tipping, a fund transfer and metadata record are combined into a single atomic transaction, avoiding separate wallet confirmations.

  • Key: tip_{post.uuid}_{timestamp}
    • A timestamp is added to the key to ensure uniqueness, allowing a user to tip the same post multiple times.
  • Implementation: A single transaction is constructed with two outputs:
    • Output 1 (Fund Transfer): A standard P2PKH output that sends satoshis to the author. It uses a PeerPay-style (BRC-29) key derivation to protect the recipient's privacy.
    • Output 2 (Metadata): A PushDrop output that creates a GlobalKVStore record for the tip, containing the key, value (amount, timestamp), and tags.
  • Tags: ["post_id_{post.uuid}", "tipper_id_{tipperPublicKey}", "recipient_id_{authorPublicKey}"]
  • Atomicity: This dual-output structure guarantees that the fund transfer and metadata record are inseparable—they either both succeed or both fail.
  • Notification: The recipient is notified of the tip via an off-chain message through @bsv/message-box-client.

4. Conclusion

The Metanet Microblog protocol represents a starting point for decentralized social media, balancing performance with user sovereignty through on-chain data ownership and client-side flexibility. This is just one implementation, and we hope it inspires the community to innovate further. We encourage developers to build upon, or create competing protocols. The ultimate goal is a rich ecosystem of user-centric applications, and we look forward to seeing the superior solutions that emerge.


Update History:

  • 2025-12-21: Document updated to align with the current codebase. Major changes include:
    • Transitioned post keys from post_{authorPublicKey}_{randomNonce} to a stable post_{uuid} format.
    • Introduced a uuid field in the post's value for consistent identification.
    • Updated key and tag structures for Likes, Reposts, and Tips to use the post uuid.
    • Added Chat to the list of official protocols.
    • Clarified the data structure for Tips to allow multiple tips from the same user.

Development Prompt: Building a Read-Only MVP for Metanet Microblog

1. Project Objective

Your task is to create a lightweight, read-only MVP (Minimum Viable Product) of a web application. This application will serve as a "mirror" to display public posts from the Metanet Microblog, a decentralized social media protocol built on the BSV blockchain.

2. Core Features

  1. Public Feed: Display a feed of the most recent public posts from the network.
  2. User Post Search: Include a search feature where a user can input an author's public key to view all posts created by that specific author.

3. Technical Stack & Dependencies

  • Framework: Use a modern JavaScript framework. React is highly recommended due to its component-based architecture.
  • Core Library: The application's connection to the blockchain data layer is managed by @bsv/sdk.
  • Styling: No complex styling is required. Focus on functionality.

4. Data Layer: GlobalKVStore

The entire protocol relies on @bsv/sdk's GlobalKVStore component, which functions as an on-chain key-value database.

  • Initialization: To query posts, you must first create a GlobalKVStore instance configured for the specific protocol. The wallet parameter can be omitted for read-only operations.

    import { GlobalKVStore } from '@bsv/sdk';
    
    // Protocol ID for production microblog posts
    const POSTS_PROTOCOL_ID = [0, 'Microblog Posts v1'];
    
    const postsKvStore = new GlobalKVStore({
      protocolID: POSTS_PROTOCOL_ID
    });

5. Data Structure & Schemas

Understanding the data schema is crucial for correctly fetching and parsing posts.

  • Post Key: The key for each post follows the format post_{uuid}, where uuid is a unique Base62 string that acts as a stable identifier.
  • Post Value: The value is a JSON string containing the post's metadata and content.
    • Example Value:
      {
        "content": "This is a sample post on the Metanet Microblog!",
        "timestamp": 1679347200000,
        "uuid": "aBcDeFg123",
        "format": "text" // Can be "text" or "markdown"
      }
  • Author Identity: The author of a post is identified by the controller field of the UTXO entry returned by the query. This field contains the author's public key.

6. Implementation Guide

Step 1: Fetching and Displaying All Public Posts

  • Use the get() method on the postsKvStore instance to retrieve a list of the latest posts.

  • Implement pagination using the limit and skip options to load data in batches.

  • Sort by newest first using sortOrder: 'desc'.

    async function fetchPublicPosts(page = 0, pageSize = 20) {
      const query = {
        limit: pageSize,
        skip: page * pageSize,
        sortOrder: 'desc'
      };
      
      // The `includeToken: true` option is useful for getting metadata
      const posts = await postsKvStore.get(query, { includeToken: true });
    
      // The result `posts` will be an array of KVStoreEntry objects.
      // You'll need to parse `entry.value` (JSON) and use `entry.controller` as the author's key.
      return posts;
    }

Step 2: Implementing the User Search Functionality

  • Add a text input field to the UI for users to paste an author's public key.

  • When a search is performed, call the get() method again, but this time, add the controller property to the query object.

    async function fetchPostsByUser(publicKey, page = 0, pageSize = 20) {
      if (!publicKey) return [];
    
      const query = {
        controller: publicKey,
        limit: pageSize,
        skip: page * pageSize,
        sortOrder: 'desc'
      };
    
      const userPosts = await postsKvStore.get(query, { includeToken: true });
      return userPosts;
    }

Step 3: Rendering the Posts

  • Create a Post component that takes the parsed post data as props.
  • Inside the component, display the post content and the author's public key (controller).
  • The application should have a state to hold the list of posts to be rendered. This state will be updated by both the public feed fetch and the user-specific search.

7. UI/UX Requirements

  • A main view that lists the posts.
  • An input field and a "Search" button to filter by author public key.
  • A "Load More" button at the bottom of the list to handle pagination.
  • A "Show All Posts" / "Clear Search" button to switch from a user-specific view back to the global public feed.

8. Optional Enhancements (Extra Credit)

  • Timestamp: Convert the timestamp field into a human-readable format (e.g., "Dec 21, 2025").
  • Markdown Support: If a post's format field is "markdown", render its content as Markdown.
  • Author Name Resolution: Instead of just showing the author's long public key, you can implement a simple function to fetch a more user-friendly identity (name/avatar) from known on-chain identity services. The original project uses a resolveIdentities utility for this. For this MVP, a hardcoded map or a simple placeholder would suffice.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment