Docs
Friend Leaderboard
Enable social engagement and drive retention by implementing friend leaderboards in your TikTok mini game. This guide covers the Open Data Domain architecture, API integration, and best practices for creating compelling leaderboard experiences.
The Open Data Domain is a secure, isolated environment that enables mini games to access social features without exposing sensitive user data directly to the main game environment. This architecture allows you to implement friend leaderboards while maintaining user privacy and platform security.
- Friend Leaderboards: Display rankings of mutual followers who play your game
- Achievement Surpassing: Show when players beat their friends' scores
- Social Engagement: Leverage TikTok's social graph to drive competitive gameplay
- User Retention: Use social connections to encourage repeat visits
User experience flow
The friend leaderboard integration follows a three-stage user journey:
Stage 1: Authorization Stage
When a user initiates an action that requires social data (such as clicking "View Friend Leaderboard"), the system requests authorization for:
- Avatar and Nickname: Display name and profile picture for identity
- Friend Relationship Chain: Mutual follower connections for the leaderboard
Incremental Authorization: If the user has previously authorized only one of these permissions, only the missing permission will be requested.
Stage 2: Data Retrieval Stage
After authorization, the game retrieves friend data from the cloud and renders the leaderboard.
Stage 3: Leaderboard Display Stage
The rendered leaderboard is displayed to the user, showing their ranking relative to friends.
System architecture
The Open Data Domain architecture consists of three main components:
Component overview
Component | Description | Responsibilities |
Main Game Domain (Game Frame) | Your primary game environment | Game logic, user input, rendering game content |
Open Data Domain (Open Data Frame) | Isolated iframe environment | Accessing social data, rendering leaderboards, handling friend interactions |
Main Frame (Architecture) | Platform-managed bridge | Message routing, security enforcement, cross-domain communication |
Communication flow
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User Action │────▶│ Game Frame │────▶│ Authorization │
│ (Click button) │ │ (Main Domain) │ │ Request │
└─────────────────┘ └─────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Leaderboard │◀────│ Image Display │◀────│ Authorization │
│ Display │ │ │ │ Response │
└─────────────────┘ └─────────────────┘ └─────────────────┘
▲
│
┌───────┴─────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Open Data │◀────│ Message Pass │◀────│ Cloud Storage │
│ Domain │ │ (postMessage) │ │ Request │
│ (Renders List) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘The complete sequence flow:
- User clicks "View Leaderboard" button in the game
- Game Frame requests authorization from the platform
- Authorization dialog displays to the user
- User completes authorization
- Game Frame sends a postMessage to the Main Frame
- Main Frame forwards the message to the Open Data Frame
- Open Data Frame requests friend cloud storage data
- Platform queries the relationship chain and returns friend data
- Open Data Frame renders the leaderboard
- Rendered content is committed back through the Main Frame
- Main Frame converts content to a cross-domain image
- Image URL is passed to the Game Frame
- Game Frame displays the leaderboard on a "tainted" canvas overlay
Implementation guide
Developer responsibilities
As a game developer, you are responsible for:
- Integration Point: Display a "View Friend Leaderboard" button at appropriate moments in your game (e.g., after level completion, on the main menu, or in the world rankings screen)
- Customization: Design and implement your own leaderboard layout, styling, and display logic. The platform provides the data; you control the presentation.
- Optimization: Monitor key metrics including:
- Leaderboard view button click-through rate (CTR)
- Leaderboard exposure rate
- User engagement with social features
Suggested integration points
- Post-Level Completion: Show friend rankings immediately after a user completes a level
- Main Menu: Provide persistent access to leaderboards from the main game screen
- World Rankings: Offer a toggle between global and friend leaderboards
- Achievement Milestones: Prompt users to check leaderboards when they achieve high scores
Note: TikTok is concurrently developing in-game direct message sharing capabilities that will integrate with friend leaderboards, enabling richer social interactions.
API Reference
Prerequisites
- CLI Tool: Install the TikTok mini games CLI
npm install -g @ttmg/cliProject Configuration
Enable the Open Data Domain feature by modifying your game.json file:
{
"dev": {
"port": 9527
},
"openDataContext": "openDataContext",
"app_id": "your_client_key"
}The openDataContext field specifies the directory containing your open data domain code. The system automatically loads openDataContext/index.js as the entry point.
Canvas Configuration
The Open Data Domain renders on a shared canvas that overlays your main game. Configure the canvas position and dimensions:
let sharedCanvas = GameGlobal.tt.getSharedCanvas();
let sharedContext = sharedCanvas.getContext('2d');
// Set internal resolution (coordinate system)
sharedCanvas.width = 800;
sharedCanvas.height = 800;
// Set display size and position using CSS-style properties
sharedCanvas.style.width = '80vw';
sharedCanvas.style.height = '40vh';
sharedCanvas.style.top = '20vh';
sharedCanvas.style.left = '10vw';Important: The canvas uses viewport-relative units (vw, vh) for responsive positioning across different screen sizes.
Game Domain APIs
These methods are called from your main game code.
getOpenDataContext
Retrieves a reference to the Open Data Context for cross-domain communication.
// TTMinis SDK
const { postMessage } = TTminis.game.getOpenDataContext();
// Native implementation
const { postMessage } = tt.getOpenDataContext();
// Send message to open data domain
postMessage({ type: 'show', data: {} });authorizeOpenContext
Requests authorization to access the user's avatar, nickname, and friend relationship chain. This must be called before accessing any social features.
TTMinis.game.authorizeOpenContext({
// When get_status_only is true, only checks status without showing UI
get_status_only: false,
success: (result) => {
const { code } = result;
// Use this code for server-side authentication
},
fail: () => {},
complete: () => {}
});
// Native implementation
tt.authorizeOpenContext({
get_status_only: false,
success: (result) => {
const { code } = result;
},
fail: () => {},
complete: () => {}
});Parameters
Field | Type | Description | Required |
| boolean | When | No |
| function | Callback on successful authorization | No |
| function | Callback on authorization failure | No |
| function | Callback when authorization flow completes | No |
Success Callback Result
Field | Type | Description |
| string | Authorization code for server-side authentication |
setUserCloudStorage
Stores user data in the cloud for persistence across sessions and for sharing with the leaderboard system.
TTMinis.game.setUserCloudStorage(options);
// Native
tt.setUserCloudStorage(options);Options Object
Field | Type | Description | Required |
| Array | Array of key-value pairs to store | Yes |
| string | Key name (max 128 characters) | Yes |
| string | Value to store (max 2048 characters) | Yes |
| function | Callback on successful save | No |
| function | Callback on save failure | No |
| function | Callback when operation completes | No |
Usage Example:
TTMinis.game.setUserCloudStorage({
data: [
{ key: 'highScore', value: '15000' },
{ key: 'level', value: '25' }
],
success: () => {
console.log('Score saved successfully');
}
});removeUserCloudStorage
Removes specific keys from the user's cloud storage.
TTMinis.game.removeUserCloudStorage(options);
// Native
tt.removeUserCloudStorage(options);Options Object
Field | Type | Description | Required |
| Array<string> | Array of keys to remove | Yes |
| function | Callback on successful removal | No |
| function | Callback on removal failure | No |
| function | Callback when operation completes | No |
Open Data Domain APIs
These methods are called from within the Open Data Domain (openDataContext/index.js).
onMessage
Listens for messages sent from the main game domain.
tt.onMessage((data) => {
if (data.command === 'render') {
// ... rendering logic
}
});Animation Methods
Standard animation frame methods for smooth rendering:
// Request animation frame
const frameId = tt.requestAnimationFrame(() => {
// Render logic
});
// Cancel scheduled animation frame
tt.cancelAnimationFrame(frameId);Timer Methods
// Set timeout
const timeoutId = tt.setTimeout(() => {
// Timeout logic
}, 1000);
// Clear timeout
tt.clearTimeout(timeoutId);getSharedCanvas
Gets the shared canvas for rendering Open Data Domain content.
const canvas = tt.getSharedCanvas();
const ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 200;createImage
Creates an image object for loading and displaying images.
const image = tt.createImage();
image.src = 'https://example.com/image.png';getFriendCloudStorage
Retrieves cloud storage data for the user's friends, essential for building leaderboards.
GameGlobal.tt.getFriendCloudStorage({
keyList: ['highScore', 'level'],
success: (data: getFriendCloudStorageResponse) => {
GameGlobal.tt.showInfo('success:' + JSON.stringify(data));
},
fail: (err) => {
GameGlobal.tt.showInfo('fail:' + JSON.stringify(err));
},
complete: () => {
console.log('=============> complete');
}
});Response Interface
interface FriendCloudStorageData {
display_name?: string;
avatar_url?: string;
open_id?: string;
data?: UserCloudStorageKVData[];
user_id?: number;
}
interface UserCloudStorageKVData {
key?: string;
value?: string;
}
interface getFriendCloudStorageResponse {
data?: FriendCloudStorageData[];
status_code?: number;
status_msg?: string;
}Response Fields
Field | Type | Description |
| string | Friend's display name |
| string | URL to friend's avatar image |
| string | Friend's unique OpenID |
| Array | Array of key-value storage data |
| number | Friend's user ID |
getUserCloudStorage
Retrieves the current user's cloud storage data within the Open Data Domain.
GameGlobal.tt.getUserCloudStorage({
keyList: ['key_test_abc'],
success: (data: getUserCloudStorageResponse) => {
GameGlobal.tt.showInfo('success:' + JSON.stringify(data));
},
fail: (err) => {
GameGlobal.tt.showInfo('fail:' + JSON.stringify(err));
},
complete: () => {
console.log('=============> complete');
}
});Response Interface
interface UserCloudStorageKVData {
key?: string;
value?: string;
}
interface getUserCloudStorageResponse {
data?: UserCloudStorageKVData[];
}setUserCloudStorage (Open Data Domain)
Sets cloud storage data from within the Open Data Domain.
GameGlobal.tt.setUserCloudStorage({
data: [{ key: 'key_test_abc', value: 'value_test_abc' }],
success: (data) => {
GameGlobal.tt.showInfo('success:' + JSON.stringify(data));
},
fail: (err) => {
GameGlobal.tt.showInfo('fail:' + JSON.stringify(err));
},
complete: () => {
console.log('=============> complete');
}
});removeUserCloudStorage (Open Data Domain)
Removes cloud storage data from within the Open Data Domain.
GameGlobal.tt.removeUserCloudStorage({
keyList: ['key_test_abc'],
success: (data) => {
GameGlobal.tt.showInfo('success:' + JSON.stringify(data));
},
fail: (err) => {
GameGlobal.tt.showInfo('fail:' + JSON.stringify(err));
},
complete: () => {
console.log('=============> complete');
}
});Social Sharing Methods
shareToUser
Initiates sharing to a specific friend via direct message. Can only be called from within the Open Data Domain.
tt.shareToUser({
imageUrl: 'https://example.com/share-image.png',
openid: 'friend_openid_from_leaderboard',
subtitle: 'Check out my new high score!',
templateType: 1,
title: 'Beat my score!',
success: () => {
console.log('==========> success');
},
fail: () => {
console.log('=========> fail');
}
});Parameters
Field | Type | Description | Required |
| string | URL of image to display in share card | Yes |
| string | Friend's OpenID (obtained from leaderboard data) | Yes |
| string | Subtitle text in share card | No |
| number | Visual template style: | No |
| string | Main title in share card | No |
| function | Callback on successful share | No |
| function | Callback on share failure | No |
Template types
Two visual templates are available:
- Template Type 1: Standard share card with title, subtitle, and image
- Template Type 2: Alternative layout with different visual styling
shareAppMessage
Initiates sharing via direct message from the main game domain, opening the contact selection interface. Can only be called from the main game domain.
tt.shareAppMessage({
imageUrl: 'https://example.com/share-image.png',
subtitle: 'Join me in this amazing game!',
templateType: 1,
title: 'Play with me!',
query: 'a=1&b=2',
path: 'sub_package_path',
success: () => {
console.log('========> success');
},
fail: () => {
console.log('=========> error');
}
});Parameters
Field | Type | Description | Required |
| string | URL of image to display in share card | Yes |
| string | Subtitle text | No |
| number | Visual template style: | No |
| string | Main title | No |
| string | Query string appended to launch parameters | No |
| string | Subpackage path for loading specific game content | No |
| function | Callback on successful share | No |
| function | Callback on share failure | No |
shareToStory
Allows users to share content to their TikTok story. Can only be called from the main game domain.
tt.shareToStory({
success: () => {
console.log('==========> success');
},
fail: () => {
console.log('=========> fail');
}
});Implementation example
Main Game Domain Code
class LeaderboardManager {
private openDataContext: any;
constructor() {
this.openDataContext = TTMinis.game.getOpenDataContext();
}
// Show friend leaderboard
showLeaderboard() {
// Request authorization
TTMinis.game.authorizeOpenContext({
get_status_only: false,
success: (result) => {
// Send message to open data domain
this.openDataContext.postMessage({
type: 'showLeaderboard',
data: {
authCode: result.code
}
});
},
fail: (error) => {
console.error('Authorization failed:', error);
}
});
}
// Hide leaderboard
hideLeaderboard() {
this.openDataContext.postMessage({
type: 'hide'
});
}
// Save high score
saveHighScore(score: number) {
TTMinis.game.setUserCloudStorage({
data: [
{ key: 'highScore', value: score.toString() }
],
success: () => {
console.log('High score saved');
}
});
}
}Open Data Domain Code (openDataContext/index.js)
// Listen for messages from main domain
tt.onMessage((message) => {
switch (message.type) {
case 'showLeaderboard':
renderLeaderboard();
break;
case 'hide':
clearCanvas();
break;
}
});
// Get canvas context
const canvas = tt.getSharedCanvas();
const ctx = canvas.getContext('2d');
// Render leaderboard
function renderLeaderboard() {
tt.getFriendCloudStorage({
keyList: ['highScore'],
success: (result) => {
const friends = result.data;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Sort by high score
friends.sort((a, b) => {
const scoreA = parseInt(a.data?.[0]?.value || '0');
const scoreB = parseInt(b.data?.[0]?.value || '0');
return scoreB - scoreA;
});
// Draw leaderboard
friends.forEach((friend, index) => {
const y = index * 80 + 20;
// Draw rank number
ctx.fillStyle = '#333333';
ctx.font = 'bold 20px sans-serif';
ctx.fillText(`${index + 1}`, 20, y + 40);
// Draw avatar
const avatar = tt.createImage();
avatar.src = friend.avatar_url;
avatar.onload = () => {
ctx.drawImage(avatar, 60, y, 60, 60);
};
// Draw name
ctx.fillStyle = '#333333';
ctx.font = '16px sans-serif';
ctx.fillText(friend.display_name, 140, y + 30);
// Draw high score
const score = friend.data?.[0]?.value || '0';
ctx.fillStyle = '#666666';
ctx.font = '14px sans-serif';
ctx.fillText(`Score: ${score}`, 140, y + 50);
});
}
});
}
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}Best practices
User experience
- Timing: Display leaderboard prompts at meaningful moments (after level completion, on high score achievement)
- Visibility: Make the "View Leaderboard" button prominent but non-intrusive
- Progressive Disclosure: Start with a simple button and expand to full leaderboard on interaction
Performance
- Data Caching: Cache friend data locally to reduce API calls
- Lazy Loading: Load leaderboard data only when requested by the user
- Canvas Optimization: Minimize redraws in the Open Data Domain
Privacy and compliance
- Incremental Authorization: Request only the permissions you need
- Clear Value Proposition: Explain to users why accessing their friend data enhances their experience
- Graceful Degradation: Provide alternative experiences for users who decline authorization
Troubleshooting
Issue | Possible Cause | Solution |
Authorization dialog not appearing |
| Set |
Friend data empty | User has no mutual followers playing the game | Display a message encouraging users to invite friends |
Canvas not displaying | Incorrect canvas positioning | Verify |
| Open Data Domain not properly configured | Check |
Images not loading in Open Data Domain | Cross-origin restrictions | Ensure image URLs allow cross-origin access |