UNPKG

oneie

Version:

Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.

815 lines (674 loc) 22.1 kB
--- title: Groups dimension: connections category: groups.md tags: groups, multi-tenancy, hierarchical, ontology related_dimensions: people, things, connections, events, knowledge scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the connections dimension in the groups.md category. Location: one/connections/groups.md Purpose: Documents the groups dimension - multi-tenant isolation and hierarchical containers Related dimensions: people, things, connections, events, knowledge For AI agents: Read this to understand groups, multi-tenancy, and hierarchical nesting. --- # Groups Dimension - Multi-Tenant Isolation & Hierarchical Containers **Version:** 1.0.0 **Status:** Complete - Production Ready **Purpose:** The foundation dimension that enables multi-tenancy and hierarchical organization --- ## Why Groups Are Dimension #1 **Groups are the FIRST dimension because they partition ALL other dimensions.** Without groups, you have a single-tenant system. With groups, you have: - **Multi-tenancy:** Each group has isolated data, billing, quotas, and customization - **Hierarchical organization:** Groups can contain groups (friend circle → team → company → government) - **Data scoping:** Every entity, connection, event, and knowledge item belongs to a group - **Access control:** Group membership determines what users can see and do - **Infinite scale:** From 2-person friend circles to billion-person governments **Key principle:** `groupId` is the FIRST field in every dimension's scoping logic. --- ## Conceptual Model ### Groups as Containers ``` ┌─────────────────────────────────────────────────────────────────┐ │ GROUPS (Dimension 1) │ │ The Container Dimension │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ Group: "Acme Corp" (type: organization) │ │ │ │ ├─ People: [Alice (owner), Bob (user), ...] │ │ │ │ ├─ Things: [Products, Courses, Agents, ...] │ │ │ │ ├─ Connections: [Alice owns Product X, ...] │ │ │ │ ├─ Events: [Product created, User joined, ...] │ │ │ │ └─ Knowledge: [Embeddings, Labels, ...] │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ │ │ Child Group: "Engineering Team" │ │ │ │ │ │ (parentGroupId: Acme Corp) │ │ │ │ │ │ ├─ People: [Carol, Dave, ...] │ │ │ │ │ │ ├─ Things: [Projects, Tasks, ...] │ │ │ │ │ │ └─ ... │ │ │ │ │ └─────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ Groups partition EVERYTHING. All data lives inside a group. │ └─────────────────────────────────────────────────────────────────┘ ``` **Visual hierarchy:** ``` Government (billions of people) ↓ State (millions) ↓ City (thousands) ↓ Business (hundreds) ↓ Team (tens) ↓ Friend Circle (2-10) ``` **Every level uses the SAME schema.** That's the power of the ontology. --- ## Schema Definition ### Group Table ```typescript interface Group { _id: Id<"groups">; slug: string; // Unique identifier (URL-friendly) name: string; // Display name type: GroupType; // 6 types: friend_circle, business, community, dao, government, organization parentGroupId?: Id<"groups">; // CRITICAL: Enables hierarchical nesting description?: string; // Optional description metadata: any; // Flexible additional data settings: { visibility: "public" | "private"; joinPolicy: "open" | "invite_only" | "approval_required"; plan?: "starter" | "pro" | "enterprise"; limits?: { users: number; storage: number; apiCalls: number; }; }; status: "active" | "archived"; createdAt: number; updatedAt: number; } ``` ### 6 Group Types **1. friend_circle** - **Scale:** 2-10 people - **Use case:** Personal networks, small collaborations - **Example:** "Running Club", "Book Club", "Family" **2. business** - **Scale:** 10-1000 people - **Use case:** Companies, startups, agencies - **Example:** "Acme Inc", "Marketing Agency", "Consulting Firm" **3. community** - **Scale:** 100-100,000 people - **Use case:** Online communities, forums, fan clubs - **Example:** "React Developers", "Fitness Enthusiasts", "Crypto Traders" **4. dao** - **Scale:** 100-1,000,000 people - **Use case:** Decentralized organizations, blockchain governance - **Example:** "DeFi DAO", "NFT Community DAO", "Protocol Governance" **5. government** - **Scale:** 1,000-1,000,000,000 people - **Use case:** Cities, states, nations - **Example:** "City of San Francisco", "State of California", "United States" **6. organization** - **Scale:** 1,000-100,000 people (deprecated - use business instead) - **Use case:** Large enterprises, institutions - **Example:** "Fortune 500 Company", "University", "Hospital Network" --- ## Hierarchical Nesting ### The Power of `parentGroupId` **Groups can contain groups infinitely.** This enables: 1. **Organizational structure:** Company → Department → Team → Project 2. **Geographic hierarchy:** Country → State → City → Neighborhood 3. **Product hierarchy:** Platform → Feature → Component → Subcomponent 4. **Access inheritance:** Parent group owners can access child groups (configurable) ### Example: Corporate Hierarchy ```typescript // Top-level organization const acmeCorpId = await db.insert("groups", { slug: "acme-corp", name: "Acme Corporation", type: "business", parentGroupId: undefined, // Top level (no parent) settings: { visibility: "public", joinPolicy: "invite_only", plan: "enterprise" }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); // Child group: Engineering department const engineeringId = await db.insert("groups", { slug: "acme-corp-engineering", name: "Engineering", type: "business", parentGroupId: acmeCorpId, // Nested under Acme Corp settings: { visibility: "private", joinPolicy: "invite_only" }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); // Grandchild group: Frontend team const frontendId = await db.insert("groups", { slug: "acme-corp-frontend", name: "Frontend Team", type: "business", parentGroupId: engineeringId, // Nested under Engineering settings: { visibility: "private", joinPolicy: "approval_required" }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); ``` **Visual hierarchy:** ``` Acme Corporation (parentGroupId: undefined) │ ├─ Engineering (parentGroupId: acmeCorpId) │ ├─ Frontend Team (parentGroupId: engineeringId) │ ├─ Backend Team (parentGroupId: engineeringId) │ └─ DevOps Team (parentGroupId: engineeringId) │ ├─ Marketing (parentGroupId: acmeCorpId) │ ├─ Content Team (parentGroupId: marketingId) │ └─ Growth Team (parentGroupId: marketingId) │ └─ Sales (parentGroupId: acmeCorpId) └─ Enterprise Sales (parentGroupId: salesId) ``` ### Querying Hierarchies **Get all child groups:** ```typescript const childGroups = await db .query("groups") .withIndex("by_parent", q => q.eq("parentGroupId", parentGroupId)) .collect(); ``` **Get all descendants (recursive):** ```typescript async function getAllDescendants(db, groupId, descendants = []) { const children = await db .query("groups") .withIndex("by_parent", q => q.eq("parentGroupId", groupId)) .collect(); for (const child of children) { descendants.push(child); await getAllDescendants(db, child._id, descendants); } return descendants; } ``` **Get all ancestors (path to root):** ```typescript async function getAncestors(db, groupId, ancestors = []) { const group = await db.get(groupId); if (!group) return ancestors; if (group.parentGroupId) { const parent = await db.get(group.parentGroupId); if (parent) { ancestors.push(parent); await getAncestors(db, parent._id, ancestors); } } return ancestors; } ``` --- ## Multi-Tenancy via `groupId` ### Universal Scoping Pattern **CRITICAL:** Every dimension MUST be scoped to a `groupId`. **Schema pattern (ALL dimensions follow this):** ```typescript // Dimension 3: Things entities: defineTable({ groupId: v.id("groups"), // REQUIRED: Multi-tenant scope type: v.string(), name: v.string(), properties: v.any(), // ... }) .index("by_group", ["groupId"]) .index("group_type", ["groupId", "type"]); // Dimension 4: Connections connections: defineTable({ groupId: v.id("groups"), // REQUIRED: Multi-tenant scope fromEntityId: v.id("entities"), toEntityId: v.id("entities"), relationshipType: v.string(), // ... }) .index("by_group", ["groupId"]) .index("group_type", ["groupId", "relationshipType"]); // Dimension 5: Events events: defineTable({ groupId: v.id("groups"), // REQUIRED: Multi-tenant scope type: v.string(), actorId: v.optional(v.id("entities")), targetId: v.optional(v.id("entities")), // ... }) .index("by_group", ["groupId"]) .index("group_type", ["groupId", "type"]); // Dimension 6: Knowledge knowledge: defineTable({ groupId: v.id("groups"), // REQUIRED: Multi-tenant scope knowledgeType: v.string(), text: v.optional(v.string()), // ... }) .index("by_group", ["groupId"]) .index("group_type", ["groupId", "knowledgeType"]); ``` **Query pattern (ALWAYS filter by groupId first):** ```typescript // ✅ CORRECT: Group-scoped query const entities = await db .query("entities") .withIndex("group_type", q => q.eq("groupId", groupId).eq("type", "user") ) .collect(); // ❌ WRONG: Unscoped query (cross-tenant data leak!) const entities = await db .query("entities") .withIndex("by_type", q => q.eq("type", "user")) .collect(); ``` ### Data Isolation Guarantees **What multi-tenancy provides:** 1. **Data isolation:** Group A cannot see Group B's data (unless explicitly shared) 2. **Billing isolation:** Each group has independent quotas and usage tracking 3. **Feature isolation:** Groups can enable/disable features independently 4. **Customization isolation:** Groups can customize branding, settings, workflows 5. **Performance isolation:** Heavy usage in Group A doesn't slow Group B **Security rules:** - Every mutation MUST validate `groupId` exists and is active - Every query MUST filter by `groupId` first (use compound indexes) - Cross-group references MUST be validated explicitly - Access control MUST check group membership before operations --- ## Group Lifecycle ### Creating a Group ```typescript export const createGroup = mutation({ args: { slug: v.string(), name: v.string(), type: v.string(), parentGroupId: v.optional(v.id("groups")) }, handler: async (ctx, args) => { // 1. Validate slug uniqueness const existing = await ctx.db .query("groups") .withIndex("by_slug", q => q.eq("slug", args.slug)) .first(); if (existing) { throw new Error("Slug already taken"); } // 2. Validate parent group (if nesting) if (args.parentGroupId) { const parent = await ctx.db.get(args.parentGroupId); if (!parent || parent.status !== "active") { throw new Error("Invalid parent group"); } } // 3. Create group const groupId = await ctx.db.insert("groups", { slug: args.slug, name: args.name, type: args.type, parentGroupId: args.parentGroupId, metadata: {}, settings: { visibility: "public", joinPolicy: "invite_only", plan: "starter" }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); return groupId; } }); ``` ### Updating a Group ```typescript export const updateGroup = mutation({ args: { groupId: v.id("groups"), name: v.optional(v.string()), description: v.optional(v.string()), settings: v.optional(v.any()) }, handler: async (ctx, args) => { // 1. Get existing group const group = await ctx.db.get(args.groupId); if (!group) { throw new Error("Group not found"); } // 2. Update fields const updates: any = { updatedAt: Date.now() }; if (args.name) updates.name = args.name; if (args.description !== undefined) updates.description = args.description; if (args.settings) { updates.settings = { ...group.settings, ...args.settings }; } await ctx.db.patch(args.groupId, updates); return args.groupId; } }); ``` ### Archiving a Group ```typescript export const archiveGroup = mutation({ args: { groupId: v.id("groups") }, handler: async (ctx, args) => { // 1. Get group const group = await ctx.db.get(args.groupId); if (!group) { throw new Error("Group not found"); } // 2. Archive all child groups first const children = await ctx.db .query("groups") .withIndex("by_parent", q => q.eq("parentGroupId", args.groupId)) .collect(); for (const child of children) { await ctx.db.patch(child._id, { status: "archived", updatedAt: Date.now() }); } // 3. Archive parent group await ctx.db.patch(args.groupId, { status: "archived", updatedAt: Date.now() }); return args.groupId; } }); ``` --- ## Access Control Patterns ### Group Membership **People belong to groups via entity type and groupId:** ```typescript // User entity in a group const user = await db.insert("entities", { groupId: groupId, type: "creator", name: "Alice", properties: { email: "alice@example.com", role: "org_owner", // Authorization level groups: [groupId] // Can belong to multiple groups }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); ``` ### Role-Based Access **4 roles with different permissions:** 1. **platform_owner** - Full access to ALL groups - Can create/delete any group - Can modify platform settings 2. **org_owner** - Admin access to their group + child groups - Can invite users, manage settings - Can create child groups 3. **org_user** - Standard access to their group - Can create content, use features - Cannot modify group settings 4. **customer** - Read-only access (for purchased content) - Can view products, courses, content - Cannot create or modify ### Access Validation Pattern ```typescript export const validateGroupAccess = async ( db, groupId: Id<"groups">, userId: string, requiredRole?: string ) => { // 1. Get group const group = await db.get(groupId); if (!group || group.status !== "active") { throw new Error("Group not found or inactive"); } // 2. Get user in group const user = await db .query("entities") .withIndex("group_type", q => q.eq("groupId", groupId).eq("type", "creator") ) .filter(q => q.eq(q.field("properties.userId"), userId)) .first(); if (!user) { throw new Error("User not in group"); } // 3. Check role (if required) if (requiredRole) { const userRole = user.properties.role; if (userRole === "platform_owner") { return true; // Platform owners have access to everything } if (userRole !== requiredRole) { throw new Error("Insufficient permissions"); } } return true; }; ``` --- ## Usage Tracking & Quotas ### Per-Group Usage ```typescript // Track usage per group per metric usage: defineTable({ groupId: v.id("groups"), metric: v.string(), // "users", "storage_gb", "api_calls", "entities_total" period: v.string(), // "daily", "monthly", "annual" value: v.number(), // Current usage limit: v.number(), // Quota limit timestamp: v.number(), periodStart: v.optional(v.number()), periodEnd: v.optional(v.number()), metadata: v.optional(v.any()) }) .index("by_group_period", ["groupId", "period"]) .index("by_group_metric", ["groupId", "metric"]) ``` ### Quota Enforcement ```typescript export const checkQuota = async ( db, groupId: Id<"groups">, metric: string ) => { const usage = await db .query("usage") .withIndex("by_group_metric", q => q.eq("groupId", groupId).eq("metric", metric) ) .first(); if (!usage) { throw new Error("Usage metric not found"); } if (usage.value >= usage.limit) { throw new Error(`Quota exceeded for ${metric}`); } return usage; }; ``` --- ## Key Patterns for AI Agents ### Pattern 1: Always Validate Group First ```typescript // ALWAYS start with group validation const group = await ctx.db.get(args.groupId); if (!group || group.status !== "active") { throw new Error("Invalid group"); } ``` ### Pattern 2: Always Scope Queries by groupId ```typescript // ALWAYS use compound index with groupId first const entities = await ctx.db .query("entities") .withIndex("group_type", q => q.eq("groupId", args.groupId).eq("type", args.type) ) .collect(); ``` ### Pattern 3: Always Include groupId in Inserts ```typescript // ALWAYS include groupId when creating entities const entityId = await ctx.db.insert("entities", { groupId: args.groupId, // REQUIRED type: args.type, name: args.name, properties: args.properties || {}, status: "draft", createdAt: Date.now(), updatedAt: Date.now() }); ``` ### Pattern 4: Respect Hierarchical Access ```typescript // Check if user has access to group or parent groups const hasAccess = await checkHierarchicalAccess( db, groupId, userId ); ``` --- ## Examples ### Example 1: Lemonade Stand (friend_circle) ```typescript const lemonadeStandId = await db.insert("groups", { slug: "toms-lemonade", name: "Tom's Lemonade Stand", type: "friend_circle", parentGroupId: undefined, settings: { visibility: "public", joinPolicy: "open", plan: "starter" }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); // Add products await db.insert("entities", { groupId: lemonadeStandId, type: "product", name: "Lemonade Cup", properties: { price: 2.00, inventory: 50 }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); ``` ### Example 2: Enterprise Corporation (business with hierarchy) ```typescript // Parent: Acme Corp const acmeId = await db.insert("groups", { slug: "acme-corp", name: "Acme Corporation", type: "business", parentGroupId: undefined, settings: { visibility: "public", joinPolicy: "invite_only", plan: "enterprise", limits: { users: 10000, storage: 10000, apiCalls: 1000000 } }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); // Child: Engineering team const engineeringId = await db.insert("groups", { slug: "acme-engineering", name: "Engineering", type: "business", parentGroupId: acmeId, settings: { visibility: "private", joinPolicy: "approval_required" }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); ``` ### Example 3: DAO (decentralized community) ```typescript const daoId = await db.insert("groups", { slug: "defi-protocol-dao", name: "DeFi Protocol DAO", type: "dao", parentGroupId: undefined, settings: { visibility: "public", joinPolicy: "open", // Token holders can join plan: "pro" }, metadata: { tokenAddress: "0x...", governanceContract: "0x...", votingThreshold: 1000 }, status: "active", createdAt: Date.now(), updatedAt: Date.now() }); ``` --- ## Summary ### The Groups Dimension Enables 1. **Multi-tenancy:** Complete data isolation per group 2. **Hierarchical organization:** Infinite nesting via `parentGroupId` 3. **Universal scoping:** All dimensions filtered by `groupId` 4. **Flexible access:** Role-based permissions per group 5. **Independent billing:** Quotas and usage tracking per group 6. **Infinite scale:** From friend circles (2 people) to governments (billions) ### Critical Rules for AI Agents 1. **Always validate groupId first** - Check exists and active 2. **Always scope queries by groupId** - Use compound indexes 3. **Always include groupId in inserts** - Multi-tenant isolation 4. **Never skip group validation** - Security critical 5. **Respect hierarchical access** - Parent groups can access children ### Key Files to Reference - `/backend/convex/schema.ts` - Groups table definition - `/one/knowledge/ontology.md` - Complete 6-dimension specification - `/one/knowledge/architecture.md` - Architecture overview --- **Groups are the foundation. Master this dimension, and everything else follows.**