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

Links
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 datalog#timestamp#<timestamp>#user#<principal>- Activity logspass#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 dataavatar#<sha256>- Avatar file metadataavatar#<sha256>#chunk#<chunkId>- Avatar file chunksuser#...- 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 metadatapost#<postId>- Post datafilemetadata#<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 canisterdeleteCanisterMapByPK(pk)- Delete all canisters for a partitiongetPKToCanisterMapping()- Get all PK to canister mappingsgetCanistersByPK(pk)- Get canisters for specific partitiongetCanisterInformation()- Get index canister statuscycles_manager_transferCycles(cyclesRequested)- Request cycles
Service Canisters (AppService, UserService, CommunityService)
getPK()- Get partition keyskExists(sk)- Check if sort key existsscanKeys(skLowerBound, skUpperBound, ascending)- Range querygetCanisterMetrics()- Get canister metricstransferCycles()- 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