# Data Model for User Favorites and Recent Visits in Plane: Architecture Guide

> Discover the data model for user favorites and recent visits in Plane. Learn about its hierarchical JSON and PostgreSQL architecture for efficient UI state management.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: architecture
- Published: 2026-06-22

---

**Plane implements two distinct persistence strategies for UI state: favorites use a hierarchical JSON structure managed through a RESTful `/user-favorites/` resource, while recent visits rely on a PostgreSQL-backed Django model (`UserRecentVisit`) exposed via a read-only `/recent-visits/` endpoint.**

The open-source project management platform Plane (makeplane/plane) separates bookmarking functionality from activity tracking through distinct storage mechanisms. Understanding the data model for user favorites and recent visits is critical for developers building custom workspace extensions or debugging synchronization issues between the React frontend and Django backend.

## User Favorites Data Model

Plane’s favorites system supports nested folders and optimistic UI updates through a **MobX** store backed by a generic REST API.

### TypeScript Interface Structure

The frontend contract is defined in [`packages/types/src/favorite/favorite.ts`](https://github.com/makeplane/plane/blob/main/packages/types/src/favorite/favorite.ts) as the `IFavorite` interface:

```typescript
export type IFavorite = {
  id: string;                     // UUID of the favorite
  name: string;                   // Human-readable name (e.g. “My Dashboard”)
  entity_type: string;            // “project”, “view”, “page”, etc.
  entity_data: {
    id?: string;
    name: string;
    logo_props?: TLogoProps;
  };
  is_folder: boolean;             // true if the favorite is a folder
  sort_order: number;             // UI ordering within a folder
  parent: string | null;          // ID of the containing folder (null = top-level)
  entity_identifier?: string | null; // Identifier of the underlying entity (e.g. page UUID)
  children: IFavorite[];          // Nested favorites (filled by the store)
  project_id: string | null;      // Project the favorite belongs to
  sequence: number;               // Used for drag-and-drop re-ordering
  workspace_id: string;           // Workspace that owns the favorite
};

```

**Key fields** include `is_folder` and `parent` for tree hierarchy, `entity_identifier` for linking to actual workspace entities, and `sequence` for stable drag-and-drop ordering calculated as floating-point numbers.

### REST API and Storage Pattern

Unlike most Plane entities, favorites are **not backed by a dedicated Django model**. Instead, data is stored in a generic `Favorite` table accessed via the `user_favorites` REST resource:

- **Add favorite**: `POST /api/workspaces/{slug}/user-favorites/`
- **List favorites**: `GET /api/workspaces/{slug}/user-favorites/`

The service layer in [`apps/web/core/services/favorite/favorite.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/favorite/favorite.service.ts) wraps these endpoints:

```typescript
// Add a favorite
await favoriteService.addFavorite(workspaceSlug, { 
  name, 
  entity_type, 
  entity_identifier, 
  ... 
});

// Fetch all favorites for the workspace
const favorites = await favoriteService.getFavorites(workspaceSlug);

```

### State Management and Optimistic Updates

The [`favorite.store.ts`](https://github.com/makeplane/plane/blob/main/favorite.store.ts) file in `apps/web/core/store/` maintains two critical maps: `favoriteMap` (ID to favorite object) and `entityMap` (entity identifier to favorite ID). This **bidirectional mapping** allows the UI to instantly toggle `is_favorite` flags on source entities without waiting for server confirmation.

When `addFavorite` is called, the store generates a temporary UUID, updates both maps optimistically, then replaces the temporary entry with the real server response. The `groupedFavorites` computed property builds parent-child hierarchies on-the-fly using the `parent` field, while `reOrderFavorite` recalculates floating-point `sequence` values to maintain stable ordering during drag-and-drop operations.

## Recent Visits Data Model

Recent activity tracking uses a traditional **Django ORM** approach with automatic timestamp updates.

### Django Model Definition

The schema lives in [`apps/api/plane/db/models/recent_visit.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/recent_visit.py) as the `UserRecentVisit` class:

```python
class UserRecentVisit(WorkspaceBaseModel):
    entity_identifier = models.UUIDField(null=True)   # ID of the visited entity

    entity_name = models.CharField(max_length=30)    # "VIEW", "PAGE", "ISSUE", …

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="user_recent_visit",
    )
    visited_at = models.DateTimeField(auto_now=True) # last-seen timestamp

    class Meta:
        db_table = "user_recent_visits"
        ordering = ("-created_at",)

```

Inheriting from `WorkspaceBaseModel` ensures every record is scoped to a workspace. The `entity_name` field uses the `EntityNameEnum` (VIEW, PAGE, ISSUE, etc.) defined in the same file. The `visited_at` field automatically updates on each save, ensuring only the latest access timestamp is retained.

### Serialization and API Endpoint

The backend exposes a read-only list endpoint through `UserRecentVisitViewSet` in [`apps/api/plane/app/views/workspace/recent_visit.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/workspace/recent_visit.py):

```python

# GET /api/workspaces/{slug}/recent-visits/

class UserRecentVisitViewSet(BaseViewSet):
    model = UserRecentVisit
    serializer_class = WorkspaceRecentVisitSerializer
    permission_classes = [...]

```

The `WorkspaceRecentVisitSerializer` in [`apps/api/plane/app/serializers/workspace.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/serializers/workspace.py) converts model instances into JSON, embedding the visited entity’s data via a `get_entity_data` method to provide immediate render context.

### Frontend Integration

The workspace service ([`apps/web/core/services/workspace.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/workspace.service.ts)) consumes the endpoint through the `fetchWorkspaceRecents` method:

```typescript
// Fetch recent visits for the current workspace (optionally filtered by entity type)
await workspaceService.fetchWorkspaceRecents(workspaceSlug, entityName);

```

The returned data conforms to the `TActivityEntityData` TypeScript interface defined in [`packages/types/src/activity.ts`](https://github.com/makeplane/plane/blob/main/packages/types/src/activity.ts). The home widget components in `apps/web/core/components/home/widgets/recents` use this array to render the most recently accessed items, displaying entity metadata without additional API calls.

## Practical Implementation Examples

### Adding a Page to Favorites

```typescript
import { FavoriteService } from "@/services/favorite";

const favSvc = new FavoriteService();
await favSvc.addFavorite("my-workspace", {
  name: "Specs",
  entity_type: "page",
  entity_identifier: "e3c2a1b4-9f12-4d5e-a8b3-c6d9e7f0ab12",
  is_folder: false,
});

```

### Loading Recent Visits

```typescript
import { WorkspaceService } from "@/services/workspace";

const wsSvc = new WorkspaceService();
const recent = await wsSvc.fetchWorkspaceRecents("my-workspace", "page");

// `recent` now contains objects like:
// { id: "...", entity_name: "PAGE", entity_identifier: "...", visited_at: "2026-06-22T..." }

```

### Toggling Favorites via the Store

```typescript
import { useFavorite } from "@/hooks/store/use-favorite";

const favoriteStore = useFavorite();
await favoriteStore.addFavorite("my-workspace", {
  name: "My View",
  entity_type: "view",
  entity_identifier: "view-123",
});

```

## Summary

- **Favorites** use a hierarchical TypeScript interface (`IFavorite`) persisted through a generic REST resource at `/user-favorites/`, supporting nested folders and optimistic UI updates via MobX stores.
- **Recent Visits** rely on the concrete Django `UserRecentVisit` model in [`apps/api/plane/db/models/recent_visit.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/recent_visit.py), automatically tracking `visited_at` timestamps and exposing read-only data via `/recent-visits/`.
- The frontend maintains bidirectional `favoriteMap` and `entityMap` structures for instant UI feedback, while recent visits follow a standard service-layer pattern returning `TActivityEntityData` arrays.

## Frequently Asked Questions

### How does Plane store favorite items without a dedicated Django model?

According to the makeplane/plane source code, favorites are stored in a generic `Favorite` table accessed through the `user_favorites` REST resource. The backend handles these as JSON documents matching the `IFavorite` TypeScript interface rather than using a dedicated Django ORM model with typed fields.

### What is the difference between `entity_identifier` and `entity_data` in the favorites data model?

The `entity_identifier` field stores the canonical UUID of the underlying workspace entity (page, view, or issue), while `entity_data` caches human-readable metadata such as name and logo properties. This denormalization allows the UI to render favorite lists immediately without fetching referenced entities.

### How does the recent visits feature handle duplicate entries for the same entity?

The `UserRecentVisit` model uses `visited_at = models.DateTimeField(auto_now=True)`, which automatically overwrites the timestamp each time a user accesses the entity. This upsert behavior ensures only the most recent visit timestamp is retained per user-entity pair, ordered by `created_at` in descending sequence.

### Can the recent visits API be extended to support write operations?

The current `UserRecentVisitViewSet` in [`apps/api/plane/app/views/workspace/recent_visit.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/workspace/recent_visit.py) is implemented as a read-only endpoint. Writes are handled internally by the backend logic when entities are accessed, not exposed through public POST or PUT methods, ensuring data integrity through controlled server-side updates.