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

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 defines a workspace-scoped entity with rich metadata and behavioral flags:

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 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 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 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)

@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)

@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 provides thin wrappers around these endpoints, maintaining type safety and consistent error handling.

Category Operations

// 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

// 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:

// 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 SQLAlchemy schema with flags and relationships
CategoryGroup model backend/app/models/category_group.py Group container definition
Category service backend/app/services/category_service.py CRUD and bulk operation logic
CategoryGroup service backend/app/services/category_group_service.py Group management with visibility handling
Category router backend/app/api/category.py FastAPI /api/categories endpoints
CategoryGroup router backend/app/api/category_groups.py FastAPI /api/category-groups endpoints
API client 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:

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. 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 and 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 and backend/app/api/category_groups.py to their respective service functions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →