diff --git a/src/routes/docs/products/auth/+layout.svelte b/src/routes/docs/products/auth/+layout.svelte index aa34ac1014..fff83c72f4 100644 --- a/src/routes/docs/products/auth/+layout.svelte +++ b/src/routes/docs/products/auth/+layout.svelte @@ -94,8 +94,12 @@ href: '/docs/products/auth/mfa' }, { - label: 'Verify user', + label: 'User verification', href: '/docs/products/auth/verify-user' + }, + { + label: 'Team invites', + href: '/docs/products/auth/team-invites' } ] }, diff --git a/src/routes/docs/products/auth/+page.markdoc b/src/routes/docs/products/auth/+page.markdoc index eb6c5abda3..943027e147 100644 --- a/src/routes/docs/products/auth/+page.markdoc +++ b/src/routes/docs/products/auth/+page.markdoc @@ -46,10 +46,13 @@ Implement custom authentication methods like biometric and passkey login by gene {% cards_item href="/docs/products/auth/mfa" title="Multifactor authentication (MFA)" %} Implementing MFA to add extra layers of security to your app. {% /cards_item %} +{% cards_item href="/docs/products/auth/team-invites" title="Team invites" %} +Manage team invites and memberships with client-side and server-side approaches. +{% /cards_item %} {% /cards %} # Flexible permissions {% #flexible-permissions %} -When users sign up using Appwrite, their identity is automatically attached to a robust permissions system. +When users sign up using Appwrite, their identity is automatically attached to a robust permissions system. Appwrite Authentication provides permissions for individual users and groups of users through [teams](/docs/products/auth/teams) and [labels](/docs/products/auth/labels). # Built in preferences {% #built-in-preferences %} diff --git a/src/routes/docs/products/auth/team-invites/+page.markdoc b/src/routes/docs/products/auth/team-invites/+page.markdoc new file mode 100644 index 0000000000..d8dcbbdd17 --- /dev/null +++ b/src/routes/docs/products/auth/team-invites/+page.markdoc @@ -0,0 +1,710 @@ +--- +layout: article +title: Team invites +description: Learn how to manage team invites in Appwrite. Implement both client-side email invites and server-side custom flows for team memberships. +--- + +Appwrite provides two approaches for adding members to teams: client-side email invites and server-side custom flows. Each approach serves different use cases and offers unique benefits. + +# Client-side email invites {% #client-side %} + +Client-side email invites are perfect for implementing user-to-user invitations, allowing your users to invite others to join their teams, organizations, or shared resources. When creating a membership, Appwrite: +1. Creates a new user account if one doesn't exist for the email address +2. Sends an automated email invitation to the user +3. Creates a pending membership +4. Activates the membership when the user accepts + +Client-side invites are ideal when you want a simple, automated process that lets your users manage their own team invitations. +Appwrite handles the email delivery with built-in templates and localization support, making it easy to implement a standard invite acceptance flow with email verification. + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +// Create membership with email invite +const membership = await teams.createMembership( + 'team_id', + ['developer'], // roles + 'user@example.com' // email +); +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +final teams = Teams(client); + +// Create membership with email invite +final membership = await teams.createMembership( + teamId: 'team_id', + roles: ['developer'], + email: 'user@example.com' +); +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +let teams = Teams(client) + +// Create membership with email invite +let membership = try await teams.createMembership( + teamId: "team_id", + roles: ["developer"], + email: "user@example.com" +) +``` +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val teams = Teams(client) + +// Create membership with email invite +val response = teams.createMembership( + teamId = "team_id", + roles = listOf("developer"), + email = "user@example.com" +) +``` +{% /multicode %} + +## Accept invitations {% #accept-invitations %} + +For client-side email invites, users must accept the invitation to join the team. The acceptance flow: +1. User receives an email with an invitation link +2. Clicking the link redirects to your app +3. Your app calls the acceptance endpoint +4. Upon success, the user gains immediate access + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +// Accept the invitation using the membership ID +const response = await teams.updateMembershipStatus( + 'team_id', + 'membership_id', + 'user_id' +); +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +final teams = Teams(client); + +// Accept the invitation using the membership ID +final response = await teams.updateMembershipStatus( + teamId: 'team_id', + membershipId: 'membership_id', + userId: 'user_id' +); +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +// Accept the invitation using the membership ID +let response = try await teams.updateMembershipStatus( + teamId: "team_id", + membershipId: "membership_id", + userId: "user_id" +) +``` +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +// Accept the invitation using the membership ID +val response = teams.updateMembershipStatus( + teamId = "team_id", + membershipId = "membership_id", + userId = "user_id" +) +``` +{% /multicode %} + +# Server-side custom flows {% #server-side %} + +Server-side membership creation bypasses the email invitation process, allowing direct member addition. This approach: +1. Creates an active membership immediately +2. Doesn't require user acceptance +3. Gives you complete control over the invitation workflow + +Server-side flows give you complete control over the membership process. +This makes them perfect for scenarios requiring custom workflows, such as bulk user management, automated team assignments, or integration with external systems. +Since memberships are created directly, users gain immediate access without waiting for email acceptance. + +{% multicode %} +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const teams = new sdk.Teams(client); + +// Create membership directly with userId +const membership = await teams.createMembership( + 'team_id', + ['developer'], + 'user_id', + 'John Doe' // optional name +); +``` +```server-python +from appwrite.client import Client +from appwrite.services.teams import Teams + +client = Client() +client.set_endpoint('https://cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +teams = Teams(client) + +# Create membership directly with userId +membership = teams.create_membership( + team_id='team_id', + roles=['developer'], + user_id='user_id', + name='John Doe' # optional +) +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + .setKey("") // Your secret API key + +let teams = Teams(client) + +// Create membership directly with userId +let membership = try await teams.createMembership( + teamId: "team_id", + roles: ["developer"], + userId: "user_id", + name: "John Doe" // optional +) +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + .setKey("") // Your secret API key + +val teams = Teams(client) + +// Create membership directly with userId +val response = teams.createMembership( + teamId = "team_id", + roles = listOf("developer"), + userId = "user_id", + name = "John Doe" // optional +) +``` +{% /multicode %} + +# Manage memberships {% #manage-memberships %} + +## Check membership status {% #status %} + +### Client-side +To check membership status client-side, first list the teams and then get the memberships for a specific team: + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +// Get list of teams the user is part of +const teamsList = await teams.list(); + +// For a specific team, get all memberships +const response = await teams.listMemberships('team_id'); + +// Find membership for specific user +const userMembership = response.memberships.find( + membership => membership.userId === 'user_id' +); + +if (userMembership) { + console.log(userMembership.confirm); // false = invited, true = joined + console.log(userMembership.roles); // ['developer', etc] +} +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +final teams = Teams(client); + +// Get list of teams the user is part of +final teamsList = await teams.list(); + +// For a specific team, get all memberships +final response = await teams.listMemberships( + teamId: 'team_id' +); + +// Find membership for specific user +final userMembership = response.memberships.firstWhere( + (membership) => membership.userId == 'user_id', + orElse: () => null +); + +if (userMembership != null) { + print(userMembership.confirm); // false = invited, true = joined + print(userMembership.roles); // ['developer', etc] +} +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +// Get list of teams the user is part of +let teamsList = try await teams.list() + +// For a specific team, get all memberships +let response = try await teams.listMemberships( + teamId: "team_id" +) + +// Find membership for specific user +if let userMembership = response.memberships.first(where: { $0.userId == "user_id" }) { + print(userMembership.confirm) // false = invited, true = joined + print(userMembership.roles) // ['developer', etc] +} +``` +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +// Get list of teams the user is part of +val teamsList = teams.list() + +// For a specific team, get all memberships +val response = teams.listMemberships( + teamId = "team_id" +) + +// Find membership for specific user +val userMembership = response.memberships.find { + it.userId == "user_id" +} + +userMembership?.let { + println(it.confirm) // false = invited, true = joined + println(it.roles) // ['developer', etc] +} +``` +{% /multicode %} + +### Server-side +Server-side requires iterating through teams and memberships since the data isn't filtered for a specific user: + +{% multicode %} +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const teams = new sdk.Teams(client); + +// Get all teams +const teamsList = await teams.list(); + +// Iterate through teams to find memberships +for (const team of teamsList.teams) { + const response = await teams.listMemberships(team.$id); + + // Find membership for specific user + const userMembership = response.memberships.find( + membership => membership.userId === 'user_id' + ); + + if (userMembership) { + console.log(`Team: ${team.name}`); + console.log(`Joined: ${userMembership.joined}`); // null if invited, timestamp if joined + console.log(`Roles: ${userMembership.roles}`); + } +} +``` +```server-python +from appwrite.client import Client +from appwrite.services.teams import Teams + +client = Client() +client.set_endpoint('https://cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +teams = Teams(client) + +// Get all teams +teams_list = teams.list() + +// Iterate through teams to find memberships +for team in teams_list['teams']: + response = teams.list_memberships(team['$id']) + + // Find membership for specific user + user_membership = next( + (m for m in response['memberships'] if m['userId'] == 'user_id'), + None + ) + + if user_membership: + print(f"Team: {team['name']}") + print(f"Joined: {user_membership['joined']}") # null if invited, timestamp if joined + print(f"Roles: {user_membership['roles']}") +``` +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let teams = Teams(client) + +// Get all teams +let teamsList = try await teams.list() + +// Iterate through teams to find memberships +for team in teamsList.teams { + let response = try await teams.listMemberships( + teamId: team.$id + ) + + // Find membership for specific user + if let userMembership = response.memberships.first(where: { $0.userId == "user_id" }) { + print("Team: \(team.name)") + print("Joined: \(userMembership.joined)") # null if invited, timestamp if joined + print("Roles: \(userMembership.roles)") + } +} +``` +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val teams = Teams(client) + +// Get all teams +val teamsList = teams.list() + +// Iterate through teams to find memberships +teamsList.teams.forEach { team -> + val response = teams.listMemberships(teamId = team.$id) + + // Find membership for specific user + val userMembership = response.memberships.find { + it.userId == "user_id" + } + + userMembership?.let { + println("Team: ${team.name}") + println("Joined: ${it.joined}") # null if invited, timestamp if joined + println("Roles: ${it.roles}") + } +} +``` +{% /multicode %} + +## Remove members {% #remove-members %} + +Team owners can remove members or users can leave teams: + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite"; + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +const teams = new Teams(client); + +await teams.deleteMembership( + 'team_id', + 'membership_id' +); +``` +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject(''); + +final teams = Teams(client); + +await teams.deleteMembership( + teamId: 'team_id', + membershipId: 'membership_id' +); +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +try await teams.deleteMembership( + teamId: "team_id", + membershipId: "membership_id" +) +``` +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +teams.deleteMembership( + teamId = "team_id", + membershipId = "membership_id" +) +``` +{% /multicode %} + +# Manage team permissions {% #permissions %} + +Teams in Appwrite use a role-based access control (RBAC) system. Each team member can be assigned one or more roles that define their permissions within the team. + +## Update roles {% #update-roles %} + +You can assign roles when creating a membership or update them later. Note that only team members with the owner role can update other members' roles: + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite" + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + +const teams = new Teams(client) + +// Update member roles +await teams.updateMembership( + 'team_id', + 'membership_id', + ['admin', 'developer'] +) +``` +```client-flutter +import 'package:appwrite/appwrite.dart' + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + +final teams = Teams(client) + +// Update member roles +await teams.updateMembership( + teamId: 'team_id', + membershipId: 'membership_id', + roles: ['admin', 'developer'] +) +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +// Update member roles +try await teams.updateMembership( + teamId: "team_id", + membershipId: "membership_id", + roles: ["admin", "developer"] +) +``` +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +// Update member roles +teams.updateMembership( + teamId = "team_id", + membershipId = "membership_id", + roles = listOf("admin", "developer") +) +``` +{% /multicode %} + +## Check role access {% #check-role-access %} + +You can verify if a user has specific roles: + +{% multicode %} +```client-web +import { Client, Teams } from "appwrite" + +const client = new Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + +const teams = new Teams(client) + +// Get team memberships +const response = await teams.listMemberships('team_id') + +// Check if user has specific role +const membership = response.memberships.find(m => m.userId === 'user_id') +const isAdmin = membership?.roles.includes('admin') ?? false +``` +```client-flutter +import 'package:appwrite/appwrite.dart' + +final client = Client() + .setEndpoint('https://cloud.appwrite.io/v1') + .setProject('') + +final teams = Teams(client) + +// Get team memberships +final response = await teams.listMemberships( + teamId: 'team_id' +) + +// Check if user has specific role +final membership = response.memberships.firstWhere( + (m) => m.userId == 'user_id', + orElse: () => null +) +final isAdmin = membership?.roles.contains('admin') ?? false +``` +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +let teams = Teams(client) + +// Get team memberships +let response = try await teams.listMemberships( + teamId: "team_id" +) + +// Check if user has specific role +let membership = response.memberships.first { $0.userId == "user_id" } +let isAdmin = membership?.roles.contains("admin") ?? false +``` +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Teams + +val client = Client(context) + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("") + +val teams = Teams(client) + +// Get team memberships +val response = teams.listMemberships( + teamId = "team_id" +) + +// Check if user has specific role +val membership = response.memberships.find { it.userId == "user_id" } +val isAdmin = membership?.roles?.contains("admin") ?: false +``` +{% /multicode %} + +{% arrow_link href="/docs/products/auth/teams" %} +Learn more about team management +{% /arrow_link %} \ No newline at end of file