Skip to main content

AgoraFora

A decentralized marketplace and forum platform for censorship-resistant commerce and discussion.

AgoraFora

Links

dApp source

Overview

Agora Fora combines the best of decentralized marketplaces and forums, enabling users to buy, sell, and discuss goods and services in a censorship-resistant environment built on the Internet Computer.

Key Features

  • Decentralized marketplace for goods and services
  • Integrated forum for community discussions
  • Anonymous posting and transactions
  • Escrow system for secure trades
  • Community governance and moderation
  • Censorship-resistant infrastructure

Technology Stack

  • Backend: Motoko on Internet Computer
  • Storage: Decentralized storage solutions
  • Frontend: SvelteKit web interface
  • Database: CanDB for scalable data management

Development Status

Status: Deployed

Resources

Inspiration

Architecture

System Overview

Agora is a decentralized social platform built on the Internet Computer (IC) using a multi-canister architecture with CanDB for scalable data storage and automatic partitioning across multiple canisters.

Core Components

Index Canister (IndexCanister.mo)

The Index Canister is the central orchestrator managing the entire backend system.

Responsibilities:

  • Maintains partition key (PK) to canister mappings (pkToCanisterMap)
  • Manages canister creation, lifecycle, and auto-scaling
  • Handles cycles management and distribution
  • Provides canister status and metrics

Partition Key Types:

  • app - Global application configuration (one CanDB map)
  • user - All user profiles and data (one CanDB map shared across all principals)
  • community#<communityId> - Community posts, threads, and files (one CanDB map per community)

Service Canisters

All service canisters follow the single CanDB map pattern with automatic scaling.

AppService (services/appservice/AppService.mo)

  • Partition Key: app
  • Purpose: Global application settings, configurations, logs, and passes
  • Access Patterns:
    • appdata - Application configuration data
    • log#timestamp#<timestamp>#user#<principal> - Activity logs
    • pass#user#<principal>#timestamp#<timestamp> - User passes/subscriptions

Data Schema Examples:

AppData (appdata):

		{
  "fileKeys": {
    "avatarFileKeys": ["avatar#abc123", "avatar#def456"],
    "demoFileKeys": ["demo#xyz789"],
    "loaderFileKeys": ["loader#aaa111"]
  },
  "moderation": {
    "allowVpns": true,
    "cooldownAnonPeriod": 60000000000,
    "cooldownAuthenticatedPeriod": 30000000000,
    "cooldownsEnabled": true,
    "recordIpAddress": false
  },
  "modes": {
    "isUnderMaintenance": false,
    "isReadOnly": false
  },
  "sourceSha256": "abcd1234efgh5678",
  "stats": {
    "visits": 10000
  },
  "version": "1.0.0",
  "whitepaper": "# Agora Whitepaper\n\nDecentralized social platform..."
}
	

