# How to Add Bookmark Categories to Your App — Copy-Paste Prompts

- Tool: App Features
- Last updated: July 2026

## TL;DR

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.

## 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** (ui): A 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.
- **Bookmark Item Card** (ui): A 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.
- **Category Filter Bar** (ui): A 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.
- **Supabase Bookmarks Table** (data): Three 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.
- **Category CRUD Actions** (backend): Supabase 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.
- **Drag-to-Organize Layer** (ui): Flutter 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.

## 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.

```sql
create table public.bookmark_categories (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  name text not null,
  position int not null default 0,
  created_at timestamptz not null default now(),
  constraint bookmark_categories_user_name_unique unique (user_id, name)
);

alter table public.bookmark_categories enable row level security;

create policy "Users can view own categories"
  on public.bookmark_categories for select
  using (auth.uid() = user_id);

create policy "Users can insert own categories"
  on public.bookmark_categories for insert
  with check (auth.uid() = user_id);

create policy "Users can update own categories"
  on public.bookmark_categories for update
  using (auth.uid() = user_id);

create policy "Users can delete own categories"
  on public.bookmark_categories for delete
  using (auth.uid() = user_id);

create table public.bookmarks (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  item_id text not null,
  item_type text not null,
  created_at timestamptz not null default now()
);

alter table public.bookmarks enable row level security;

create policy "Users can view own bookmarks"
  on public.bookmarks for select
  using (auth.uid() = user_id);

create policy "Users can insert own bookmarks"
  on public.bookmarks for insert
  with check (auth.uid() = user_id);

create policy "Users can delete own bookmarks"
  on public.bookmarks for delete
  using (auth.uid() = user_id);

create table public.bookmark_category_assignments (
  bookmark_id uuid references public.bookmarks(id) on delete cascade not null,
  category_id uuid references public.bookmark_categories(id) on delete cascade not null,
  primary key (bookmark_id, category_id)
);

alter table public.bookmark_category_assignments enable row level security;

create policy "Users can view own assignments"
  on public.bookmark_category_assignments for select
  using (
    exists (
      select 1 from public.bookmarks b
      where b.id = bookmark_id and b.user_id = auth.uid()
    )
  );

create policy "Users can insert own assignments"
  on public.bookmark_category_assignments for insert
  with check (
    exists (
      select 1 from public.bookmarks b
      where b.id = bookmark_id and b.user_id = auth.uid()
    )
  );

create policy "Users can delete own assignments"
  on public.bookmark_category_assignments for delete
  using (
    exists (
      select 1 from public.bookmarks b
      where b.id = bookmark_id and b.user_id = auth.uid()
    )
  );

create index bookmark_categories_user_pos_idx
  on public.bookmark_categories (user_id, position);

create index bca_category_idx
  on public.bookmark_category_assignments (category_id);
```

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 paths

### Flutterflow — fit 5/10, 2–3 hours

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.

1. Create 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
2. Build 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
3. Add 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
4. Create 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
5. Add 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

Limitations:

- 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

### Lovable — fit 3/10, 3–5 hours

Lovable builds the category drawer and chip filter row quickly in React. Supabase integration handles CRUD. Best if targeting a web-first PWA where mobile-feeling touch gestures are secondary.

1. Create a new Lovable project with Lovable Cloud so the three tables and auth are provisioned; paste the data model SQL in the Supabase SQL Editor attached to your Lovable project
2. Paste the prompt below and let Agent Mode build the category sheet, chip bar, and assignment picker
3. Test multi-category assignment on the published URL — long-press context menu behavior varies between the Lovable preview and a real mobile browser
4. Use Design Mode to refine chip colors, empty-state illustrations, and sheet animation without spending credits
5. Enable PWA in Lovable Publish settings so users can install the app on their home screen for near-native touch feel

Starter prompt:

```
Build a user-defined bookmark categories feature with Supabase. Use three tables: bookmark_categories (id, user_id, name text, position int, created_at, UNIQUE(user_id, name)), bookmarks (id, user_id, item_id text, item_type text, created_at), and bookmark_category_assignments (bookmark_id uuid, category_id uuid, PRIMARY KEY(bookmark_id, category_id)). ON DELETE CASCADE only on the assignments FK, not on bookmarks. Build: (1) a Category Manager drawer that lists all categories with inline rename (click to edit) and delete buttons — deleting a category shows a confirmation toast 'Category deleted. Bookmarks preserved.', (2) a horizontal FilterChip row above the bookmark list with an 'All' chip always first — tapping a chip filters the displayed bookmarks by calling a Supabase RPC function 'get_bookmarks_by_category(p_category_id, p_user_id)' that JOINs all three tables, (3) item count badge on each chip derived from the assignments table, (4) right-click / long-press context menu on each BookmarkCard to open a category assignment picker showing all user categories with checkboxes pre-checked for existing assignments — toggling a checkbox calls INSERT or DELETE on bookmark_category_assignments, (5) empty state per category with an illustration and 'No bookmarks yet — save something first', (6) a position integer on categories with drag-to-reorder using @dnd-kit/core with separate PointerSensor and TouchSensor, (7) edge case: duplicate category name shows 'Category already exists' toast (catch 23505 error), (8) optimistic UI for all category CRUD with rollback on error.
```

Limitations:

- Lovable builds for web (Vite/React); mobile-feeling drag-and-drop on touch requires extra prompting for @dnd-kit/core touch sensor configuration with correct activation constraints
- Long-press context menu in a PWA web context is less reliable than Flutter PopupMenuButton on native mobile — test on the target mobile browser
- The Supabase RPC function for three-table JOIN must be created manually in Supabase SQL Editor before the Lovable-generated code can call it

### Custom — fit 4/10, 2–4 days

Custom development gives full control over offline-first architecture with SQLite local cache plus Supabase sync. Necessary when bookmarks span heterogeneous content types with different metadata schemas.

1. Implement the three-table schema with sqflite as the local database and Supabase as the remote sync target — write a SyncService that resolves conflicts using updated_at timestamps
2. Build the heterogeneous bookmark model: a base Bookmark class with subtypes (ArticleBookmark, VideoBookmark, ProductBookmark) each with their own metadata fields stored in a typed JSONB column
3. Implement full offline support: category CRUD and assignment changes queue to a local pending_operations table and flush to Supabase on reconnect
4. Add conflict resolution strategy for concurrent edits on the same category from two devices: last-write-wins on name changes, union merge on assignments

Limitations:

- sqflite plus Supabase sync logic adds meaningful complexity; conflict resolution for concurrent device edits needs an explicit, tested strategy
- Each new bookmark content type (heterogeneous metadata) requires extending the data model and the preview card renderer

## Gotchas

- **Deleting a category also removes all bookmarked items** — 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** — 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** — 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** — 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** — 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/user-defined-bookmark-categories
© RapidDev — https://www.rapidevelopers.com/app-features/user-defined-bookmark-categories
