A Minecraft Fabric mod that connects the game with ATProtocol ⛏️
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

AppView Guide: Displaying Minecraft Data on AT Protocol#

Overview#

An AppView is a custom service that indexes AT Protocol records and provides rich display and query capabilities. The social-sync AppView allows players and community members to:

  • 📊 View leaderboards of top players by various statistics
  • 🏆 Browse achievements earned across the community
  • 👤 See player profiles with stats summaries
  • 🔥 Discover trending achievements and active players
  • 🔍 Search for players by username or display name

Architecture#

AT Protocol Network
  ↓ (Published Records)
Firehose Subscription
  ↓ (Real-time Events)
AppView Service (Index & Query)
  ↓ (HTTP Endpoints)
Web Clients / Bluesky Custom Feeds

Components#

  1. AppViewService: In-memory indexing and querying (uses database in production)
  2. AppViewHttpServer: REST API for serving data to clients
  3. Firehose Subscription: Real-time updates from AT Protocol
  4. Data Models: Records synced from Minecraft via social-sync mod

How It Works#

1. Publishing Records#

When a player syncs data with the social-sync mod:

// Player syncs their stats
val stats = PlayerStatsRecord(
    player = PlayerReference("uuid", "AlicePlayer"),
    statistics = listOf(
        Statistic("minecraft.mined.oak_log", 1250)
    ),
    playtimeMinutes = 7200,
    level = 34,
    syncedAt = now()
)

// Record published to AT Protocol
recordManager.createRecord(playerUuid, "com.jollywhoppers.minecraft.player.stats", stats)
// Result: at://did:plc:alice/com.jollywhoppers.minecraft.player.stats/8l6rvp4j6d3e2c4b9

2. Indexing Records#

The AppView subscribes to repository events and indexes published records:

appViewService.indexPlayerStats(uri, statsJson)
// Now searchable and queryable through leaderboards

3. Querying Data#

Clients query the AppView through HTTP endpoints:

# Get player stats
curl https://appview.example.com/player/550e8400-e29b-41d4-a716-446655440000/stats

# Get leaderboard
curl https://appview.example.com/leaderboard/minecraft.mined.oak_log

# Search players
curl "https://appview.example.com/search?q=Alice"

API Endpoints#

Health Check#

GET /health

Response:
{
  "success": true,
  "data": {
    "status": "healthy",
    "version": "1.0.0",
    "uptime": 3600
  }
}

Get Player Profile#

GET /player/{uuid}

Response:
{
  "success": true,
  "data": {
    "profile": {
      "did": "did:plc:alice123",
      "playerUuid": "550e8400-e29b-41d4-a716-446655440000",
      "username": "AlicePlayer",
      "displayName": "Alice",
      "bio": "Minecraft enthusiast and builder",
      "publicStats": true,
      "publicSessions": true,
      "createdAt": "2026-04-20T10:30:00Z"
    },
    "latestStats": {
      "level": 34,
      "playtimeMinutes": 7200,
      "gamemode": "survival"
    },
    "statsCount": 5,
    "achievementCount": 23
  },
  "timestamp": 1703004000000
}

Get Player Stats History#

GET /player/{uuid}/stats?limit=10&offset=0

Response:
{
  "success": true,
  "data": [
    {
      "uri": "at://did:plc:alice123/.../8l6rvp4j6d3e2c4b9",
      "playerUuid": "550e8400-e29b-41d4-a716-446655440000",
      "username": "AlicePlayer",
      "server": "Main SMP",
      "level": 34,
      "playtimeMinutes": 7200,
      "gamemode": "survival",
      "statistics": [
        {"key": "minecraft.mined.oak_log", "value": 1250},
        {"key": "minecraft.killed.zombie", "value": 425}
      ],
      "syncedAt": "2026-04-25T14:22:00Z"
    }
  ],
  "pagination": {
    "limit": 10,
    "offset": 0,
    "count": 1
  }
}

