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.

1,171 lines (984 loc) 28.5 kB
--- title: Membership dimension: connections category: membership.md tags: connections, ontology related_dimensions: events, groups, people, things 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 membership.md category. Location: one/connections/membership.md Purpose: Documents membership connections Related dimensions: events, groups, people, things For AI agents: Read this to understand membership. --- # Membership Connections **Last Updated:** 2025-10-08 **Status:** Complete **Layer:** Ontology Connections --- ## Overview **Membership connections** represent formal relationships between users and organizational structures within the ONE Platform. They enable: 1. **Organization Membership** - Users joining multi-tenant organizations with specific roles 2. **Community Membership** - Following creators, joining communities, participating in conversations 3. **Role-Based Access Control** - Granular permissions based on membership roles 4. **Invitation Flows** - Structured processes for adding members to organizations 5. **Multi-Tenant Routing** - Dashboard access based on organization membership **Key Principle:** Memberships are bidirectional relationships stored in the `connections` table with rich metadata defining roles, permissions, and invitation context. --- ## Connection Types The ontology defines **4 membership connection types**: ```typescript type MembershipConnectionType = | 'member_of' // Organization/community membership (with roles) | 'following' // Creator/audience following relationship | 'moderates' // Moderator role in community/conversation | 'participated_in'; // Event/conversation participation ``` ### 1. member_of (Organization/Community Membership) **Purpose:** Links users to organizations or communities with role-based metadata. **Direction:** User Organization/Community **Use Cases:** - Multi-tenant organization membership (org_owner, org_user) - Community membership (member, moderator, admin) - Permission-based access control - Dashboard routing (platform owner vs org owner vs org user) **Metadata Structure:** ```typescript { role: 'platform_owner' | 'org_owner' | 'org_user' | 'member' | 'moderator' | 'admin', permissions: string[], // Array of permission strings invitedBy?: Id<'entities'>, // User who invited this member invitedAt?: number, // Invitation timestamp joinedAt: number, // When user joined expiresAt?: number, // Optional membership expiration } ``` ### 2. following (Creator/Audience Relationship) **Purpose:** Represents audience members following creators for content updates. **Direction:** Audience Member Creator **Use Cases:** - Newsletter subscriptions - Content notifications - Creator analytics (follower count) - Targeted content distribution **Metadata Structure:** ```typescript { followedAt: number, // When follow started notificationPreferences?: { email: boolean, push: boolean, inApp: boolean, }, source?: string, // How they found the creator } ``` ### 3. moderates (Moderator Role) **Purpose:** Grants moderation privileges in communities or conversations. **Direction:** User Community/Conversation **Use Cases:** - Community moderation - Conversation thread moderation - Content approval workflows - User management in communities **Metadata Structure:** ```typescript { permissions: string[], // Specific moderation permissions assignedBy: Id<'entities'>, // Who assigned moderator role assignedAt: number, // When role was assigned scope?: string, // Optional scope restrictions } ``` ### 4. participated_in (Event/Conversation Participation) **Purpose:** Tracks participation in events, conversations, or collaborative spaces. **Direction:** User Event/Conversation **Use Cases:** - Conversation tracking - Event attendance - Collaborative project participation - Activity history **Metadata Structure:** ```typescript { participatedAt: number, // When participation started lastActiveAt: number, // Most recent activity messageCount?: number, // Number of messages/contributions role?: string, // Participant role (optional) } ``` --- ## Ontology Mapping ### 6-Dimension Structure **Things Involved:** - `creator` - Platform owner, org owner, org user (with role property) - `organization` - Multi-tenant organization - `community` - Community space - `conversation` - Discussion thread - `audience_member` - Fan/customer **Connections Created:** ```typescript // Organization membership { fromEntityId: userId, // User entity toEntityId: organizationId, // Organization entity relationshipType: 'member_of', metadata: { role: 'org_owner', permissions: ['*'], joinedAt: Date.now(), } } // Creator following { fromEntityId: audienceMemberId, toEntityId: creatorId, relationshipType: 'following', metadata: { followedAt: Date.now(), notificationPreferences: { email: true } } } // Community moderation { fromEntityId: userId, toEntityId: communityId, relationshipType: 'moderates', metadata: { permissions: ['delete_messages', 'ban_users'], assignedBy: adminUserId, assignedAt: Date.now(), } } // Conversation participation { fromEntityId: userId, toEntityId: conversationId, relationshipType: 'participated_in', metadata: { participatedAt: Date.now(), lastActiveAt: Date.now(), messageCount: 5, } } ``` **Events Generated:** ```typescript // User invited to organization { type: 'user_invited_to_org', actorId: inviterId, targetId: organizationId, metadata: { invitedEmail: 'user@example.com', role: 'org_user', } } // User joined organization { type: 'user_joined_org', actorId: userId, targetId: organizationId, metadata: { role: 'org_user', invitedBy: inviterId, } } // User started following creator { type: 'content_event', actorId: audienceMemberId, targetId: creatorId, metadata: { action: 'followed', source: 'website', } } // Moderator assigned { type: 'user_role_updated', actorId: userId, targetId: communityId, metadata: { newRole: 'moderator', assignedBy: adminId, } } ``` --- ## TypeScript Definitions ### Connection Schema ```typescript import type { Id } from '../_generated/dataModel'; /** * Membership connection in connections table */ export interface MembershipConnection { _id: Id<'connections'>; fromEntityId: Id<'entities'>; // User toEntityId: Id<'entities'>; // Organization/Community/Creator relationshipType: 'member_of' | 'following' | 'moderates' | 'participated_in'; metadata?: MembershipMetadata; createdAt: number; updatedAt?: number; deletedAt?: number; } /** * Membership metadata variants */ export type MembershipMetadata = | OrganizationMembershipMetadata | FollowingMetadata | ModerationMetadata | ParticipationMetadata; /** * Organization membership metadata */ export interface OrganizationMembershipMetadata { role: 'platform_owner' | 'org_owner' | 'org_user' | 'member' | 'moderator' | 'admin'; permissions: string[]; invitedBy?: Id<'entities'>; invitedAt?: number; joinedAt: number; expiresAt?: number; } /** * Creator following metadata */ export interface FollowingMetadata { followedAt: number; notificationPreferences?: { email: boolean; push: boolean; inApp: boolean; }; source?: string; } /** * Moderation metadata */ export interface ModerationMetadata { permissions: string[]; assignedBy: Id<'entities'>; assignedAt: number; scope?: string; } /** * Participation metadata */ export interface ParticipationMetadata { participatedAt: number; lastActiveAt: number; messageCount?: number; role?: string; } ``` --- ## Role-Based Patterns ### Multi-Tenant Organization Roles The ONE Platform supports **4 primary roles** through the `member_of` connection: #### 1. Platform Owner - **User:** Anthony O'Connell (toc@anthonyoconnell.com) - **Role:** `platform_owner` (set in creator entity `properties.role`) - **Access:** Global dashboard, all organizations, revenue tracking - **Connections:** Member of platform administration organization - **Properties:** ```typescript { type: 'creator', properties: { role: 'platform_owner', email: 'toc@anthonyoconnell.com', // ... other properties } } ``` #### 2. Organization Owner - **Role:** `org_owner` (set in connection metadata) - **Access:** Full control over their organization, can invite users, manage billing - **Permissions:** `['*']` (all permissions within org) - **Connection:** ```typescript { fromEntityId: userId, toEntityId: orgId, relationshipType: 'member_of', metadata: { role: 'org_owner', permissions: ['*'], joinedAt: Date.now(), } } ``` #### 3. Organization User - **Role:** `org_user` (set in connection metadata) - **Access:** Limited to assigned permissions - **Permissions:** Granular array of strings - **Connection:** ```typescript { fromEntityId: userId, toEntityId: orgId, relationshipType: 'member_of', metadata: { role: 'org_user', permissions: ['create_agents', 'create_content', 'view_analytics'], invitedBy: orgOwnerId, invitedAt: Date.now(), joinedAt: Date.now(), } } ``` #### 4. Customer - **Role:** `customer` (set in creator entity `properties.role`) - **Access:** Limited to their own resources, no organization context - **Properties:** ```typescript { type: 'audience_member', properties: { role: 'customer', email: 'customer@example.com', } } ``` ### Permission Patterns **Permission strings** are stored in connection metadata and checked by middleware: ```typescript // Common permissions const PERMISSIONS = { // Organization management 'manage_org': 'Update organization settings', 'manage_billing': 'Manage subscriptions and billing', 'manage_users': 'Invite/remove organization members', // Resource creation 'create_agents': 'Create AI agents', 'create_content': 'Create content (posts, videos, courses)', 'create_tokens': 'Create tokens and token contracts', // Resource management 'edit_agents': 'Modify AI agents', 'edit_content': 'Modify content', 'delete_resources': 'Delete any resource', // Analytics 'view_analytics': 'View analytics dashboard', 'export_data': 'Export organization data', // Special '*': 'All permissions (admin)', }; ``` --- ## Code Examples ### 1. Create Organization Membership ```typescript // convex/mutations/organizations.ts import { v } from 'convex/values'; import { mutation } from '../_generated/server'; import type { Id } from '../_generated/dataModel'; export const createOrganization = mutation({ args: { name: v.string(), slug: v.string(), creatorId: v.id('entities'), plan: v.optional(v.string()), }, handler: async (ctx, args) => { // 1. Create organization entity const orgId = await ctx.db.insert('entities', { type: 'organization', name: args.name, properties: { slug: args.slug, plan: args.plan || 'starter', settings: { limits: { users: 10, agents: 3 }, features: { aiClone: false }, }, }, status: 'active', createdAt: Date.now(), updatedAt: Date.now(), }); // 2. Add creator as org_owner await ctx.db.insert('connections', { fromEntityId: args.creatorId, toEntityId: orgId, relationshipType: 'member_of', metadata: { role: 'org_owner', permissions: ['*'], joinedAt: Date.now(), }, createdAt: Date.now(), }); // 3. Log event await ctx.db.insert('events', { type: 'organization_created', actorId: args.creatorId, targetId: orgId, timestamp: Date.now(), metadata: { name: args.name, plan: args.plan || 'starter', }, }); return { orgId, slug: args.slug }; }, }); ``` ### 2. Invite User to Organization ```typescript // convex/mutations/organizations.ts export const inviteUser = mutation({ args: { orgId: v.id('entities'), email: v.string(), role: v.union(v.literal('org_user'), v.literal('admin')), inviterId: v.id('entities'), }, handler: async (ctx, args) => { // 1. Check inviter has permission const inviterMembership = await ctx.db .query('connections') .withIndex('from_type', (q) => q.eq('fromEntityId', args.inviterId).eq('relationshipType', 'member_of') ) .filter((q) => q.eq(q.field('toEntityId'), args.orgId)) .first(); if (!inviterMembership || inviterMembership.metadata?.role !== 'org_owner') { throw new Error('Insufficient permissions'); } // 2. Create invitation entity const inviteId = await ctx.db.insert('entities', { type: 'invitation', name: `Invitation for ${args.email}`, properties: { email: args.email, orgId: args.orgId, role: args.role, token: crypto.randomUUID(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days status: 'pending', }, status: 'active', createdAt: Date.now(), updatedAt: Date.now(), }); // 3. Log event await ctx.db.insert('events', { type: 'user_invited_to_org', actorId: args.inviterId, targetId: args.orgId, timestamp: Date.now(), metadata: { invitedEmail: args.email, role: args.role, inviteId, }, }); // 4. TODO: Send invitation email (via internal action) return { inviteId }; }, }); ``` ### 3. Accept Organization Invitation ```typescript // convex/mutations/organizations.ts export const acceptInvitation = mutation({ args: { token: v.string(), userId: v.id('entities'), }, handler: async (ctx, args) => { // 1. Find invitation const invite = await ctx.db .query('entities') .withIndex('by_type', (q) => q.eq('type', 'invitation')) .filter((q) => q.eq(q.field('properties.token'), args.token)) .first(); if (!invite) { throw new Error('Invalid invitation'); } if (invite.properties.status !== 'pending') { throw new Error('Invitation already used'); } if (invite.properties.expiresAt < Date.now()) { throw new Error('Invitation expired'); } // 2. Create membership connection const permissions = invite.properties.role === 'admin' ? ['*'] : ['create_agents', 'create_content', 'view_analytics']; await ctx.db.insert('connections', { fromEntityId: args.userId, toEntityId: invite.properties.orgId, relationshipType: 'member_of', metadata: { role: invite.properties.role, permissions, invitedBy: invite.properties.inviterId, invitedAt: invite.createdAt, joinedAt: Date.now(), }, createdAt: Date.now(), }); // 3. Mark invitation as accepted await ctx.db.patch(invite._id, { properties: { ...invite.properties, status: 'accepted', acceptedAt: Date.now(), acceptedBy: args.userId, }, }); // 4. Log event await ctx.db.insert('events', { type: 'user_joined_org', actorId: args.userId, targetId: invite.properties.orgId, timestamp: Date.now(), metadata: { role: invite.properties.role, invitedBy: invite.properties.inviterId, }, }); return { success: true, orgId: invite.properties.orgId }; }, }); ``` ### 4. Check User Permissions ```typescript // convex/queries/organizations.ts import { v } from 'convex/values'; import { query } from '../_generated/server'; export const checkPermission = query({ args: { userId: v.id('entities'), orgId: v.id('entities'), permission: v.string(), }, handler: async (ctx, args) => { // 1. Get user entity (check for platform_owner role) const user = await ctx.db.get(args.userId); if (user?.properties.role === 'platform_owner') { return true; // Platform owner has all permissions everywhere } // 2. Get membership connection const membership = await ctx.db .query('connections') .withIndex('from_type', (q) => q.eq('fromEntityId', args.userId).eq('relationshipType', 'member_of') ) .filter((q) => q.eq(q.field('toEntityId'), args.orgId)) .first(); if (!membership) { return false; // Not a member } // 3. Check permissions const permissions = membership.metadata?.permissions || []; return permissions.includes('*') || permissions.includes(args.permission); }, }); export const getUserRole = query({ args: { userId: v.id('entities'), orgId: v.id('entities'), }, handler: async (ctx, args) => { // Check for platform_owner first const user = await ctx.db.get(args.userId); if (user?.properties.role === 'platform_owner') { return 'platform_owner'; } // Get membership const membership = await ctx.db .query('connections') .withIndex('from_type', (q) => q.eq('fromEntityId', args.userId).eq('relationshipType', 'member_of') ) .filter((q) => q.eq(q.field('toEntityId'), args.orgId)) .first(); return membership?.metadata?.role || null; }, }); ``` ### 5. List Organization Members ```typescript // convex/queries/organizations.ts export const listMembers = query({ args: { orgId: v.id('entities'), }, handler: async (ctx, args) => { // 1. Get all member_of connections for this org const memberships = await ctx.db .query('connections') .withIndex('to_type', (q) => q.eq('toEntityId', args.orgId).eq('relationshipType', 'member_of') ) .collect(); // 2. Get user entities const members = await Promise.all( memberships.map(async (membership) => { const user = await ctx.db.get(membership.fromEntityId); return { userId: membership.fromEntityId, name: user?.name, email: user?.properties.email, role: membership.metadata?.role, permissions: membership.metadata?.permissions, joinedAt: membership.metadata?.joinedAt, invitedBy: membership.metadata?.invitedBy, }; }) ); return members; }, }); ``` --- ## Multi-Tenant Dashboard Routing ### Dashboard Access Matrix | Role | Dashboard Route | Access Level | |-------------------|---------------------------|---------------------------------------| | `platform_owner` | `/admin/dashboard` | All orgs, global analytics, revenue | | `org_owner` | `/org/:slug/dashboard` | Full org control, billing, users | | `org_user` | `/org/:slug/dashboard` | Limited by permissions | | `customer` | `/dashboard` | Personal resources only | ### Route Protection Pattern ```typescript // src/middleware/auth.ts import { defineMiddleware } from 'astro:middleware'; import { ConvexHttpClient } from 'convex/browser'; import { api } from '@/convex/_generated/api'; export const onRequest = defineMiddleware(async (context, next) => { const session = context.locals.session; if (!session) { return context.redirect('/login'); } const convex = new ConvexHttpClient(import.meta.env.PUBLIC_CONVEX_URL); const user = await convex.query(api.queries.users.get, { id: session.userId }); // Platform owner can access anything if (user.properties.role === 'platform_owner') { context.locals.role = 'platform_owner'; return next(); } // Extract org slug from URL const orgSlug = context.params.org; if (!orgSlug) { // Customer accessing personal dashboard context.locals.role = 'customer'; return next(); } // Get organization const org = await convex.query(api.queries.organizations.getBySlug, { slug: orgSlug, }); if (!org) { return context.redirect('/404'); } // Check membership const role = await convex.query(api.queries.organizations.getUserRole, { userId: session.userId, orgId: org._id, }); if (!role) { return context.redirect('/forbidden'); } // Set context context.locals.role = role; context.locals.org = org; return next(); }); ``` ### Frontend Permission Checks ```typescript // src/components/admin/PermissionGate.tsx import { useOrganization } from '@/lib/organization-context'; interface PermissionGateProps { permission: string; children: React.ReactNode; fallback?: React.ReactNode; } export function PermissionGate({ permission, children, fallback = null, }: PermissionGateProps) { const { hasPermission } = useOrganization(); if (!hasPermission(permission)) { return <>{fallback}</>; } return <>{children}</>; } // Usage <PermissionGate permission="manage_billing"> <BillingSettings /> </PermissionGate> ``` --- ## Event Tracking ### Organization Events ```typescript // User invited { type: 'user_invited_to_org', actorId: inviterId, targetId: organizationId, timestamp: Date.now(), metadata: { invitedEmail: 'user@example.com', role: 'org_user', inviteToken: 'inv_xyz', expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, } } // User joined { type: 'user_joined_org', actorId: userId, targetId: organizationId, timestamp: Date.now(), metadata: { role: 'org_user', invitedBy: inviterId, } } // User removed { type: 'user_removed_from_org', actorId: adminId, targetId: organizationId, timestamp: Date.now(), metadata: { removedUserId: userId, reason: 'voluntary_leave', } } // Role updated { type: 'user_role_updated', actorId: adminId, targetId: userId, timestamp: Date.now(), metadata: { orgId: organizationId, oldRole: 'org_user', newRole: 'org_owner', } } ``` ### Community Events ```typescript // User followed creator { type: 'content_event', actorId: audienceMemberId, targetId: creatorId, timestamp: Date.now(), metadata: { action: 'followed', source: 'website', } } // User joined community { type: 'community_joined', actorId: userId, targetId: communityId, timestamp: Date.now(), metadata: { source: 'invitation', } } // Moderator assigned { type: 'moderator_assigned', actorId: adminId, targetId: communityId, timestamp: Date.now(), metadata: { moderatorUserId: userId, permissions: ['delete_messages', 'ban_users'], } } ``` --- ## Query Patterns ### Get User's Organizations ```typescript export const getUserOrganizations = query({ args: { userId: v.id('entities') }, handler: async (ctx, args) => { const memberships = await ctx.db .query('connections') .withIndex('from_type', (q) => q.eq('fromEntityId', args.userId).eq('relationshipType', 'member_of') ) .collect(); const organizations = await Promise.all( memberships.map(async (membership) => { const org = await ctx.db.get(membership.toEntityId); return { ...org, role: membership.metadata?.role, permissions: membership.metadata?.permissions, }; }) ); return organizations.filter(Boolean); }, }); ``` ### Get Organization Members by Role ```typescript export const getMembersByRole = query({ args: { orgId: v.id('entities'), role: v.string(), }, handler: async (ctx, args) => { const memberships = await ctx.db .query('connections') .withIndex('to_type', (q) => q.eq('toEntityId', args.orgId).eq('relationshipType', 'member_of') ) .filter((q) => q.eq(q.field('metadata.role'), args.role)) .collect(); const members = await Promise.all( memberships.map((m) => ctx.db.get(m.fromEntityId)) ); return members.filter(Boolean); }, }); ``` ### Check Multi-Org Membership ```typescript export const isMultiOrgUser = query({ args: { userId: v.id('entities') }, handler: async (ctx, args) => { const memberships = await ctx.db .query('connections') .withIndex('from_type', (q) => q.eq('fromEntityId', args.userId).eq('relationshipType', 'member_of') ) .collect(); return memberships.length > 1; }, }); ``` --- ## Best Practices ### 1. Always Check Role Before Creating Connections ```typescript // Good: Verify inviter has permission const inviterMembership = await ctx.db .query('connections') .withIndex('from_type', (q) => q.eq('fromEntityId', inviterId).eq('relationshipType', 'member_of') ) .filter((q) => q.eq(q.field('toEntityId'), orgId)) .first(); if (inviterMembership?.metadata?.role !== 'org_owner') { throw new Error('Insufficient permissions'); } // Bad: No permission check await ctx.db.insert('connections', { ... }); ``` ### 2. Use Specific Permissions, Not Just Roles ```typescript // Good: Check specific permission const hasPermission = permissions.includes('*') || permissions.includes('manage_users'); // Bad: Check role directly in business logic if (role === 'org_owner') { ... } ``` ### 3. Log All Membership Changes ```typescript // Good: Log every membership event await ctx.db.insert('events', { type: 'user_joined_org', actorId: userId, targetId: orgId, timestamp: Date.now(), metadata: { role, invitedBy }, }); // Bad: Silent membership changes await ctx.db.insert('connections', { ... }); ``` ### 4. Support Multi-Organization Users ```typescript // Good: Users can be members of multiple orgs const orgs = await getUserOrganizations({ userId }); const currentOrg = orgs.find(o => o._id === currentOrgId); // Bad: Assume one org per user const org = await getOrganization({ userId }); ``` --- ## Migration from Legacy Systems ### Step 1: Create Organization Entities ```typescript const defaultOrg = await ctx.db.insert('entities', { type: 'organization', name: 'Default Organization', properties: { slug: 'default' }, status: 'active', createdAt: Date.now(), updatedAt: Date.now(), }); ``` ### Step 2: Create Membership Connections ```typescript const users = await ctx.db.query('users').collect(); for (const user of users) { await ctx.db.insert('connections', { fromEntityId: user._id, toEntityId: defaultOrg, relationshipType: 'member_of', metadata: { role: user.isAdmin ? 'org_owner' : 'org_user', permissions: user.isAdmin ? ['*'] : ['create_content'], joinedAt: user.createdAt, }, createdAt: Date.now(), }); } ``` ### Step 3: Update Queries to Filter by Organization ```typescript // Before: Global query const content = await ctx.db.query('entities') .withIndex('by_type', q => q.eq('type', 'blog_post')) .collect(); // After: Org-scoped query const orgContent = await ctx.db.query('connections') .withIndex('from_type', q => q.eq('fromEntityId', orgId).eq('relationshipType', 'owns') ) .collect(); const content = await Promise.all( orgContent.map(c => ctx.db.get(c.toEntityId)) ); ``` --- ## Summary **Membership connections** are the foundation of ONE Platform's multi-tenant architecture: **Organization membership** with role-based permissions **Creator following** for audience engagement **Community moderation** with granular permissions **Event participation** tracking **Multi-org support** for complex workflows **Permission-based UI rendering** in frontend **Invitation flows** with email verification **Dashboard routing** based on role context **Key Takeaway:** Use connection metadata to store rich membership context (roles, permissions, invitation history) while keeping the ontology clean and scalable. --- **Related Documentation:** - [Ontology.md](../ontology.md) - Complete 6-dimension specification - [Multitenant.md](../multitenant.md) - Multi-tenant architecture patterns - [Owner.md](../things/owner.md) - Platform owner role and revenue tracking - [Organisation.md](../things/organisation.md) - Organization entity details