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

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 as the IFavorite interface:

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 wraps these endpoints:

// 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 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 as the UserRecentVisit class:

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:


# 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 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) consumes the endpoint through the fetchWorkspaceRecents method:

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

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

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

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, 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 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.

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 →