# How Categories and Category Groups Are Managed in the Securo API: A Complete Implementation Guide

> Learn how Securo API manages categories and category groups. Explore its layered architecture, access controls, and system protections in this complete implementation guide.

- Repository: [securo-finance/securo](https://github.com/securo-finance/securo)
- Tags: how-to-guide
- Published: 2026-08-28

---

**Securo manages categories and category groups as first-class REST resources with workspace-scoped access, visibility controls, and system-level protections through a layered architecture spanning SQLAlchemy models, service layers, FastAPI routers, and TypeScript client wrappers.**

In `securo-finance/securo`, the personal finance platform implements a robust hierarchical classification system where **categories** can optionally belong to **category groups**, enabling flexible budget organization. This article examines the complete implementation stack—from database models to front-end API calls—demonstrating how the system handles creation, retrieval, updates, and deletion while enforcing workspace isolation and visibility rules.

---

## Data Models: Category and CategoryGroup Definitions

The foundation of Securo's classification system resides in two SQLAlchemy models that establish the relationship between individual categories and their optional groupings.

### Category Model Structure

The `Category` class in [`backend/app/models/category.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/category.py) defines a workspace-scoped entity with rich metadata and behavioral flags:

```python
class Category(Base):
    __tablename__ = "categories"
    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"))
    workspace_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("workspaces.id", ondelete="CASCADE"), index=True)
    group_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("category_groups.id"), nullable=True)
    name: Mapped[str] = mapped_column(String(100))
    icon: Mapped[str] = mapped_column(String(50))
    color: Mapped[str] = mapped_column(String(7))  # Hex color code

    is_system: Mapped[bool] = mapped_column(Boolean, default=False)
    is_hidden: Mapped[bool] = mapped_column(Boolean, default=False)
    treat_as_transfer: Mapped[bool] = mapped_column(Boolean, default=False)
    is_ignored: Mapped[bool] = mapped_column(Boolean, default=False)

```

Key design decisions in this model include:

- **`group_id`** – A nullable foreign key enabling optional category-to-group assignment
- **`is_system`** – Protects built-in categories from deletion
- **`is_hidden`** – Controls visibility without removing the category from historical data
- **`treat_as_transfer`** – Alters how transactions are aggregated in reports
- **`is_ignored`** – Excludes transactions from budgeting calculations

### CategoryGroup Model Structure

The `CategoryGroup` model in [`backend/app/models/category_group.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/category_group.py) provides simpler container functionality with parallel visibility controls. Groups contain **name**, **icon**, **color**, and the **`is_hidden`** flag, plus `is_system` protection for built-in group definitions.

---

## Service Layer: Business Logic and Access Control

Securo encapsulates all category and category group operations in dedicated service modules that enforce workspace scoping, permission checks, and visibility rules.

### Category Service Operations

[`backend/app/services/category_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/category_service.py) exposes these primary functions:

| Function | Purpose |
|----------|---------|
| `get_categories(session, workspace_id, include_hidden=False)` | Retrieves workspace-scoped categories with optional hidden inclusion |
| `create_category(session, workspace_id, user_id, data)` | Creates new category with ownership assignment |
| `update_category(session, category_id, workspace_id, data)` | Modifies category fields with workspace validation |
| `delete_category(session, category_id, workspace_id)` | Removes non-system categories |

### Category Group Service Operations

[`backend/app/services/category_group_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/category_group_service.py) provides parallel functionality:

| Function | Purpose |
|----------|---------|
| `get_groups(session, workspace_id, include_hidden=False)` | Lists groups respecting visibility flags |
| `create_group(session, workspace_id, user_id, data)` | Creates new group container |
| `update_group(session, group_id, workspace_id, data)` | Modifies group metadata |
| `delete_group(session, group_id, workspace_id)` | Removes non-system groups |

Both services implement **workspace isolation**—every operation validates that the requested resource belongs to the caller's workspace before proceeding. The `include_hidden` parameter determines whether categories or groups marked with `is_hidden=True` appear in results, supporting UI toggles between simplified and complete views.

