# How to Manage Packing Lists with Bags, Categories, and Member Assignments in TREK

> Master packing lists in TREK using bags categories and member assignments. Learn how this three-tier system simplifies item organization and tracking for your trips.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: how-to-guide
- Published: 2026-07-11

---

**TREK manages packing lists through a three-tier architecture that organizes items into categories, tracks them in physical bags, and assigns trip members to both categories and bags, all implemented in [`server/src/services/packingService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/packingService.ts).**

TREK is an open-source trip planning application that provides collaborative packing list functionality through a relational data model. The system enables travelers to categorize items, assign them to specific bags with weight limits, and distribute responsibility among trip members using fine-grained visibility controls. According to the TREK source code, all packing logic resides in the [`packingService.ts`](https://github.com/mauriceboe/TREK/blob/main/packingService.ts) service layer and follows Zod validation schemas defined in [`shared/src/packing/packing.schema.ts`](https://github.com/mauriceboe/TREK/blob/main/shared/src/packing/packing.schema.ts).

## Core Architecture and Data Model

The packing feature centers on four database entities managed through [`packingService.ts`](https://github.com/mauriceboe/TREK/blob/main/packingService.ts):

- **`packing_items`** – Stores individual items with visibility flags and category names
- **`packing_item_recipients`** – Links items to shared users for the "Shared" visibility tier
- **`packing_bags`** – Defines physical bags with color, weight limits, and sort order
- **`packing_bag_members`** – Assigns trip members to specific bags
- **`packing_category_assignees`** – Pins members to category headers for notifications

The API exposes these through endpoints under `/api/trips/:tripId/packing`, including routes for items, bags, and category assignees.

## Managing Item Visibility and Creation

TREK implements three visibility tiers that control item access:

- **Common** – Group pool visible to all trip members (`is_private = 0`)
- **Personal** – Private to the owner (`is_private = 1` and owner matches)
- **Shared** – Visible to selected members via the `packing_item_recipients` table

When fetching items, the `listItems` function in [`packingService.ts`](https://github.com/mauriceboe/TREK/blob/main/packingService.ts) applies user-specific filtering:

```typescript
// server/src/services/packingService.ts
export function listItems(tripId: string | number, userId?: number) {
  const rows = userId == null
    ? db.prepare('SELECT * FROM packing_items WHERE trip_id = ? ORDER BY sort_order ASC, created_at ASC')
        .all(tripId)
    : db.prepare(`
        SELECT * FROM packing_items
        WHERE trip_id = ?
          AND (is_private = 0
               OR owner_id = ?
               OR EXISTS (SELECT 1 FROM packing_item_recipients r WHERE r.item_id = packing_items.id AND r.user_id = ?))
        ORDER BY sort_order ASC, created_at ASC
      `).all(tripId, userId, userId);
  return enrichItems(rows);
}

```

Creating items involves mapping the `visibility` parameter to the internal `is_private` flag via `visibilityToPrivate`, then inserting recipient rows for shared items:

```typescript
// server/src/services/packingService.ts
export function createItem(
  tripId: string | number,
  data: {
    name: string;
    category?: string;
    visibility?: PackingVisibility;
    recipient_ids?: number[];
  },
  ownerId?: number,
) {
  const sortOrder = (db.prepare('SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?')
                      .get(tripId).max ?? -1) + 1;
  const isPrivate = visibilityToPrivate(data.visibility, data.is_private);
  const create = db.transaction(() => {
    const result = db.prepare(
      'INSERT INTO packing_items (trip_id, name, checked, category, sort_order, quantity, is_private, owner_id, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)'
    ).run(tripId, data.name, data.checked ? 1 : 0, data.category || 'Allgemein', sortOrder,
          Math.max(1, Math.min(999, Number(data.quantity) || 1)),
          isPrivate, ownerId ?? null);
    const itemId = Number(result.lastInsertRowid);
    if (data.visibility === 'shared' && Array.isArray(data.recipient_ids)) {
      const ins = db.prepare('INSERT OR IGNORE INTO packing_item_recipients (item_id, user_id) VALUES (?, ?)');
      for (const uid of data.recipient_ids) if (uid !== ownerId) ins.run(itemId, uid);
    }
    return itemId;
  });
  const itemId = create();
  return enrichItems([db.prepare('SELECT * FROM packing_items WHERE id = ?').get(itemId)])[0];
}

```

## Organizing Categories and Assigning Members

Categories in TREK function as grouping headers that support member assignment. When users assign members to a category, the system stores these in `packing_category_assignees` and triggers packing notifications without affecting item visibility.

The `updateCategoryAssignees` function handles this assignment:

```typescript
// server/src/services/packingService.ts
export function updateCategoryAssignees(
  tripId: string | number,
  categoryName: string,
  userIds: number[] | undefined,
) {
  db.prepare('DELETE FROM packing_category_assignees WHERE trip_id = ? AND category_name = ?')
    .run(tripId, categoryName);
  if (Array.isArray(userIds) && userIds.length > 0) {
    const insert = db.prepare('INSERT OR IGNORE INTO packing_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)');
    for (const uid of userIds) insert.run(tripId, categoryName, uid);
  }
  const updated = db.prepare(`
    SELECT pca.user_id, COALESCE(u.display_name, u.username) AS username, u.avatar
    FROM packing_category_assignees pca
    JOIN users u ON pca.user_id = u.id
    WHERE pca.trip_id = ? AND pca.category_name = ?
  `).all(tripId, categoryName);
  return updated.map(m => ({ ...m, avatar: avatarUrl(m) }));
}

```

The UI sends assignments via `POST /api/trips/:tripId/packing/category-assignees` with a JSON payload containing `category` and `user_ids`.

## Bag Tracking and Member Assignment

Bags provide physical organization with optional weight tracking and member assignment. Each bag supports a name, hex color code, weight limit, and assigned member list.

Creating a bag uses the `createBag` function:

```typescript
// server/src/services/packingService.ts
export function createBag(tripId: string | number, data: { name: string; color?: string }) {
  const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_bags WHERE trip_id = ?')
    .get(tripId).max ?? -1;
  const result = db.prepare('INSERT INTO packing_bags (trip_id, name, color, sort_order) VALUES (?, ?, ?, ?)')
    .run(tripId, data.name.trim(), data.color || '#6366f1', maxOrder + 1);
  return db.prepare('SELECT * FROM packing_bags WHERE id = ?').get(result.lastInsertRowid);
}

```

Assigning members to bags occurs through `setBagMembers`, which manages the `packing_bag_members` junction table:

```typescript
// server/src/services/packingService.ts
export function setBagMembers(tripId: string | number, bagId: string | number, userIds: number[]) {
  const bag = db.prepare('SELECT * FROM packing_bags WHERE id = ? AND trip_id = ?')
    .get(bagId, tripId);
  if (!bag) return null;
  db.prepare('DELETE FROM packing_bag_members WHERE bag_id = ?').run(bagId);
  const ins = db.prepare('INSERT OR IGNORE INTO packing_bag_members (bag_id, user_id) VALUES (?, ?)');
  for (const uid of userIds) ins.run(bagId, uid);
  const rows = db.prepare(`
    SELECT bm.user_id, COALESCE(u.display_name, u.username) AS username, u.avatar
    FROM packing_bag_members bm JOIN users u ON bm.user_id = u.id
    WHERE bm.bag_id = ?
  `).all(bagId);
  return rows.map(m => ({ ...m, avatar: avatarUrl(m) }));
}

```

Items reference bags through a `bag_id` foreign key, enabling the UI to filter items by physical container and display weight totals per bag.

## Templates and Offline Conflict Detection

The service supports reusable templates through `saveAsTemplate` and `applyTemplate` functions, allowing users to serialize entire packing lists for future trips. For offline scenarios, items track an `updated_at` timestamp, and the `updateItem` function accepts an `If-Match` header to detect conflicts when timestamps diverge, returning a conflict object if the server state has changed since the client last fetched.

## Summary

- **Three-tier visibility** (Common, Personal, Shared) controls access to items through `is_private` flags and the `packing_item_recipients` table
- **Category assignments** notify members via `packing_category_assignees` without affecting item visibility permissions
- **Bag tracking** organizes items physically, supports weight limits, and assigns members through `packing_bag_members`
- **Conflict detection** uses `updated_at` timestamps to handle offline modifications safely
- **Template system** enables saving and reapplying complete packing lists across trips

## Frequently Asked Questions

### How does TREK handle visibility for shared packing items?

TREK implements shared visibility through the `packing_item_recipients` table. When an item's `visibility` is set to `"shared"`, the `createItem` function inserts rows into `packing_item_recipients` for each user ID in the `recipient_ids` array, excluding the owner. The `listItems` function then checks for these recipient relationships when filtering queries for specific users.

### Can multiple trip members be assigned to a single bag?

Yes. The `setBagMembers` function in [`packingService.ts`](https://github.com/mauriceboe/TREK/blob/main/packingService.ts) supports assigning multiple members to a bag by accepting a `userIds` array. It clears existing assignments for the bag in `packing_bag_members` and inserts new junction rows, then returns enriched member data including avatar URLs for UI display.

### What is the difference between category assignees and item recipients?

**Category assignees** (`packing_category_assignees`) trigger packing notifications for members responsible for an entire category but do not control item visibility. **Item recipients** (`packing_item_recipients`) determine which users can view and interact with specific items when visibility is set to "Shared", functioning as an access control list.

### How does TREK prevent conflicts when users edit packing items offline?

Each packing item maintains an `updated_at` timestamp. When updating via `PATCH /packing/:id`, clients include this timestamp in an `If-Match` header. The service compares the provided timestamp against the current database value; if they differ, indicating a concurrent modification, the service returns a conflict response rather than applying the update.