Build Brief
WeWeb + Supabase 8 hours Beginner–Intermediate

What You Are Building

A team member directory for a workspace. Admins can add, edit, and remove members. All workspace members can view the directory. Each member has a profile with a name, role, department, bio, and avatar photo stored in Supabase Storage.

Roles & Access
  • MemberBrowse the directory, open member profiles, and view shared workspace information.
  • Admin / OwnerAdd members, edit profiles, change status, and remove members when needed.
Core Flows

How this product should work

This should feel like a real internal people directory, with strong list behavior, a clean profile view, and role-based actions that are obvious at the start.

Searchable directory

The main list page supports search, department and status filters, pagination, and the correct empty or error state for each case.

Profile detail view

Users can open a member profile to see avatar, role, department, bio, and contact information with proper not-found handling.

Shared member form

One modal handles both add and edit, including validation, avatar upload, status handling, and pre-filled edit mode.

Removal and storage rules

Delete requires confirmation, and avatar files live in a private storage structure tied to workspace-scoped access rules.

Features to Build

#FeatureEst. Time
1members table + RLS policies1h
2member-avatars storage bucket + RLS30m
3Members list page (search + department filter + pagination)1h 30m
4Member detail page (full profile view)45m
5Add / Edit member modal (shared form with avatar upload)2h
6Delete member (confirmation dialog, admin only)45m
7Loading, empty, and error states on all pages30m

1. Database — members Table

Schema

ColumnTypeRules
iduuidPK, default gen_random_uuid()
workspace_iduuidFK → workspaces.id, ON DELETE CASCADE
full_nametextNOT NULL, max 100 chars
emailtextNOT NULL, unique per workspace
roletextNOT NULL, one of: 'owner', 'admin', 'member'
departmenttextnullable
biotextnullable, max 500 chars
avatar_pathtextnullable — path in storage bucket
statustextNOT NULL, default 'active', one of: 'active', 'inactive'
created_attimestamptzdefault now()
updated_attimestamptzdefault now(), updated by trigger

Constraints

-- Unique email per workspace
UNIQUE (workspace_id, email)

-- Status must be valid
CHECK (status IN ('active', 'inactive'))

-- Role must be valid
CHECK (role IN ('owner', 'admin', 'member'))

Indexes

CREATE INDEX idx_members_workspace_id ON members (workspace_id);
CREATE INDEX idx_members_department ON members (workspace_id, department);
CREATE INDEX idx_members_status ON members (workspace_id, status);

Trigger

Create a set_timestamps trigger that sets updated_at = now() on every UPDATE.

RLS Policies

SELECT: workspace members can view all members in their workspace
INSERT: only workspace admins/owners can insert
UPDATE: only workspace admins/owners can update
DELETE: only workspace admins/owners can delete

2. Storage — member-avatars Bucket

  • Visibility: Private
  • Allowed MIME types: image/jpeg, image/png, image/webp
  • Max file size: 2 MB
  • Path structure: {workspace_id}/{member_id}/avatar.{ext}

RLS on storage.objects

SELECT: workspace members can view avatars for their workspace (first path segment = workspace_id)
INSERT: workspace admins/owners can upload
UPDATE: workspace admins/owners can replace
DELETE: workspace admins/owners can delete

3. Members List Page

URL: /workspace/{id}/members

[ ] Page header: "Team Members" + "Add Member" button (admin only — hidden for non-admins)
[ ] Search input: searches by full_name and email — debounced 300ms — sent to Supabase
[ ] Filter: Department dropdown (all departments from backend, not hardcoded)
[ ] Filter: Status toggle (Active / All)
[ ] Results shown in a table or card grid (your choice — be consistent)
[ ] Columns (table): Avatar · Full Name · Role badge · Department · Status badge · Actions
[ ] Actions column: Edit (admin only) · Delete (admin only)
[ ] Role badge: owner=purple, admin=blue, member=grey
[ ] Status badge: active=green, inactive=grey
[ ] Pagination: 20 per page, "Showing X–Y of Z members"

STATES:
[ ] Loading: skeleton rows (matching page size)
[ ] Empty (no members): "No team members yet." + "Add your first member" button
[ ] Empty (filter/search): "No members match your search." + clear button
[ ] Error: "Could not load members." + retry button

4. Member Detail Page

URL: /workspace/{id}/members/{member_id}

[ ] Shows avatar (or initials fallback if no avatar)
[ ] Full name, role badge, department, status badge
[ ] Bio text (or "No bio added." if empty — not a blank space)
[ ] Email (clickable mailto link)
[ ] "Edit Member" button (admin only)
[ ] "Back to Members" link
[ ] Loading skeleton while data loads
[ ] Error state if member not found: "Member not found." + back link

5. Add / Edit Member Modal

