Overview
Family member roles control what actions users can perform within a family. Zen Nurture uses a role-based access control system with three primary roles: owner , admin , and caregiver .
Schema
The familyMembers table links users to families with specific roles:
familyMembers : defineTable ({
familyId: v . id ( "families" ),
userId: v . string (),
role: v . string (),
joinedAt: v . string (),
})
. index ( "by_familyId" , [ "familyId" ])
. index ( "by_userId" , [ "userId" ])
. index ( "by_familyId_userId" , [ "familyId" , "userId" ])
Role Types
Owner
Full administrative control
Cannot be removed
One per family
Automatically assigned at family creation
Admin
Send invitations
Remove members (except owner)
Manage family settings
Full data access
Caregiver
View family data
Log baby events
Create caregiver profiles
Limited administrative access
Role Permissions
Inviting Members
Only owner and admin roles can send family invitations.
export const inviteCaregiver = mutation ({
args: {
familyId: v . id ( "families" ),
email: v . string (),
role: v . optional ( v . string ()),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
const membership = await ctx . db
. query ( "familyMembers" )
. withIndex ( "by_familyId_userId" , ( q ) =>
q . eq ( "familyId" , args . familyId ). eq ( "userId" , user . _id )
)
. first ();
if ( ! membership || ! [ "owner" , "admin" ]. includes ( membership . role )) {
throw new Error ( "Only owners and admins can invite caregivers" );
}
// Create invitation...
},
});
Removing Members
Owners cannot be removed from a family. Only owners and admins can remove other members.
export const removeFamilyMember = mutation ({
args: {
familyId: v . id ( "families" ),
memberId: v . id ( "familyMembers" ),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
// Check requester permissions
const myMembership = await ctx . db
. query ( "familyMembers" )
. withIndex ( "by_familyId_userId" , ( q ) =>
q . eq ( "familyId" , args . familyId ). eq ( "userId" , user . _id )
)
. first ();
if ( ! myMembership || ! [ "owner" , "admin" ]. includes ( myMembership . role )) {
throw new Error ( "Only owners and admins can remove members" );
}
// Verify target member
const target = await ctx . db . get ( args . memberId );
if ( ! target || target . familyId !== args . familyId ) {
throw new Error ( "Member not found in this family" );
}
if ( target . role === "owner" ) {
throw new Error ( "Cannot remove the family owner" );
}
await ctx . db . delete ( args . memberId );
},
});
Creating Families
When a user creates a family, they automatically become the owner :
export const createFamily = mutation ({
args: { name: v . string () },
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
const now = new Date (). toISOString ();
// Create family record
const familyId = await ctx . db . insert ( "families" , {
name: args . name ,
ownerId: user . _id ,
createdAt: now ,
});
// Automatically add creator as owner
await ctx . db . insert ( "familyMembers" , {
familyId ,
userId: user . _id ,
role: "owner" ,
joinedAt: now ,
});
return familyId ;
},
});
Listing Family Members
Retrieve all members of a family with enriched user information:
export const listFamilyMembers = query ({
args: { familyId: v . id ( "families" ) },
handler : async ( ctx , args ) => {
const user = await authComponent . safeGetAuthUser ( ctx );
if ( ! user ) return [];
// Verify requesting user is a member
const myMembership = await ctx . db
. query ( "familyMembers" )
. withIndex ( "by_familyId_userId" , ( q ) =>
q . eq ( "familyId" , args . familyId ). eq ( "userId" , user . _id )
)
. first ();
if ( ! myMembership ) return [];
// Get all family members
const members = await ctx . db
. query ( "familyMembers" )
. withIndex ( "by_familyId" , ( q ) => q . eq ( "familyId" , args . familyId ))
. collect ();
// Enrich with user details
const enriched = await Promise . all (
members . map ( async ( m ) => {
const memberUser = await authComponent . getAnyUserById ( ctx , m . userId );
return {
... m ,
userName: memberUser ?. name ?? "Unknown" ,
userEmail: memberUser ?. email ?? "" ,
};
})
);
return enriched ;
},
});
Access Control Pattern
Use a consistent pattern for role-based access control:
Authenticate user
const user = await requireAuth ( ctx );
Fetch user's membership
const membership = await ctx . db
. query ( "familyMembers" )
. withIndex ( "by_familyId_userId" , ( q ) =>
q . eq ( "familyId" , args . familyId ). eq ( "userId" , user . _id )
)
. first ();
Check role permissions
if ( ! membership || ! [ "owner" , "admin" ]. includes ( membership . role )) {
throw new Error ( "Insufficient permissions" );
}
Perform authorized action
// Execute the privileged operation
Viewing User’s Families
Users can be members of multiple families. List all families for the current user:
export const listMyFamilies = query ({
args: {},
handler : async ( ctx ) => {
const user = await authComponent . safeGetAuthUser ( ctx );
if ( ! user ) return [];
// Find all family memberships
const memberships = await ctx . db
. query ( "familyMembers" )
. withIndex ( "by_userId" , ( q ) => q . eq ( "userId" , user . _id ))
. collect ();
// Fetch family details with role
const families = await Promise . all (
memberships . map ( async ( m ) => {
const family = await ctx . db . get ( m . familyId );
return family ? { ... family , role: m . role } : null ;
})
);
return families . filter ( Boolean );
},
});
Role Assignment on Invitation
When inviting members, you can specify their role (defaults to “caregiver”):
// Invite as caregiver (default)
await inviteCaregiver ({
familyId: family . _id ,
email: "user@example.com"
});
// Invite as admin
await inviteCaregiver ({
familyId: family . _id ,
email: "admin@example.com" ,
role: "admin"
});
The role is stored in the invitation and applied when the user accepts:
const inviteId = await ctx . db . insert ( "familyInvitations" , {
familyId: args . familyId ,
email: args . email ,
role: args . role ?? "caregiver" , // Default to caregiver
invitedBy: user . _id ,
status: "pending" ,
createdAt: now . toISOString (),
expiresAt: expiresAt . toISOString (),
});
Checking Family Access
Helper function for verifying a user has access to a baby through family membership:
export async function requireBabyAccess (
ctx : QueryCtx | MutationCtx ,
babyId : Id < "babyProfiles" >,
userId : string
) {
const babyProfile = await ctx . db . get ( babyId );
if ( ! babyProfile ) throw new Error ( "Baby profile not found" );
if ( ! babyProfile . familyId ) throw new Error ( "Baby not associated with family" );
const familyIds = await getUserFamilyIds ( ctx , userId );
if ( ! familyIds . includes ( babyProfile . familyId )) {
throw new Error ( "Not a member of this family" );
}
}
Best Practices
Always verify membership before queries
Even for read-only operations, check that the user is a family member: const myMembership = await ctx . db
. query ( "familyMembers" )
. withIndex ( "by_familyId_userId" , ( q ) =>
q . eq ( "familyId" , args . familyId ). eq ( "userId" , user . _id )
)
. first ();
if ( ! myMembership ) return [];
Use role arrays for permission checks
Make permission checks readable and maintainable: if ( ! [ "owner" , "admin" ]. includes ( membership . role )) {
throw new Error ( "Insufficient permissions" );
}
Always prevent owner deletion: if ( target . role === "owner" ) {
throw new Error ( "Cannot remove the family owner" );
}
Return role with family data
Include the user’s role when returning family information: const family = await ctx . db . get ( args . familyId );
return family ? { ... family , role: membership . role } : null ;
Permission Matrix
Action Owner Admin Caregiver Create family ✓ ✓ ✓ View family data ✓ ✓ ✓ Send invitations ✓ ✓ ✗ Remove members ✓ ✓ ✗ Remove owner ✗ ✗ ✗ Log baby events ✓ ✓ ✓ Create caregivers ✓ ✓ ✓ Delete family ✓ ✗ ✗
Family Invitations Learn how to invite new members to your family
Caregivers Set up caregiver profiles for event attribution