Get Player Achievements#

GET /player/{uuid}/achievements?limit=25&offset=0

Response:
{
  "success": true,
  "data": [
    {
      "uri": "at://did:plc:alice123/.../8l6rvp4j6d3e2c5a7",
      "playerUuid": "550e8400-e29b-41d4-a716-446655440000",
      "username": "AlicePlayer",
      "server": "Main SMP",
      "achievementId": "minecraft:adventure/kill_a_mob",
      "achievementName": "Monster Hunter",
      "achievementDescription": "Kill any type of monster",
      "category": "adventure",
      "isChallenge": false,
      "achievedAt": "2026-04-24T15:45:00Z"
    }
  ],
  "pagination": {
    "limit": 25,
    "offset": 0,
    "count": 1
  }
}

Get Leaderboard#

GET /leaderboard/{statistic}?limit=20

Example: /leaderboard/minecraft.mined.oak_log

Response:
{
  "success": true,
  "data": [
    {
      "playerUuid": "550e8400-e29b-41d4-a716-446655440000",
      "username": "AlicePlayer",
      "server": "Main SMP",
      "statistic": "minecraft.mined.oak_log",
      "value": 5250,
      "recordedAt": "2026-04-25T14:22:00Z"
    },
    {
      "playerUuid": "660e8400-e29b-41d4-a716-446655440001",
      "username": "BobBuilder",
      "server": "Main SMP",
      "statistic": "minecraft.mined.oak_log",
      "value": 4100,
      "recordedAt": "2026-04-25T13:15:00Z"
    }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "count": 2
  }
}

Search Players#

GET /search?q={query}

Example: /search?q=Alice

Response:
{
  "success": true,
  "data": [
    {
      "did": "did:plc:alice123",
      "playerUuid": "550e8400-e29b-41d4-a716-446655440000",
      "username": "AlicePlayer",
      "displayName": "Alice",
      "bio": "Minecraft enthusiast and builder",
      "publicStats": true,
      "publicSessions": true
    }
  ],
  "timestamp": 1703004000000
}
GET /trending/achievements?limit=10

Response:
{
  "success": true,
  "data": [
    {
      "achievementId": "minecraft:adventure/kill_a_mob",
      "achievementName": "Monster Hunter",
      "category": "adventure",
      "timesEarned": 47,
      "recentlyEarnedBy": ["AlicePlayer", "BobBuilder", "Charlie"]
    },
    {
      "achievementId": "minecraft:story/mine_stone",
      "achievementName": "Stone Age",
      "category": "story",
      "timesEarned": 89,
      "recentlyEarnedBy": ["Diana", "Eve", "Frank"]
    }
  ],
  "timestamp": 1703004000000
}

Get Stats Summary#

GET /stats/summary/{uuid}

Response:
{
  "success": true,
  "data": {
    "playerUuid": "550e8400-e29b-41d4-a716-446655440000",
    "username": "AlicePlayer",
    "playtimeMinutes": 7200,
    "level": 34,
    "gamemode": "survival",
    "server": "Main SMP",
    "topStatistics": [
      {"key": "minecraft.mined.oak_log", "value": 5250},
      {"key": "minecraft.mined.stone", "value": 4100},
      {"key": "minecraft.killed.zombie", "value": 425}
    ],
    "lastSyncedAt": "2026-04-25T14:22:00Z"
  },
  "timestamp": 1703004000000
}

Implementation Examples#

Example 1: Index a Player Profile#

val profileRecord = json.parseToJsonElement("""
{
  "$type": "com.jollywhoppers.minecraft.player.profile",
  "player": {
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "username": "AlicePlayer"
  },
  "displayName": "Alice",
  "bio": "Minecraft enthusiast",
  "publicStats": true,
  "publicSessions": true,
  "createdAt": "2026-04-20T10:30:00Z"
}
""")

appViewService.indexPlayerProfile(
  uri = "at://did:plc:alice123/com.jollywhoppers.minecraft.player.profile/self",
  record = profileRecord
)