A single shared modal component used for both Add and Edit. The only difference is the title and submit button label.

Form Fields

FieldTypeRules
Full Nametext inputRequired, max 100 chars
Emailemail inputRequired, valid email format, unique in workspace (check on submit)
RoledropdownRequired, options: Admin / Member (Owner cannot be set via form)
Departmenttext inputOptional, max 100 chars
BiotextareaOptional, max 500 chars, show character count
Avatarfile uploadOptional, JPG/PNG/WebP only, max 2 MB, show preview
Statustoggle (Active/Inactive)Default: Active, visible in Edit mode only

Behavior

ADD MODE:
[ ] Modal title: "Add Member"
[ ] All fields empty on open
[ ] Submit button: "Add Member" — shows loading state during submit
[ ] On success: close modal, show "Member added." toast, list refreshes

EDIT MODE:
[ ] Modal title: "Edit Member"
[ ] All fields pre-filled with current member data
[ ] Avatar shows current avatar (or initials)
[ ] Submit button: "Save Changes" — shows loading state during submit
[ ] On success: close modal, show "Changes saved." toast, list refreshes

BOTH MODES:
[ ] X close button + Escape key close the modal
[ ] If email already exists: show "A member with this email already exists." error below email field
[ ] API error: show banner above submit button (not a toast)
[ ] Loading during submit: disable all fields and close button

6. Delete Member

[ ] Delete button visible only to admins (hidden for member role)
[ ] Clicking Delete opens a confirmation modal:
    Title:  "Remove [Full Name]?"
    Body:   "[Full Name] will be removed from the workspace and lose access immediately."
    Body:   "This action cannot be undone."
    Buttons: "Cancel" (left) · "Remove Member" red (right)
[ ] Confirm button shows loading state during delete
[ ] On success: modal closes, "Member removed." toast shown, row disappears without page refresh
[ ] On error: show error banner in the modal, keep modal open

7. Permissions Summary

ActionMemberAdminOwner
View listYesYesYes
View detailYesYesYes
Add memberNoYesYes
Edit memberNoYesYes
Delete memberNoYesYes
Set role to AdminNoYesYes
[ ] "Add Member" button hidden for member role — not just disabled
[ ] Edit and Delete buttons hidden in table rows for member role
[ ] Backend (RLS) enforces all of the above — frontend is display only

What You Should NOT Do

x Hardcode department options — fetch from the backend (distinct departments query)
x Use a public storage bucket for avatars — they contain user data
x Store signed avatar URLs in the database — generate them at render time
x Duplicate the Add and Edit form — one shared component only
x Filter members client-side — all search/filter must go to Supabase
x Show a blank area for empty states — always show a message
x Skip loading skeletons — every data fetch needs a skeleton, not a spinner
x Let the delete work without a confirmation dialog
x Show the Add/Edit/Delete buttons to a member-role user

Checklists to Run (in order)

[ ] New Table — members table
[ ] Database Trigger — updated_at trigger
[ ] RLS Policies — members table
[ ] Storage Bucket — member-avatars bucket
[ ] WeWeb Page Build — members list page
[ ] WeWeb Page Build — member detail page
[ ] Add/Edit Consistency — member modal
[ ] Modals & Dialogs — member modal
[ ] Loading States & Skeletons — list and detail pages
[ ] Delete & Destructive Actions — delete member
[ ] Notifications & Toasts — all async actions
[ ] Permissions & Role-Based UI — all pages and actions

Done When

[ ] members table created with all columns, constraints, indexes
[ ] updated_at trigger works: UPDATE sets updated_at automatically
[ ] RLS: member can read; only admin/owner can write — tested for both roles
[ ] member-avatars storage bucket: private, MIME restricted, RLS enforced
[ ] Members list: search and department filter sent to Supabase (not client-side)
[ ] Members list: all 4 states work (loading/empty/empty-filter/error)
[ ] Members list: pagination shows "Showing X–Y of Z"
[ ] Member detail: loading skeleton and not-found error state work
[ ] Add/Edit: same component used for both modes
[ ] Add/Edit: pre-fill works in edit mode
[ ] Add/Edit: duplicate email error shown as field error (not toast)
[ ] Add/Edit: loading state during submit, button disabled
[ ] Avatar upload: MIME type validated, 2 MB limit enforced, preview shown
[ ] Signed URL generated for avatar display (not stored public URL)
[ ] Delete: confirmation dialog shown, loading state on confirm button
[ ] Delete: row removed from list without page refresh
[ ] Add/Edit/Delete buttons hidden for member-role users
[ ] Backend RLS tested: member cannot INSERT/UPDATE/DELETE via Supabase directly
[ ] Mobile tested at 375px and 768px
[ ] No silent failures — every error shown in UI