Workspace members are users who have access to your Featul workspace with specific roles and permissions. Members can create posts, moderate feedback, manage settings, and more based on their assigned role.
Member Roles
Featul provides three predefined roles:
Admin
Full workspace access with all permissions enabled by default:
- Manage workspace settings
- Manage billing and subscriptions
- Invite and remove members
- Create and configure boards
- Moderate all boards
- Configure branding
Member
Standard access for team members:
- Create and comment on posts
- Vote on feedback
- View workspace content
- Custom permissions can be granted
Viewer
Read-only access:
- View posts and comments
- View roadmap and changelog
- No creation or moderation permissions by default
The workspace owner is automatically added as an admin with full permissions when the workspace is created.
Permission System
Each workspace member has granular permissions stored in the permissions JSON field:
type MemberPermissions = {
canManageWorkspace: boolean // Update workspace settings
canManageBilling: boolean // Manage subscriptions and billing
canManageMembers: boolean // Invite/remove members
canManageBoards: boolean // Create/edit board configuration
canModerateAllBoards: boolean // Moderate content on any board
canConfigureBranding: boolean // Update branding settings
}
Default Permissions by Role
Admin role (set on workspace creation):
{
"canManageWorkspace": true,
"canManageBilling": true,
"canManageMembers": true,
"canManageBoards": true,
"canModerateAllBoards": true,
"canConfigureBranding": true
}
Member role (default for new members):
{
"canManageWorkspace": false,
"canManageBilling": false,
"canManageMembers": false,
"canManageBoards": false,
"canModerateAllBoards": false,
"canConfigureBranding": false
}
Custom permissions override role defaults. Always verify both role and permissions when checking access.
Permission Checks
When performing privileged operations, Featul checks permissions in this order:
- Workspace owner: Always has full access
- Admin role: Has full access by default
- Custom permissions: Specific permission flags are checked
Example permission check from workspace.ts:27-49:
const meId = ctx.session.user.id
let allowed = ws.ownerId === meId
if (!allowed) {
const [me] = await ctx.db
.select({ role: workspaceMember.role, permissions: workspaceMember.permissions })
.from(workspaceMember)
.where(and(
eq(workspaceMember.workspaceId, ws.id),
eq(workspaceMember.userId, meId)
))
.limit(1)
const perms = (me?.permissions || {}) as Record<string, boolean>
if (me?.role === "admin" || perms?.canManageWorkspace) {
allowed = true
}
}
if (!allowed) throw new HTTPException(403, { message: "Forbidden" })
Member Lifecycle
Invitation Flow
Workspace invitations are tracked in the workspaceInvite table:
- Create invitation: Generate unique token and expiration date
- Send email: Invite link sent to user’s email
- Accept invitation: User clicks link and joins workspace
- Create membership:
workspaceMember record created with specified role
Invitation schema:
{
id: string
workspaceId: string
email: string // Invitee email
role: 'admin' | 'member' | 'viewer'
invitedBy: string // User ID of inviter
token: string // Unique invitation token
expiresAt: Date // Expiration timestamp
acceptedAt: Date | null // Acceptance timestamp
}
Each email can only have one active invitation per workspace. The unique constraint prevents duplicate invitations.
Membership Tracking
The workspaceMember table tracks:
- invitedBy: User who sent the invitation
- invitedAt: When the invitation was sent
- joinedAt: When the user accepted and joined
- isActive: Whether membership is currently active
Deactivate members instead of deleting them to preserve audit trails:
await db.update(workspaceMember)
.set({ isActive: false, updatedAt: new Date() })
.where(eq(workspaceMember.id, memberId))
Member Statistics
Retrieve activity statistics for workspace members:
const stats = await client.member.statsByWorkspaceSlug.$get({
slug: 'acme',
userId: 'user_123'
})
Returns:
{
"stats": {
"posts": 15,
"comments": 42,
"upvotes": 128
},
"topPosts": [
{
"id": "post_1",
"title": "Dark mode support",
"slug": "dark-mode-support",
"upvotes": 85,
"status": "completed"
}
]
}
Statistics include:
- Posts created: Total feedback posts authored
- Comments: Total comments on posts
- Upvotes: Combined votes on posts and comments
- Top posts: 5 most upvoted posts by the member
Member Activity Log
Track detailed member activity with pagination:
const activity = await client.member.activityByWorkspaceSlug.$get({
slug: 'acme',
userId: 'user_123',
limit: 20,
cursor: '2024-03-01T12:00:00.000Z' // Optional for pagination
})
Response:
{
"items": [
{
"id": "log_1",
"type": "post.create",
"title": "Created post: API rate limits",
"entity": "post",
"entityId": "post_123",
"createdAt": "2024-03-04T10:30:00Z",
"status": "pending"
}
],
"nextCursor": "2024-03-01T09:15:00.000Z"
}
Activity types tracked:
- Post creation, updates, and deletions
- Comment creation
- Status changes
- Votes and reactions
- Board and tag modifications
Activity logs use cursor-based pagination for efficient large dataset traversal. Use nextCursor for subsequent pages.
Access Control Patterns
Workspace Manager Access
Many operations require workspace management permissions:
const allowed =
workspace.ownerId === userId ||
member.role === 'admin' ||
member.permissions.canManageWorkspace === true
Branding Configuration Access
Branding changes require specific permission:
const allowed =
workspace.ownerId === userId ||
member.role === 'admin' ||
member.permissions.canConfigureBranding === true
Member Management Access
Inviting or removing members:
const allowed =
workspace.ownerId === userId ||
member.role === 'admin' ||
member.permissions.canManageMembers === true
Multi-Workspace Membership
Users can be members of multiple workspaces simultaneously. The unique constraint on (workspaceId, userId) ensures each user has only one membership record per workspace.
List all workspaces for the current user:
const result = await client.workspace.listMine.$get()
Returns both owned and member workspaces with deduplication.
Best Practices
Use least privilege
Grant members only the permissions they need. Start with the Member role and add specific permissions as required.
Regular access reviews
Periodically review member permissions and deactivate unused accounts.
Separate owner and admin
Maintain a clear distinction between workspace owner (account responsibility) and admins (operational access).
Track invitations
Monitor invitation acceptance and clean up expired invitations regularly.
Cascading Deletes
When a workspace is deleted, all associated members are automatically removed due to the foreign key cascade constraint:
workspaceId: text('workspace_id')
.notNull()
.references(() => workspace.id, { onDelete: 'cascade' })
When a user is deleted, their workspace memberships are also removed.