Log (log#timestamp#<timestamp>#user#<principal>):

		{
  "action": "create_post",
  "principal": "aaaaa-aa",
  "timestamp": 1700000000000000000,
  "url": "/community/general/post/123"
}
	

Pass (pass#user#<principal>#timestamp#<timestamp>):

		{
  "accountId": "a1b2c3d4e5f6...",
  "blockHeight": 12345,
  "duration": 2592000000000000,
  "principal": "aaaaa-aa",
  "timestamp": 1700000000000000000
}
	

UserService (services/userservice/UserService.mo)

  • Partition Key: user
  • Purpose: All user profile data across all principals
  • Access Patterns:
    • user#<principal> - User profile data
    • avatar#<sha256> - Avatar file metadata
    • avatar#<sha256>#chunk#<chunkId> - Avatar file chunks
    • user#... - Scan all users (range query)

Data Schema Examples:

User Profile (user#<principal>):

		{
  "accountId": "a1b2c3d4e5f6...",
  "avatarKey": "avatar#abc123def456",
  "badges": ["contributor", "early-adopter"],
  "canisterId": "ryjl3-tyaaa-aaaaa-aaaba-cai",
  "index": 42,
  "links": ["https://twitter.com/alice", "https://github.com/alice"],
  "markdownPage": "# About Me\nCommunity builder and developer",
  "principal": "aaaaa-aa",
  "role": "user",
  "presence": "online",
  "statusDescription": "Building the future",
  "timestampFirstAuthentication": 1700000000000000000,
  "timestampLastAuthentication": 1700500000000000000,
  "username": "alice"
}
	

Avatar Metadata (avatar#<sha256>):

		{
  "mime": "image/png",
  "name": "avatar.png",
  "sha256": "abc123def456",
  "size": 102400
}
	

Avatar Chunk (avatar#<sha256>#chunk#<chunkId>):

  • Stored as raw binary data [Nat8] (byte array)

CommunityService (services/communityservice/CommunityService.mo)

  • Partition Key: community#<communityId>
  • Purpose: Community posts, threads, and files
  • Access Patterns:
    • community - Community metadata
    • post#<postId> - Post data
    • filemetadata#<sha256> - File metadata

Data Schema Examples:

Community Metadata (community):

		{
  "creatorPrincipal": "aaaaa-aa",
  "description": "Main community board for general discussion",
  "logoSha256": "xyz789abc123",
  "name": "General Discussion",
  "slug": "general-discussion",
  "timestampCreated": 1700000000000000000,
  "tags": ["community", "general", "discussion"]
}
	

Post (post#<postId>):

		{
  "body": "This is a post about the future of decentralized social networks",
  "fileKeys": ["filemetadata#abc123def456", "filemetadata#def456ghi789"],
  "gate": [
    {
      "token": "ICP",
      "nft": "",
      "amount": 100
    }
  ],
  "poll": [
    {
      "allowWriteIn": true,
      "options": [
        {
          "option": "Option A",
          "fileKey": "",
          "index": 0,
          "votersAnonId": ["anon1", "anon2"],
          "votersPrincipal": ["principal1", "principal2"]
        }
      ],
      "deadline": 1700500000000000000,
      "description": "Vote on this proposal",
      "eligibility": "all",
      "latestVoteTimestamp": 1700400000000000000,
      "question": "What do you think?"
    }
  ],
  "poster": {
    "anonId": "anon123",
    "accountId": "a1b2c3d4e5f6...",
    "avatarKey": "avatar#xyz789",
    "badges": ["contributor"],
    "isVpn": false,
    "countryCode": "US",
    "principal": "aaaaa-aa",
    "username": "alice"
  },
  "postId": 123,
  "properties": {
    "isFlagged": false,
    "isListed": true,
    "isLocked": false,
    "isOp": true,
    "isPinned": false,
    "isSaged": false
  },
  "reactions": [
    {
      "reaction": "πŸ‘",
      "anonId": "anon456",
      "principal": "bbbbb-bb",
      "timestamp": 1700100000000000000
    }
  ],
  "reports": {
    "principals": [],
    "anonIds": []
  },
  "subject": "The Future of Social Networks",
  "threadId": 456,
  "threadStats": [
    {
      "fileCount": 2,
      "fileSize": 204800,
      "pollCount": 1,
      "postCount": 5,
      "postIds": [123, 124, 125, 126, 127],
      "timestampBump": 1700200000000000000,
      "visits": 42
    }
  ],
  "timestampBump": 1700200000000000000,
  "timestampCreate": 1700000000000000000,
  "tokenReceiveEvents": [
    {
      "blockHeight": "12345",
      "fromPrincipal": "ccccc-cc",
      "token": "ICP",
      "tokenAmount": "1.5",
      "usdAmount": "15.75",
      "timestamp": 1700050000000000000
    }
  ],
  "visits": 42
}
	

File Metadata (filemetadata#<sha256>):

		{
  "mime": "image/png",
  "name": "screenshot.png",
  "sha256": "abc123def456",
  "size": 204800
}
	

File Chunk (file#<board/country/dao>#<categoryId>#thread/proposal#<threadId>#post#<postId>#file#<sha256>#chunk#<chunkId>):

  • Stored as raw binary data [Nat8] (byte array)

Key Features:

  • One-to-one mapping: Community ID β†’ CommunityService Canister
  • Simplified key structure: posts stored directly with post#<postId>
  • File metadata stored separately with filemetadata#<sha256>
  • File chunks use hierarchical keys for organization
  • Automatic scaling when heap size exceeds 475MB threshold

High-Level Architecture

		
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Frontend (SvelteKit App + CanDB TypeScript Client)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        β”‚
                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Index Canister (Battery)                  β”‚
β”‚  β€’ Manages canister lifecycle                          β”‚
β”‚  β€’ Routes requests                                     β”‚
β”‚  β€’ Cycles management                                   β”‚
β”‚  β€’ Auto-scaling                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚                β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
        β–Ό            β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  AppService  β”‚ β”‚ UserService  β”‚ β”‚ CommunityServiceβ”‚
β”‚    (app)     β”‚ β”‚   (user)     β”‚ β”‚ (community#id)  β”‚
β”‚              β”‚ β”‚              β”‚ β”‚                 β”‚
β”‚  Config &    β”‚ β”‚ Profiles &   β”‚ β”‚ Posts, threads &β”‚
β”‚  logs        β”‚ β”‚ avatars      β”‚ β”‚ files           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

	

Cycles Management

Battery Canister Pattern:

  • Index Canister acts as central cycles distributor
  • Default quota: 500 billion cycles per canister
  • Aggregate limit: 10 trillion cycles per 24 hours

Per-Canister Configuration:

  • Threshold: 1 trillion cycles
  • Topup amount: 200 billion cycles
  • Auto-topup when cycles fall below threshold

API Endpoints Summary

Index Canister

  • createServicePartition(pk, sizeLimit) - Create new service canister
  • deleteCanisterMapByPK(pk) - Delete all canisters for a partition
  • getPKToCanisterMapping() - Get all PK to canister mappings
  • getCanistersByPK(pk) - Get canisters for specific partition
  • getCanisterInformation() - Get index canister status
  • cycles_manager_transferCycles(cyclesRequested) - Request cycles

Service Canisters (AppService, UserService, CommunityService)

  • getPK() - Get partition key
  • skExists(sk) - Check if sort key exists
  • scanKeys(skLowerBound, skUpperBound, ascending) - Range query
  • getCanisterMetrics() - Get canister metrics
  • transferCycles() - Transfer cycles to owner

Storage Guarantees

  • Durability: All data stored in stable memory via CanDB
  • Consistency: Single-canister consistency model per partition
  • Availability: Auto-scaling ensures no single point of failure per partition
  • Isolation: Each user/community has isolated data storage

Future Considerations

  • Cross-canister queries for complex joins
  • Replication strategy for high-availability communities
  • Archival and cleanup policies for old data
  • Query optimization for large-scale communities