---

## FastAPI Router Endpoints

The HTTP interface in Securo follows REST conventions with explicit workspace context injection through FastAPI dependencies.

### Category Endpoints ([`backend/app/api/category.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/category.py))

```python
@router.get("", response_model=list[CategoryRead])
async def list_categories(
    include_hidden: bool = Query(False),
    ctx: WorkspaceContext = Depends(current_workspace),
    session: AsyncSession = Depends(get_async_session),
):
    return await category_service.get_categories(session, ctx.workspace.id, include_hidden)

```

The categories router exposes:

- **GET `/api/categories`** – List categories with optional `?include_hidden=true`
- **POST `/api/categories`** – Create new category
- **PATCH `/api/categories/{id}`** – Update category fields
- **DELETE `/api/categories/{id}`** – Delete category (system categories protected)
- **POST `/api/transactions/bulk-categorize`** – Mass-assign category to transactions

### Category Group Endpoints ([`backend/app/api/category_groups.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/category_groups.py))

```python
@router.get("", response_model=list[CategoryGroupRead])
async def list_groups(
    include_hidden: bool = Query(False),
    ctx: WorkspaceContext = Depends(current_workspace),
    session: AsyncSession = Depends(get_async_session),
):
    return await category_group_service.get_groups(
        session, ctx.workspace.id, include_hidden=include_hidden,
    )

```

The category groups router exposes:

- **GET `/api/category-groups`** – List groups with optional hidden inclusion
- **POST `/api/category-groups`** – Create new group
- **PATCH `/api/category-groups/{id}`** – Update group metadata
- **DELETE `/api/category-groups/{id}`** – Delete group (system groups protected)

---

## Front-End API Integration

