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 assignmentis_system– Protects built-in categories from deletionis_hidden– Controls visibility without removing the category from historical datatreat_as_transfer– Alters how transactions are aggregated in reportsis_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=Truereject 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:
- Models (
category.py,category_group.py) define the schema with workspace scoping, optional group relationships, and behavioral flags - Services (
category_service.py,category_group_service.py) encapsulate business logic, permission checks, and visibility filtering - Routers (
category.py,category_groups.py) expose REST endpoints with FastAPI dependency injection for workspace context - Client (
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. 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →