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.

at main 476 lines 12 kB view raw view rendered
1# AppView Guide: Displaying Minecraft Data on AT Protocol 2 3## Overview 4 5An **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: 6 7- 📊 **View leaderboards** of top players by various statistics 8- 🏆 **Browse achievements** earned across the community 9- 👤 **See player profiles** with stats summaries 10- 🔥 **Discover trending** achievements and active players 11- 🔍 **Search** for players by username or display name 12 13## Architecture 14 15``` 16AT Protocol Network 17 ↓ (Published Records) 18Firehose Subscription 19 ↓ (Real-time Events) 20AppView Service (Index & Query) 21 ↓ (HTTP Endpoints) 22Web Clients / Bluesky Custom Feeds 23``` 24 25### Components 26 271. **AppViewService**: In-memory indexing and querying (uses database in production) 282. **AppViewHttpServer**: REST API for serving data to clients 293. **Firehose Subscription**: Real-time updates from AT Protocol 304. **Data Models**: Records synced from Minecraft via social-sync mod 31 32## How It Works 33 34### 1. Publishing Records 35 36When a player syncs data with the social-sync mod: 37 38```kotlin 39// Player syncs their stats 40val stats = PlayerStatsRecord( 41 player = PlayerReference("uuid", "AlicePlayer"), 42 statistics = listOf( 43 Statistic("minecraft.mined.oak_log", 1250) 44 ), 45 playtimeMinutes = 7200, 46 level = 34, 47 syncedAt = now() 48) 49 50// Record published to AT Protocol 51recordManager.createRecord(playerUuid, "com.jollywhoppers.minecraft.player.stats", stats) 52// Result: at://did:plc:alice/com.jollywhoppers.minecraft.player.stats/8l6rvp4j6d3e2c4b9 53``` 54 55### 2. Indexing Records 56 57The AppView subscribes to repository events and indexes published records: 58 59```kotlin 60appViewService.indexPlayerStats(uri, statsJson) 61// Now searchable and queryable through leaderboards 62``` 63 64### 3. Querying Data 65 66Clients query the AppView through HTTP endpoints: 67 68```bash 69# Get player stats 70curl https://appview.example.com/player/550e8400-e29b-41d4-a716-446655440000/stats 71 72# Get leaderboard 73curl https://appview.example.com/leaderboard/minecraft.mined.oak_log 74 75# Search players 76curl "https://appview.example.com/search?q=Alice" 77``` 78 79## API Endpoints 80 81### Health Check 82 83``` 84GET /health 85 86Response: 87{ 88 "success": true, 89 "data": { 90 "status": "healthy", 91 "version": "1.0.0", 92 "uptime": 3600 93 } 94} 95``` 96 97### Get Player Profile 98 99``` 100GET /player/{uuid} 101 102Response: 103{ 104 "success": true, 105 "data": { 106 "profile": { 107 "did": "did:plc:alice123", 108 "playerUuid": "550e8400-e29b-41d4-a716-446655440000", 109 "username": "AlicePlayer", 110 "displayName": "Alice", 111 "bio": "Minecraft enthusiast and builder", 112 "publicStats": true, 113 "publicSessions": true, 114 "createdAt": "2026-04-20T10:30:00Z" 115 }, 116 "latestStats": { 117 "level": 34, 118 "playtimeMinutes": 7200, 119 "gamemode": "survival" 120 }, 121 "statsCount": 5, 122 "achievementCount": 23 123 }, 124 "timestamp": 1703004000000 125} 126``` 127 128### Get Player Stats History 129 130``` 131GET /player/{uuid}/stats?limit=10&offset=0 132 133Response: 134{ 135 "success": true, 136 "data": [ 137 { 138 "uri": "at://did:plc:alice123/.../8l6rvp4j6d3e2c4b9", 139 "playerUuid": "550e8400-e29b-41d4-a716-446655440000", 140 "username": "AlicePlayer", 141 "server": "Main SMP", 142 "level": 34, 143 "playtimeMinutes": 7200, 144 "gamemode": "survival", 145 "statistics": [ 146 {"key": "minecraft.mined.oak_log", "value": 1250}, 147 {"key": "minecraft.killed.zombie", "value": 425} 148 ], 149 "syncedAt": "2026-04-25T14:22:00Z" 150 } 151 ], 152 "pagination": { 153 "limit": 10, 154 "offset": 0, 155 "count": 1 156 } 157} 158``` 159 160### Get Player Achievements 161 162``` 163GET /player/{uuid}/achievements?limit=25&offset=0 164 165Response: 166{ 167 "success": true, 168 "data": [ 169 { 170 "uri": "at://did:plc:alice123/.../8l6rvp4j6d3e2c5a7", 171 "playerUuid": "550e8400-e29b-41d4-a716-446655440000", 172 "username": "AlicePlayer", 173 "server": "Main SMP", 174 "achievementId": "minecraft:adventure/kill_a_mob", 175 "achievementName": "Monster Hunter", 176 "achievementDescription": "Kill any type of monster", 177 "category": "adventure", 178 "isChallenge": false, 179 "achievedAt": "2026-04-24T15:45:00Z" 180 } 181 ], 182 "pagination": { 183 "limit": 25, 184 "offset": 0, 185 "count": 1 186 } 187} 188``` 189 190### Get Leaderboard 191 192``` 193GET /leaderboard/{statistic}?limit=20 194 195Example: /leaderboard/minecraft.mined.oak_log 196 197Response: 198{ 199 "success": true, 200 "data": [ 201 { 202 "playerUuid": "550e8400-e29b-41d4-a716-446655440000", 203 "username": "AlicePlayer", 204 "server": "Main SMP", 205 "statistic": "minecraft.mined.oak_log", 206 "value": 5250, 207 "recordedAt": "2026-04-25T14:22:00Z" 208 }, 209 { 210 "playerUuid": "660e8400-e29b-41d4-a716-446655440001", 211 "username": "BobBuilder", 212 "server": "Main SMP", 213 "statistic": "minecraft.mined.oak_log", 214 "value": 4100, 215 "recordedAt": "2026-04-25T13:15:00Z" 216 } 217 ], 218 "pagination": { 219 "limit": 20, 220 "offset": 0, 221 "count": 2 222 } 223} 224``` 225 226### Search Players 227 228``` 229GET /search?q={query} 230 231Example: /search?q=Alice 232 233Response: 234{ 235 "success": true, 236 "data": [ 237 { 238 "did": "did:plc:alice123", 239 "playerUuid": "550e8400-e29b-41d4-a716-446655440000", 240 "username": "AlicePlayer", 241 "displayName": "Alice", 242 "bio": "Minecraft enthusiast and builder", 243 "publicStats": true, 244 "publicSessions": true 245 } 246 ], 247 "timestamp": 1703004000000 248} 249``` 250 251### Get Trending Achievements 252 253``` 254GET /trending/achievements?limit=10 255 256Response: 257{ 258 "success": true, 259 "data": [ 260 { 261 "achievementId": "minecraft:adventure/kill_a_mob", 262 "achievementName": "Monster Hunter", 263 "category": "adventure", 264 "timesEarned": 47, 265 "recentlyEarnedBy": ["AlicePlayer", "BobBuilder", "Charlie"] 266 }, 267 { 268 "achievementId": "minecraft:story/mine_stone", 269 "achievementName": "Stone Age", 270 "category": "story", 271 "timesEarned": 89, 272 "recentlyEarnedBy": ["Diana", "Eve", "Frank"] 273 } 274 ], 275 "timestamp": 1703004000000 276} 277``` 278 279### Get Stats Summary 280 281``` 282GET /stats/summary/{uuid} 283 284Response: 285{ 286 "success": true, 287 "data": { 288 "playerUuid": "550e8400-e29b-41d4-a716-446655440000", 289 "username": "AlicePlayer", 290 "playtimeMinutes": 7200, 291 "level": 34, 292 "gamemode": "survival", 293 "server": "Main SMP", 294 "topStatistics": [ 295 {"key": "minecraft.mined.oak_log", "value": 5250}, 296 {"key": "minecraft.mined.stone", "value": 4100}, 297 {"key": "minecraft.killed.zombie", "value": 425} 298 ], 299 "lastSyncedAt": "2026-04-25T14:22:00Z" 300 }, 301 "timestamp": 1703004000000 302} 303``` 304 305## Implementation Examples 306 307### Example 1: Index a Player Profile 308 309```kotlin 310val profileRecord = json.parseToJsonElement(""" 311{ 312 "$type": "com.jollywhoppers.minecraft.player.profile", 313 "player": { 314 "uuid": "550e8400-e29b-41d4-a716-446655440000", 315 "username": "AlicePlayer" 316 }, 317 "displayName": "Alice", 318 "bio": "Minecraft enthusiast", 319 "publicStats": true, 320 "publicSessions": true, 321 "createdAt": "2026-04-20T10:30:00Z" 322} 323""") 324 325appViewService.indexPlayerProfile( 326 uri = "at://did:plc:alice123/com.jollywhoppers.minecraft.player.profile/self", 327 record = profileRecord 328) 329``` 330 331### Example 2: Query Leaderboard 332 333```kotlin 334val leaderboard = appViewService.getLeaderboard( 335 statisticKey = "minecraft.mined.oak_log", 336 limit = 10 337).getOrNull() 338 339leaderboard?.forEach { entry -> 340 println("${entry.username}: ${entry.value} blocks") 341} 342``` 343 344### Example 3: Search for Players 345 346```kotlin 347val results = appViewService.searchPlayers("Alice").getOrNull() 348 349results?.forEach { player -> 350 println("${player.username} - ${player.displayName}") 351} 352``` 353 354## Production Deployment 355 356### Recommended Stack 357 358- **Framework**: Ktor or Spring Boot for HTTP server 359- **Database**: PostgreSQL for record indexing and caching 360- **Cache**: Redis for leaderboard and trending queries 361- **Messaging**: AT Protocol Firehose for real-time updates 362- **Hosting**: Docker container on cloud provider 363 364### Key Considerations 365 3661. **Indexing**: Subscribe to the AT Protocol Firehose to capture published records 3672. **Databases**: Use PostgreSQL to store indexed records for fast queries 3683. **Caching**: Cache leaderboards and trending data with Redis 3694. **Pagination**: Implement cursor-based pagination for large result sets 3705. **Filtering**: Support filtering by server, time period, or category 3716. **Performance**: Add indexes on frequently queried fields 3727. **Privacy**: Respect `publicStats` and `publicSessions` flags 3738. **Registration**: Register your AppView in the AT Protocol registry 374 375### Sample Ktor Implementation 376 377```kotlin 378import io.ktor.server.application.* 379import io.ktor.server.routing.* 380import io.ktor.server.response.* 381 382fun main() { 383 embeddedServer(Netty, port = 8080) { 384 routing { 385 get("/player/{uuid}") { 386 val uuid = call.parameters["uuid"] 387 val profile = appViewService.getPlayerProfile(uuid!!) 388 call.respond(httpServer.handleGetPlayerProfile(uuid)) 389 } 390 391 get("/leaderboard/{stat}") { 392 val stat = call.parameters["stat"] 393 val limit = call.parameters["limit"]?.toInt() ?: 20 394 call.respond(httpServer.handleGetLeaderboard(stat!!, limit.toString())) 395 } 396 } 397 }.start(wait = true) 398} 399``` 400 401## Integration with AT Protocol Clients 402 403### Bluesky Custom Feeds 404 405AppViews can be used to create custom feeds in Bluesky: 406 407- **"Top Builders"**: Feed of recent high-value block mining achievements 408- **"Achievement Feeds"**: Achievements in specific categories 409- **"Speedrun Records"**: Fastest times for specific challenges 410- **"Server Highlights"**: Notable stats from specific servers 411 412### Web Dashboard 413 414Embed AppView data in a custom web dashboard: 415 416```html 417<div id="leaderboard"> 418 <!-- JavaScript fetches from /leaderboard/minecraft.mined.oak_log --> 419</div> 420``` 421 422## Data Privacy 423 424- **Public by Default**: All AT Protocol data is public 425- **Opt-in Syncing**: Players control sync consent in `/atproto sync` 426- **Respect Flags**: Check `publicStats` and `publicSessions` before displaying 427- **Anonymous Option**: Players can disable syncing entirely 428 429## Monitoring 430 431Monitor your AppView with these metrics: 432 433- **Indexing Latency**: Time from publish to index 434- **Query Performance**: Response times for popular queries 435- **Cache Hit Ratio**: Percentage of queries served from cache 436- **Record Count**: Total indexed records by type 437- **Active Players**: Number of players with data published 438 439## Troubleshooting 440 441### No Data in Queries 442 4431. Ensure Firehose subscription is active and receiving events 4442. Verify records are being published by players (check `/atproto sync` settings) 4453. Check indexing function is being called on record events 446 447### Slow Leaderboard Queries 448 4491. Add database indexes on statistic keys 4502. Implement caching layer (Redis) 4513. Consider pre-computing leaderboards periodically 452 453### Missing Records 454 4551. Verify player privacy settings allow public stats 4562. Check server subscription is capturing all record types 4573. Review error logs for indexing failures 458 459## Examples 460 461See `docs/examples/AppViewExample.kt` for complete working examples. 462 463## References 464 465- [AT Protocol AppView Documentation](https://atproto.com/specs/app-view) 466- [Lexicon Specifications](https://atproto.com/specs/lexicon) 467- [XRPC API Reference](https://atproto.com/specs/xrpc) 468- [Bluesky Custom Feeds](https://docs.bsky.app/docs/tutorials/custom-feeds) 469 470## Next Steps 471 4721. **Deploy AppView service** to a public URL 4732. **Subscribe to Firehose** for real-time record updates 4743. **Set up database** for production indexing 4754. **Register AppView** in AT Protocol registry 4765. **Publish to Bluesky** for community integration