The TypeScript client in [`frontend/src/lib/api.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/api.ts) provides thin wrappers around these endpoints, maintaining type safety and consistent error handling.

### Category Operations

```typescript
// Fetch visible categories only
const categories = await api.get('/categories');

// Create category with optional group assignment
await api.post('/categories', {
  name: 'Dining Out',
  icon: 'utensils',
  color: '#FF5722',
  group_id: 'group-uuid',   // nullable: omit for ungrouped
});

// Update category
await api.patch('/categories/${id}', { name: 'Restaurants', color: '#E91E63' });

// Delete category
await api.delete('/categories/${id}');

```

### Category Group Operations

```typescript
// Fetch visible groups (default behavior)
const groups = await api.get('/category-groups');

// Include hidden groups for administrative views
const allGroups = await api.get('/category-groups?include_hidden=true');

// Create organizational container
await api.post('/category-groups', {
  name: 'Discretionary Spending',
  icon: 'shopping-bag',
  color: '#9C27B0'
});

```

### Bulk Categorization

A specialized endpoint enables efficient transaction classification:

```typescript
// Assign category to multiple transactions
await api.post('/transactions/bulk-categorize', {
  transaction_ids: ['txn-1', 'txn-2', 'txn-3'],
  category_id: 'cat-uuid',   // null clears existing categorization
});

```

---

## Visibility and System Protection Mechanisms

Securo implements several safeguards to maintain data integrity and UI clarity:

- **Workspace scoping** – Every query filters by `workspace_id`, ensuring multi-tenant isolation at the database level
- **System flag protection** – Resources with `is_system=True` reject deletion requests regardless of caller permissions
- **Hidden state management** – Categories and groups can be hidden without deleting historical associations, preserving report accuracy
- **Cascading workspace deletion** – Foreign key with `ondelete="CASCADE"` ensures cleanup when workspaces are removed

These mechanisms enable sophisticated UI patterns: users can archive old categories through `is_hidden`, prevent accidental deletion of built-in classifications via `is_system`, and exclude specific transaction types from budgets using `is_ignored` or `treat_as_transfer`.

---

## Key Implementation Files

| Component | Path | Responsibility |
|-----------|------|----------------|
| Category model | [`backend/app/models/category.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/category.py) | SQLAlchemy schema with flags and relationships |
| CategoryGroup model | [`backend/app/models/category_group.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/category_group.py) | Group container definition |
| Category service | [`backend/app/services/category_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/category_service.py) | CRUD and bulk operation logic |
| CategoryGroup service | [`backend/app/services/category_group_service.py`](https://github.com/securo-finance/securo/blob/main/backend/app/services/category_group_service.py) | Group management with visibility handling |
| Category router | [`backend/app/api/category.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/category.py) | FastAPI `/api/categories` endpoints |
| CategoryGroup router | [`backend/app/api/category_groups.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/category_groups.py) | FastAPI `/api/category-groups` endpoints |
| API client | [`frontend/src/lib/api.ts`](https://github.com/securo-finance/securo/blob/main/frontend/src/lib/api.ts) | TypeScript wrapper for all endpoints |

---

## Summary

Managing categories and category groups in the Securo API involves a four-layer architecture:

- **Models** ([`category.py`](https://github.com/securo-finance/securo/blob/main/category.py), [`category_group.py`](https://github.com/securo-finance/securo/blob/main/category_group.py)) define the schema with workspace scoping, optional group relationships, and behavioral flags
- **Services** ([`category_service.py`](https://github.com/securo-finance/securo/blob/main/category_service.py), [`category_group_service.py`](https://github.com/securo-finance/securo/blob/main/category_group_service.py)) encapsulate business logic, permission checks, and visibility filtering
- **Routers** ([`category.py`](https://github.com/securo-finance/securo/blob/main/category.py), [`category_groups.py`](https://github.com/securo-finance/securo/blob/main/category_groups.py)) expose REST endpoints with FastAPI dependency injection for workspace context
- **Client** ([`api.ts`](https://github.com/securo-finance/securo/blob/main/api.ts)) provides type-safe TypeScript methods matching the back-end contract

The system supports hierarchical organization through optional `group_id` assignment, flexible visibility through `include_hidden` query parameters, and data protection through `is_system` flags that prevent deletion of built-in resources.

---

## Frequently Asked Questions

### What is the relationship between categories and category groups in Securo?

Categories can optionally belong to a single category group through the nullable `group_id` foreign key defined in [`backend/app/models/category.py`](https://github.com/securo-finance/securo/blob/main/backend/app/models/category.py). This design enables flexible organization—categories exist independently, but can be collected into groups for UI presentation or budget rollups. The relationship is many-to-one: multiple categories may share a group, but each category references at most one group.

### How does Securo prevent accidental deletion of built-in categories?

The `is_system` boolean flag on both `Category` and `CategoryGroup` models marks resources that ship with the application and cannot be removed. Service layer checks in [`category_service.py`](https://github.com/securo-finance/securo/blob/main/category_service.py) and [`category_group_service.py`](https://github.com/securo-finance/securo/blob/main/category_group_service.py) reject deletion requests when this flag is `True`, returning appropriate error responses regardless of the caller's permissions.

### What is the difference between `is_hidden` and `is_ignored` flags on categories?

`is_hidden` controls UI visibility—hidden categories don't appear in default dropdowns or lists but remain attached to historical transactions. `is_ignored` affects budget calculations specifically, excluding categorized transactions from spending totals and progress tracking. A category can be both hidden and ignored, either separately or together, supporting flexible archival and budgeting workflows.

### How do I retrieve hidden categories or groups through the API?

Append `?include_hidden=true` to the list endpoints: `/api/categories?include_hidden=true` or `/api/category-groups?include_hidden=true`. The `include_hidden` query parameter defaults to `False`, ensuring clean default responses while allowing administrative or archival views when needed. The same parameter flows through from [`backend/app/api/category.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/category.py) and [`backend/app/api/category_groups.py`](https://github.com/securo-finance/securo/blob/main/backend/app/api/category_groups.py) to their respective service functions.