Skip to main content
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:
  1. Workspace owner: Always has full access
  2. Admin role: Has full access by default
  3. 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:
  1. Create invitation: Generate unique token and expiration date
  2. Send email: Invite link sent to user’s email
  3. Accept invitation: User clicks link and joins workspace
  4. 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

1

Use least privilege

Grant members only the permissions they need. Start with the Member role and add specific permissions as required.
2

Regular access reviews

Periodically review member permissions and deactivate unused accounts.
3

Separate owner and admin

Maintain a clear distinction between workspace owner (account responsibility) and admins (operational access).
4

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.

Build docs developers (and LLMs) love