Example 2: Query Leaderboard#

val leaderboard = appViewService.getLeaderboard(
  statisticKey = "minecraft.mined.oak_log",
  limit = 10
).getOrNull()

leaderboard?.forEach { entry ->
  println("${entry.username}: ${entry.value} blocks")
}

Example 3: Search for Players#

val results = appViewService.searchPlayers("Alice").getOrNull()

results?.forEach { player ->
  println("${player.username} - ${player.displayName}")
}

Production Deployment#

  • Framework: Ktor or Spring Boot for HTTP server
  • Database: PostgreSQL for record indexing and caching
  • Cache: Redis for leaderboard and trending queries
  • Messaging: AT Protocol Firehose for real-time updates
  • Hosting: Docker container on cloud provider

Key Considerations#

  1. Indexing: Subscribe to the AT Protocol Firehose to capture published records
  2. Databases: Use PostgreSQL to store indexed records for fast queries
  3. Caching: Cache leaderboards and trending data with Redis
  4. Pagination: Implement cursor-based pagination for large result sets
  5. Filtering: Support filtering by server, time period, or category
  6. Performance: Add indexes on frequently queried fields
  7. Privacy: Respect publicStats and publicSessions flags
  8. Registration: Register your AppView in the AT Protocol registry

Sample Ktor Implementation#

import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.response.*

fun main() {
    embeddedServer(Netty, port = 8080) {
        routing {
            get("/player/{uuid}") {
                val uuid = call.parameters["uuid"]
                val profile = appViewService.getPlayerProfile(uuid!!)
                call.respond(httpServer.handleGetPlayerProfile(uuid))
            }
            
            get("/leaderboard/{stat}") {
                val stat = call.parameters["stat"]
                val limit = call.parameters["limit"]?.toInt() ?: 20
                call.respond(httpServer.handleGetLeaderboard(stat!!, limit.toString()))
            }
        }
    }.start(wait = true)
}

Integration with AT Protocol Clients#

Bluesky Custom Feeds#

AppViews can be used to create custom feeds in Bluesky:

  • "Top Builders": Feed of recent high-value block mining achievements
  • "Achievement Feeds": Achievements in specific categories
  • "Speedrun Records": Fastest times for specific challenges
  • "Server Highlights": Notable stats from specific servers

Web Dashboard#

Embed AppView data in a custom web dashboard:

<div id="leaderboard">
  <!-- JavaScript fetches from /leaderboard/minecraft.mined.oak_log -->
</div>

Data Privacy#

  • Public by Default: All AT Protocol data is public
  • Opt-in Syncing: Players control sync consent in /atproto sync
  • Respect Flags: Check publicStats and publicSessions before displaying
  • Anonymous Option: Players can disable syncing entirely

Monitoring#

Monitor your AppView with these metrics:

  • Indexing Latency: Time from publish to index
  • Query Performance: Response times for popular queries
  • Cache Hit Ratio: Percentage of queries served from cache
  • Record Count: Total indexed records by type
  • Active Players: Number of players with data published

Troubleshooting#

No Data in Queries#

  1. Ensure Firehose subscription is active and receiving events
  2. Verify records are being published by players (check /atproto sync settings)
  3. Check indexing function is being called on record events

Slow Leaderboard Queries#

  1. Add database indexes on statistic keys
  2. Implement caching layer (Redis)
  3. Consider pre-computing leaderboards periodically

Missing Records#

  1. Verify player privacy settings allow public stats
  2. Check server subscription is capturing all record types
  3. Review error logs for indexing failures

Examples#

See docs/examples/AppViewExample.kt for complete working examples.

References#

Next Steps#

  1. Deploy AppView service to a public URL
  2. Subscribe to Firehose for real-time record updates
  3. Set up database for production indexing
  4. Register AppView in AT Protocol registry
  5. Publish to Bluesky for community integration