Skip to main content

Relationships

Build relational data structures by linking documents and users together with automatic population support across all platforms.
Relationships enable you to structure complex data models like social networks, e-commerce systems, and collaborative applications.

Overview

Cocobase supports powerful relationship features:
  • Document references - Store IDs to link documents
  • Population - Automatically fetch related data
  • Nested population - Multi-level relationship resolution
  • User-to-user relationships - Followers, friends, referrals
  • User-to-document relationships - Bookmarks, favorites, ownership
  • Document-to-document relationships - Comments, reviews, hierarchies

Relationship Types

One-to-One

A single reference to another entity.
{
  "id": "user_123",
  "email": "[email protected]",
  "data": {
    "username": "johndoe",
    "referred_by": "user_456"
  }
}

One-to-Many / Many-to-Many

Arrays of references for multiple relationships.
{
  "id": "user_123",
  "email": "[email protected]",
  "data": {
    "username": "johndoe",
    "followers_ids": ["user_456", "user_789", "user_012"],
    "following_ids": ["user_456", "user_999"]
  }
}

Creating Relationships

User to User

import { Cocobase } from 'cocobase';

const db = new Cocobase({ apiKey: 'your-api-key' });

// User signs up with referral
await db.auth.signup({
  email: '[email protected]',
  password: 'password123',
  data: {
    username: 'bob',
    referred_by: 'user_abc123'
  }
});

// Follow a user
const currentUser = await db.auth.getUser();
const currentFollowing = currentUser.data.following_ids || [];

await db.auth.updateUser({
  data: {
    following_ids: [...currentFollowing, 'user_to_follow']
  }
});

// Unfollow a user
await db.auth.updateUser({
  data: {
    following_ids: currentFollowing.filter(id => id !== 'user_to_unfollow')
  }
});

User to Document

// User bookmarks a post
const currentUser = await db.auth.getUser();
const bookmarks = currentUser.data.bookmarked_posts || [];

await db.auth.updateUser({
  data: {
    bookmarked_posts: [...bookmarks, 'post_123']
  }
});

// Remove bookmark
await db.auth.updateUser({
  data: {
    bookmarked_posts: bookmarks.filter(id => id !== 'post_123')
  }
});

Document to Document

// Create a post
const post = await db.createDocument('posts', {
  title: 'My first post',
  content: 'Hello world!',
  author_id: 'user_123'
});

// Create a comment on the post
const comment = await db.createDocument('comments', {
  post_id: post.id,
  author_id: 'user_456',
  text: 'Great post!',
  created_at: new Date().toISOString()
});

Populating Relationships

Automatically fetch related data instead of just IDs.

Basic Population

// Without population (just IDs)
const users = await db.listDocuments('users');
console.log(users[0].data.referred_by); // "user_456"

// With population (full data)
const usersWithReferrer = await db.listDocuments('users', {
  populate: ['referred_by']
});
console.log(usersWithReferrer[0].data.referred_by);
// { id: "user_456", email: "[email protected]", data: { username: "alice" } }

Populate Multiple Fields

const users = await db.listDocuments('users', {
  populate: ['referred_by', 'followers_ids', 'following_ids']
});

console.log(users[0].data.referred_by); // Full user object
console.log(users[0].data.followers_ids); // Array of full user objects
console.log(users[0].data.following_ids); // Array of full user objects

Explicit Source Specification

Force population from specific collections or AppUser model.
// Force fetch from AppUser model
const posts = await db.listDocuments('posts', {
  populate: ['author:appuser']
});

// Force fetch from specific collection
const projects = await db.listDocuments('projects', {
  populate: ['owner:team_members']
});

Social Features

Follow System

class FollowSystem {
  constructor(db, token) {
    this.db = db;
    this.token = token;
  }

  async follow(userIdToFollow) {
    const currentUser = await this.db.auth.getUser();
    const following = currentUser.data.following_ids || [];

    if (following.includes(userIdToFollow)) {
      console.log('Already following');
      return;
    }

    await this.db.auth.updateUser({
      data: {
        following_ids: [...following, userIdToFollow]
      }
    });
  }

  async unfollow(userIdToUnfollow) {
    const currentUser = await this.db.auth.getUser();
    const following = currentUser.data.following_ids || [];

    await this.db.auth.updateUser({
      data: {
        following_ids: following.filter(id => id !== userIdToUnfollow)
      }
    });
  }

  async getFollowers(userId) {
    const response = await this.db.listDocuments('users', {
      filters: { id: userId },
      populate: ['followers_ids']
    });

    return response[0]?.data.followers_ids || [];
  }

  async getFollowing(userId) {
    const response = await this.db.listDocuments('users', {
      filters: { id: userId },
      populate: ['following_ids']
    });

    return response[0]?.data.following_ids || [];
  }
}

// Usage
const followSystem = new FollowSystem(db, userToken);
await followSystem.follow('user_456');
const followers = await followSystem.getFollowers('user_123');

Referral System

async function getReferralStats(db, userId) {
  // Get the user who referred this user
  const [user] = await db.listDocuments('users', {
    filters: { id: userId },
    populate: ['referred_by']
  });

  // Get all users this user referred
  const referrals = await db.listDocuments('users', {
    filters: { 'data.referred_by': userId }
  });

  return {
    referredBy: user.data.referred_by,
    referralCount: referrals.length,
    referrals: referrals
  };
}

// Usage
const stats = await getReferralStats(db, 'user_123');
console.log('Referred by:', stats.referredBy?.data?.username);
console.log('Total referrals:', stats.referralCount);

Best Practices

Population uses optimized batch queries to avoid N+1 problems.
// ✓ Good: Single query with populate
const users = await db.listDocuments('users', {
  populate: ['followers_ids', 'following_ids']
});

// ✗ Bad: Multiple individual queries
for (const user of users) {
  const follower = await db.getDocument('users', user.data.followers_ids[0]);
}
Avoid over-populating to reduce response size and improve performance.
// ✓ Good: Only populate what's needed
const users = await db.listDocuments('users', {
  populate: ['referred_by']
});

// ✗ Bad: Populating unnecessary relationships
const users = await db.listDocuments('users', {
  populate: ['followers_ids', 'following_ids', 'referred_by', 'friends_ids']
});
Don’t store thousands of IDs in a single array field.
// ✓ Good: Reasonable array size (< 1000 items)
followers_ids: ['user_1', 'user_2', ..., 'user_500']

// ✗ Bad: Massive arrays (> 10,000 items)
// Consider a separate junction collection for this
Combine filters and population for efficient queries.
// ✓ Good: Filter and populate together
const activeUsers = await db.listDocuments('users', {
  filters: { status: 'active' },
  populate: ['referred_by']
});
Cache frequently accessed relationships to reduce API calls.
const cache = new Map();

async function getUserWithRelationships(userId, populate) {
  const cacheKey = `${userId}:${populate.join(',')}`;

  if (cache.has(cacheKey)) {
    return cache.get(cacheKey);
  }

  const data = await db.getDocument('users', userId, { populate });
  cache.set(cacheKey, data);

  return data;
}

Next Steps