Feature spec
BeginnerCategory
personalization-ux
Build with AI
2–4 hours with FlutterFlow or Lovable
Custom build
2–4 days custom dev
Running cost
$0–25/mo up to 10K users
Works on
Everything it takes to ship User-Defined Bookmark Categories — parts, prompts, and real costs.
User-defined bookmark categories need three Supabase tables (bookmarks, bookmark_categories, bookmark_category_assignments), a chip filter bar, a category manager bottom sheet, and many-to-many assignment logic. With FlutterFlow or Lovable you can ship working category management in 2–4 hours for $0/month. All logic is Supabase CRUD with no external APIs.
What User-Defined Bookmark Categories Actually Are
Bookmark categories let users organize saved content into their own named folders or tags — think custom labels like 'Read Later', 'Client Research', or 'Recipes to Try'. Unlike fixed system categories, user-defined ones are created, renamed, and deleted by the user inline without leaving the screen. The data pattern is a many-to-many join table: one bookmark can belong to multiple categories, and one category holds many bookmarks. AI tools build the CRUD and chip filter UI quickly; the complexity hides in the JOIN query needed to filter bookmarks by category and in the cascade-delete behavior on the foreign key, which if set incorrectly destroys saved items when a user deletes a folder.
What users consider table stakes in 2026
- Create, rename, and delete custom category names inline without leaving the screen — no separate settings page
- Tap to assign an item to one or more categories from a context menu or long-press action
- Horizontal scrollable chip row showing all categories at the top of the bookmark list with an 'All' chip always visible
- Item count badge on each category chip so users can see how full each folder is at a glance
- Empty-state illustration and message when a category contains zero bookmarks
- Drag-and-drop or tap-to-reorder category list with position saved persistently across sessions
Anatomy of the Feature
Six components build user-defined bookmark categories. The many-to-many join table and the server-side JOIN query are where first builds fail; the UI widgets are straightforward.
Category Manager Sheet
UIA bottom sheet (Flutter BottomSheet or a Lovable drawer component) listing existing categories with edit and delete icons alongside each row. An inline text field at the bottom creates a new category on keyboard submit. The sheet is triggered by a 'Manage Categories' button or a long-press on the chip row. Optimistic UI adds the new category to the list before the Supabase INSERT confirms.
Note: Validate that the new name is non-empty and not a duplicate before INSERT. Surface a 'Category already exists' toast on 23505 unique violation rather than silently failing.
Bookmark Item Card
UIA card showing saved content preview (title, thumbnail, domain) with a long-press context menu (Flutter PopupMenuButton or a shadcn/ui DropdownMenu on web) that opens a multi-select category picker. The picker shows all user categories with checkboxes; tapping one toggles the assignment via INSERT or DELETE on bookmark_category_assignments.
Note: Multi-category tagging requires the picker to load current assignments first, so the checkboxes reflect existing state. Fetch current category_ids for this bookmark_id before showing the picker.
Category Filter Bar
UIA horizontal scrollable chip row (Flutter FilterChip or Tailwind badge buttons) at the top of the bookmark list. The 'All' chip is always the first item. Tapping a chip sets a selectedCategoryId in local state and re-fetches the bookmark list filtered to that category. Active chip is highlighted with an accent color.
Note: The count badge on each chip must be computed from a separate aggregation query or derived client-side from the assignments table — not from the bookmarks table length, which ignores category filters.
Supabase Bookmarks Table
DataThree tables implement the full pattern: bookmark_categories (id, user_id, name, position, created_at), bookmarks (id, user_id, item_id, item_type, created_at), and bookmark_category_assignments (bookmark_id, category_id) as a composite primary key join table. RLS restricts all tables to the owning user. Cascade delete is configured only on bookmark_category_assignments, not on bookmarks, so deleting a category removes the folder but preserves all saved items.
Note: The position integer on bookmark_categories enables user-defined ordering. After a drag-to-reorder event, batch-update position values for all affected rows rather than updating one at a time.
Category CRUD Actions
BackendSupabase client calls for INSERT (create category), UPDATE (rename), and DELETE (remove category) on the bookmark_categories table. All mutations use optimistic UI: local state is updated immediately and rolled back if the Supabase response returns an error. The DELETE call removes the category row; ON DELETE CASCADE on bookmark_category_assignments.category_id FK automatically removes assignment rows without touching bookmarks.
Note: Use a UNIQUE constraint on (user_id, name) at the database level rather than relying on client-side validation alone. Handle error code 23505 in the client for graceful duplicate-name feedback.
Drag-to-Organize Layer
UIFlutter ReorderableListView for reordering the category list in mobile apps. On web, @dnd-kit/core (DragOverlay + sortable preset) handles drag-and-drop with separate pointer and touch sensors. After each reorder event, update the position column for all moved rows via a batch Supabase UPDATE. The UI updates instantly via optimistic reorder; the DB write confirms in the background.
Note: @dnd-kit/core is recommended over react-beautiful-dnd for web because it supports separate touch and pointer sensors without long-press conflicts. If using react-beautiful-dnd, configure the touch sensor with a distinct activation delay to avoid clashing with the long-press context menu on item cards.
The data model
Three tables cover the full many-to-many bookmark category pattern. Run this in the Supabase SQL Editor — it is copy-paste ready.
1create table public.bookmark_categories (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete cascade not null,4 name text not null,5 position int not null default 0,6 created_at timestamptz not null default now(),7 constraint bookmark_categories_user_name_unique unique (user_id, name)8);910alter table public.bookmark_categories enable row level security;1112create policy "Users can view own categories"13 on public.bookmark_categories for select14 using (auth.uid() = user_id);1516create policy "Users can insert own categories"17 on public.bookmark_categories for insert18 with check (auth.uid() = user_id);1920create policy "Users can update own categories"21 on public.bookmark_categories for update22 using (auth.uid() = user_id);2324create policy "Users can delete own categories"25 on public.bookmark_categories for delete26 using (auth.uid() = user_id);2728create table public.bookmarks (29 id uuid primary key default gen_random_uuid(),30 user_id uuid references auth.users(id) on delete cascade not null,31 item_id text not null,32 item_type text not null,33 created_at timestamptz not null default now()34);3536alter table public.bookmarks enable row level security;3738create policy "Users can view own bookmarks"39 on public.bookmarks for select40 using (auth.uid() = user_id);4142create policy "Users can insert own bookmarks"43 on public.bookmarks for insert44 with check (auth.uid() = user_id);4546create policy "Users can delete own bookmarks"47 on public.bookmarks for delete48 using (auth.uid() = user_id);4950create table public.bookmark_category_assignments (51 bookmark_id uuid references public.bookmarks(id) on delete cascade not null,52 category_id uuid references public.bookmark_categories(id) on delete cascade not null,53 primary key (bookmark_id, category_id)54);5556alter table public.bookmark_category_assignments enable row level security;5758create policy "Users can view own assignments"59 on public.bookmark_category_assignments for select60 using (61 exists (62 select 1 from public.bookmarks b63 where b.id = bookmark_id and b.user_id = auth.uid()64 )65 );6667create policy "Users can insert own assignments"68 on public.bookmark_category_assignments for insert69 with check (70 exists (71 select 1 from public.bookmarks b72 where b.id = bookmark_id and b.user_id = auth.uid()73 )74 );7576create policy "Users can delete own assignments"77 on public.bookmark_category_assignments for delete78 using (79 exists (80 select 1 from public.bookmarks b81 where b.id = bookmark_id and b.user_id = auth.uid()82 )83 );8485create index bookmark_categories_user_pos_idx86 on public.bookmark_categories (user_id, position);8788create index bca_category_idx89 on public.bookmark_category_assignments (category_id);Heads up: The UNIQUE constraint on (user_id, name) prevents duplicate category names at the database level. ON DELETE CASCADE on bookmark_category_assignments.category_id removes assignments when a category is deleted but leaves bookmarks intact — this is the correct behavior. ON DELETE CASCADE on bookmark_category_assignments.bookmark_id removes assignments when a bookmark is deleted, also correct.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
FlutterFlow's Backend Queries map cleanly to the categories and assignments tables. FilterChip row and BottomSheet category manager are visual widgets requiring minimal custom code. Best path for mobile-native apps.
Step by step
- 1Create the three Supabase tables using the data model SQL above in your Supabase SQL Editor, then add them as data sources in FlutterFlow under Supabase Settings
- 2Build the Category Manager BottomSheet widget: add a ListView of categories with edit/delete SwipeToDismiss actions, and a TextField at the bottom that calls a Backend Query INSERT on submit
- 3Add the FilterChip row to the bookmark list page: query all bookmark_categories for the current user and render a FilterChip per row; set selectedCategoryId in Page State on chip tap
- 4Create a Custom Action called get_bookmarks_by_category that calls a Supabase RPC function performing the three-table JOIN — use this to populate the bookmark ListView when a category chip is selected
- 5Add the long-press context menu to each BookmarkCard using a PopupMenuButton that opens a multi-select category picker dialog using the assignments table to pre-check existing assignments
Where this path bites
- Many-to-many assignment requires a Supabase RPC function for the JOIN — FlutterFlow's query builder cannot handle a three-table JOIN without a helper function
- ReorderableListView for drag-to-reorder categories requires a Custom Widget in FlutterFlow; the built-in ListView does not support reorder
- FlutterFlow's optimistic UI requires Custom Actions; built-in Backend Queries wait for server confirmation before updating the UI
Third-party services you'll need
Bookmark categories use no third-party services beyond Supabase. All logic is client-side CRUD with open-source drag-and-drop libraries.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Stores categories, bookmarks, and assignment join table with RLS | Free tier: 500MB DB, 2 projects | Pro $25/mo for production apps with many users |
| @dnd-kit/core (web) | Drag-and-drop category reordering with separate touch and pointer sensors | Open-source MIT license | Free |
| sqflite (Flutter) | Local SQLite cache for offline-first category and bookmark storage | Open-source, pub.dev | Free |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier covers all category, bookmark, and assignment storage. No external APIs required. All logic is CRUD.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Deleting a category also removes all bookmarked items
Symptom: This is the most destructive bug in bookmark category builds. If the AI generates ON DELETE CASCADE on the foreign key from bookmarks to bookmark_category_assignments pointing in the wrong direction, or adds a cascade from bookmark_category_assignments to bookmarks, deleting a category cascades to removing the actual bookmarked content — not just the folder.
Fix: Set ON DELETE CASCADE only on bookmark_category_assignments.category_id FK (removing assignment rows when the category is deleted) and on bookmark_category_assignments.bookmark_id FK (removing assignments when the bookmark is deleted). Never add a cascade that reaches back to the bookmarks table from a category deletion. Verify by creating a test category, assigning a bookmark, deleting the category, and confirming the bookmark still appears in the 'All' view.
Category chip filter shows wrong item count after assignment changes
Symptom: AI tools often compute the item count badge from a separate aggregation query run at page load. After the user adds or removes a bookmark from a category, the badge shows the stale count from the original query — it does not reflect the change until the page reloads.
Fix: After any assignment INSERT or DELETE, either re-fetch the category counts via a Supabase query or update the local state counts atomically in the same handler. If using React Query or similar, invalidate the 'bookmark_counts' query key immediately after any mutation resolves.
FlutterFlow filter query returns all bookmarks ignoring the category selection
Symptom: FlutterFlow's query builder generates two separate queries — one for bookmarks and one for assignments — and merges them client-side. The merge logic is incorrect: it returns all bookmarks belonging to the user rather than filtering to the selected category. The user sees no filtering effect even though the chip is visually selected.
Fix: Create a Supabase RPC function called get_bookmarks_by_category with parameters p_category_id uuid and p_user_id uuid that performs the JOIN server-side across all three tables. Call this from FlutterFlow as a Custom Action that replaces the built-in Backend Query for the filtered list.
Drag-to-reorder on mobile has poor responsiveness
Symptom: react-beautiful-dnd's default touch sensor triggers on long-press. If the same item card also has a long-press context menu for category assignment, the two gestures conflict — the drag starts when the user intends to open the context menu, or vice versa.
Fix: Switch the drag-and-drop library to @dnd-kit/core and configure separate sensors: PointerSensor for mouse/desktop and TouchSensor with a 250ms activation delay for mobile. The distinct activation thresholds allow the context menu (immediate long-press) and the drag (delayed hold-and-move) to coexist without conflict.
Two devices create the same category name simultaneously causing duplicates
Symptom: If a user is logged in on two devices and creates a category with the same name on both before either syncs, both INSERTs succeed without the UNIQUE constraint catching the collision — because the constraint only fires per-database and the client-side check is bypassed by race condition.
Fix: Add UNIQUE(user_id, name) as a database-level constraint on bookmark_categories. Catch Postgres error code 23505 in the client and display 'Category already exists' as a toast rather than a hard error. This is the only reliable duplicate prevention — do not rely solely on a client-side name check before INSERT.
Best practices
Set ON DELETE CASCADE only on the assignments join table rows — never on bookmarks themselves — so deleting a category preserves all the user's saved items
Add a UNIQUE(user_id, name) database constraint and handle error code 23505 with a human-readable toast rather than a raw error message
Create the three-table JOIN as a named Supabase RPC function so both FlutterFlow and web clients call the same server-side logic — avoids duplicating the JOIN in client code
Update item count badges atomically in local state after every assignment change rather than re-fetching all counts; stale badges erode user trust in the filter
Store a position integer on categories and update it in a single batch after each reorder rather than N separate UPDATEs — batch updates are faster and less likely to cause partial-order states
Show an empty-state illustration per category tab, not a generic 'no items' message — users need to understand that the category exists but is empty, not that something is broken
Prefetch the current bookmark's existing category assignments before opening the assignment picker so checkboxes are pre-checked correctly on first render
When You Need Custom Development
AI tools handle single-user category management with Supabase CRUD comfortably. These scenarios require custom work:
- Bookmarks span heterogeneous content types — articles, videos, products, user profiles — each needing different preview metadata and card layouts that a single schema can't cleanly accommodate
- Collaborative bookmark collections shared between multiple users with granular permission levels (view-only, can-assign, can-manage-categories)
- Smart auto-categorization using an LLM or ML Kit to suggest categories based on the saved item's content, requiring an Edge Function with Anthropic or OpenAI API integration
- Offline-first requirement where the full category tree must function with zero connectivity — needs sqflite local cache, Supabase sync, and conflict resolution across devices
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Can a bookmark belong to multiple categories?
Yes — that is the entire point of the many-to-many join table. A single bookmark row can have multiple rows in bookmark_category_assignments, each pointing to a different category. The assignment picker on the bookmark card shows all user categories with checkboxes, and checking multiple boxes creates multiple assignment rows.
What happens to my bookmarks if I delete a category?
Nothing — your bookmarks are preserved. Deleting a category removes only the category row and its assignment rows (via ON DELETE CASCADE on the join table). The underlying bookmarks table is untouched. All previously assigned items reappear under the 'All' filter. This is why the cascade must be configured carefully at the database level.
Can I share a category with another user?
Not with the standard build — RLS restricts all category and assignment rows to the owning user. Shared collaborative categories require adding a collaborators table, updating RLS policies to allow SELECT for collaborator user_ids, and a UI for inviting users. This is a custom development requirement.
How many categories can I create?
No hard limit is enforced by the schema. Practically, the FilterChip row becomes unwieldy past about 15–20 categories, so consider adding a 'Show more' collapse or a search field within the chip row if your users are power organizers. You can add a max_categories CHECK constraint at the database level if you want to enforce a plan-based limit.
Does the order of categories save automatically?
Yes, if you implement the position integer column and update it after every drag-to-reorder. The position is stored in Supabase and fetched on next load ordered by (user_id, position). The AI prompt requests this explicitly, but verify in your build that the reorder actually writes to the position column rather than just reordering in local state.
Can I import bookmarks from a browser?
Not with the standard build — browser bookmark HTML exports use a proprietary Netscape format that requires a parser to convert to your schema's item_id and item_type format. This is a custom feature requiring an import utility built as a Supabase Edge Function or a server-side parser.
Is there a search within a category?
Not by default — the filtered view shows all bookmarks in a category. Adding search within a category means extending the get_bookmarks_by_category RPC to accept an optional search_term parameter and adding ILIKE matching on the item metadata. Include this in your initial prompt if it is a required feature.
How do category filters work when multiple categories are selected?
The standard build filters to exactly one category at a time (plus the 'All' option). Multi-category filtering — show bookmarks that belong to category A OR category B — requires a modified RPC function that accepts an array of category_ids and uses a WHERE category_id = ANY($1) clause. Specify 'multi-select chip filter with OR logic' in your prompt if this is needed from the start.
Need this feature production-ready?
RapidDev builds user-defined bookmark categories into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.