Data model

erDiagram
    FOLDER ||--o{ FOLDER : "parent / children"
    FOLDER ||--o{ BOOKMARK : contains
    BOOKMARK ||--o{ BOOKMARK_TAG : has
    TAG ||--o{ BOOKMARK_TAG : "tagged on"

    FOLDER {
        string Id PK
        string Name
        string ParentId FK
        string Icon
    }

    BOOKMARK {
        string Id PK
        string Title
        string Url
        string Description
        string Thumbnail
        string FolderId FK
        bool Favorite
        datetime DateAdded
    }

    TAG {
        int Id PK
        string Name UK
    }

    BOOKMARK_TAG {
        string BookmarkId PK_FK
        int TagId PK_FK
    }

Entities

Folder

A self-referencing tree. ParentId is nullable (top-level folders have none). Deleting a folder that still has bookmarks or subfolders returns 409 Conflict unless the caller passes ?cascade=true, which detaches (not deletes) its bookmarks and re-parents its subfolders to the folder’s own parent. Deleting a folder that a bookmark points to via a foreign key sets Bookmark.FolderId to null at the database level (OnDelete(DeleteBehavior.SetNull)).

Bookmark

The core entity. Description and Thumbnail default to an empty string rather than null. Belongs to at most one folder (optional) and any number of tags via the BookmarkTag join entity.

Tag

A flat, globally unique (Name has a unique index) label. Tags are created on demand: when a bookmark request references a tag name that doesn’t exist yet, BookmarkService.SetTagsAsync creates it. Tag names are trimmed and de-duplicated (case-sensitive) before persistence — see Validation.NormalizeTags.

BookmarkTag

The many-to-many join entity between Bookmark and Tag, keyed on the composite (BookmarkId, TagId). Both sides cascade-delete: removing a bookmark or a tag removes the corresponding join rows.

Validation rules

Field Rule
BookmarkRequest.Title Required, non-empty
BookmarkRequest.Url Required, must be an absolute http/https URL (AbsoluteUrlAttribute)
BookmarkRequest.FolderId If set, must reference an existing folder
FolderRequest.Name Required, non-empty
FolderRequest.ParentId If set, must reference an existing folder, and must not create a cycle

Validation failures and “referenced entity not found” errors both surface as 400 Bad Request with a ValidationProblemDetails body (see Security for the full error-mapping table).