Skip to content

Entity Relationships

This document describes the data model relationships between Peek's cached entities. Understanding these relationships is essential for working with content filtering, queries, and the sync system.


Entity Types

Peek mirrors Stash's entity model, caching entities locally in SQLite. This enables:

  • Performant queries — No network round-trips to Stash for library browsing
  • Per-user features — Content restrictions, hidden items, ratings, favorites, watch history
  • Offline resilience — Library remains accessible if Stash is temporarily unavailable
Entity Stash Source Peek Cache Table Notes
Scene Yes StashScene Primary content type
Performer Yes StashPerformer
Studio Yes StashStudio Has parent/child hierarchy
Tag Yes StashTag Has parent/child DAG
Group Yes StashGroup Has parent/child hierarchy (containing_groups/sub_groups)
Gallery Yes StashGallery Contains Images
Image Yes StashImage
Clip Yes (scene markers) StashClip A timed section of a scene; Stash calls it a scene marker
Playlist Peek-only Playlist User-created scene collections, shareable with groups

Every cached entity is keyed by its id and its Stash server (instance): the entity tables use the primary key (id, stashInstanceId), and the junction tables carry the instance of both ends, so two Stash servers can hold the same id without mixing. Peek's own per-user tables call the instance instanceId.


Scene Relationships

Related Entity Cardinality Junction Table Notes
Studio Many-to-One — StashScene.studioId
Performer Many-to-Many ScenePerformer
Tag Many-to-Many SceneTag
Group Many-to-Many SceneGroup Includes sceneIndex for ordering
Gallery Many-to-Many SceneGallery
Clip One-to-Many None StashClip.sceneId (with sceneInstanceId); a scene's clips go with it
Inherited Tag Many-to-Many SceneInheritedTag Tags the scene gets from its performers, studio and groups; see Scene Tag Inheritance

Performer Relationships

Related Entity Cardinality Junction Table Notes
Scene Many-to-Many ScenePerformer Inverse of Scene→Performer
Tag Many-to-Many PerformerTag
Image Many-to-Many ImagePerformer
Gallery Many-to-Many GalleryPerformer
Group — — Computed by Stash (performers appearing in group's scenes)

Studio Relationships

Related Entity Cardinality Junction Table Notes
Scene One-to-Many — Inverse of Scene→Studio
Tag Many-to-Many StudioTag
Image One-to-Many — StashImage.studioId
Gallery One-to-Many — StashGallery.studioId
Parent Studio Many-to-One — StashStudio.parentId
Child Studios One-to-Many — Inverse of parentId
Group — — Computed by Stash (groups with this studio)

Tag Relationships

Related Entity Cardinality Junction Table Notes
Scene Many-to-Many SceneTag Inverse of Scene→Tag
Performer Many-to-Many PerformerTag Inverse of Performer→Tag
Studio Many-to-Many StudioTag Inverse of Studio→Tag
Group Many-to-Many GroupTag Inverse of Group→Tag
Gallery Many-to-Many GalleryTag Inverse of Gallery→Tag
Image Many-to-Many ImageTag Inverse of Image→Tag
Clip Many-to-Many ClipTag Inverse of Clip→Tag
Clip (primary tag) One-to-Many None StashClip.primaryTagId (with primaryTagInstanceId)
Scene (inherited) Many-to-Many SceneInheritedTag Scenes that inherit the tag from a performer, studio or group
Parent Tags Many-to-Many — StashTag.parentIds (JSON array)
Child Tags Many-to-Many — Inverse, resolved at runtime

Tag Hierarchies

Tag hierarchies form a DAG (directed acyclic graph), not a tree. Tags can have multiple parents.


Clip Relationships

A clip is a Stash scene marker, cached as StashClip.

Related Entity Cardinality Junction Table Notes
Scene Many-to-One None StashClip.sceneId and sceneInstanceId; the clip is deleted with its scene
Primary Tag Many-to-One None StashClip.primaryTagId and primaryTagInstanceId
Tag Many-to-Many ClipTag The marker's other tags

Clips carry the start (seconds) and an optional end (endSeconds), and the paths of the preview, screenshot and stream. A clip's scene filters read the scene's tags and its inherited tags.


Group Relationships

Related Entity Cardinality Junction Table Notes
Scene Many-to-Many SceneGroup Includes sceneIndex for ordering
Tag Many-to-Many GroupTag
Studio Many-to-One — StashGroup.studioId
Containing Groups Many-to-Many GroupRelation Rows where the group is the sub-group (subId)
Sub Groups Many-to-Many GroupRelation Rows where the group contains (containingId); orderIndex keeps Stash's order

Group Hierarchy

Stash stores the group hierarchy as one table, and its containing_groups and sub_groups are the same links read from either end. Peek keeps it the same way: one GroupRelation row per link, from the containing group to the sub-group, with the sub-group's place in the list (orderIndex) and Stash's description of the link (description, e.g. "Part 2"). Both groups are keyed on their instance; a link never crosses Stash servers. Every sync reads the whole hierarchy again, since Stash's sub-group edits move no group's updated_at.


Related Entity Cardinality Junction Table Notes
Scene Many-to-Many SceneGallery Inverse of Scene→Gallery
Performer Many-to-Many GalleryPerformer
Tag Many-to-Many GalleryTag
Studio Many-to-One — StashGallery.studioId
Image One-to-Many ImageGallery

Image Relationships

Related Entity Cardinality Junction Table Notes
Gallery Many-to-Many ImageGallery
Performer Many-to-Many ImagePerformer
Tag Many-to-Many ImageTag
Studio Many-to-One — StashImage.studioId

Playlist Relationships (Peek-only)

Related Entity Cardinality Junction Table Notes
Scene Many-to-Many PlaylistItem Includes position for ordering
User Many-to-One — Playlist.userId
Group Many-to-Many PlaylistShare One row per group the playlist is shared with; recipients see only what their own exclusions allow

Metadata Inheritance

Peek pre-computes inherited metadata during sync to enable efficient filtering and consistent behavior.

Scene Tag Inheritance

Scenes inherit tags from their associated performers, studio, and groups. This enables tag-based filtering to work transparently across all related entities.

Inheritance sources:

Source Description
Performer Tags Tags on any performer in the scene
Studio Tags Tags on the scene's studio
Group Tags Tags on any group the scene belongs to

Key rules:

  • No duplication: Tags already directly on the scene are not added to inherited tags
  • Deduplicated: Same tag from multiple sources (e.g., two performers) appears once
  • Stored separately: Inherited tags are stored apart from direct tags: as the inheritedTagIds JSON column on StashScene and as SceneInheritedTag rows (one per scene and tag, indexed by tag). The tag filters and counts read the rows
  • When computed: After every full sync, and after an incremental or smart sync in which a scene changed or a performer's, studio's or group's tag set changed

Query behavior:

When you filter scenes by tag, Peek searches both direct tags AND inherited tags. This means:

  • Filtering by a performer's tag shows scenes with that performer
  • Filtering by a studio's tag shows scenes from that studio
  • Content restrictions on tags apply to inherited tags too
  • "Has none" and "has any" on tags count inherited tags too: a scene whose only tags are inherited is tagged, and is in that tag's folder in the folder view
  • A clip's scene tags filter reads the scene's own and inherited tags
  • An image's tags include the tags it copied from its galleries (see Image Gallery Inheritance), so tag filters, counts and the Untagged folder treat them as its own

Example: If performer "Jane" has tag "Comedy", all scenes with Jane will match a "Comedy" tag filter, even if the scene itself isn't tagged "Comedy".

Sub-tags:

With Include sub-tags (a depth, or every level), each tag you pick stands for itself and its descendants, which Peek walks through every parent, since tags form a DAG. The match is the same on direct and inherited tags:

  • Has any of a set of tags matches a scene that has any of them or any of their descendants
  • Has all of is one test per picked tag, each with that tag's own descendants: a scene matches when, for every picked tag, it has the tag or one of its descendants. It does not need a descendant of every picked tag at once
  • Has none of, and the excluded tags of an include/exclude pick, leave out the descendants too, to the same depth

Favorite tags:

The Favorite Tags filter (Yes or No) uses your favorite tags only, always with all their descendants, and counts a tag the scene has directly or inherits. A favorite you hid, or one a content restriction removes for you, does not count (an admin is not restricted). If you have no favorite tags, Yes matches nothing and No is no filter. Favorite Studios works the same way with sub-studios. Favorite Performers matches the scene's own performers. Galleries and images follow the same rules (an image's tags include its galleries'), and the performer filter Has a Favorite Tag matches a performer with a favorite tag or a sub-tag of one.

Performer tags:

The Performer Tags filter on scenes, galleries and images matches an item with a performer who holds one of the tags (and their descendants, with a depth). Only performers and tags you can see count.

The same filter twice:

In a match-all group a filter can appear in several rows, and each row is its own test, all AND-ed: two Tags rows, "has all of A, B" and "has any of C, D", need both. In a match-any group, rows on the same filter that each say "has any of" (or "has all of" one tag) and use the same depth merge into one test over all their values; rows that cannot merge, such as "has all of" two or more tags, stay separate tests.

Images inherit metadata from their parent gallery when the image's own field is empty. This ensures images in a gallery share consistent metadata without manual duplication.

Inherited fields:

Field Inheritance Rule
Performers Inherit if image has no performers
Tags Inherit if image has no tags
Studio Inherit if image has no studio
Date Inherit if image has no date
Photographer Inherit if image has no photographer
Details Inherit if image has no details

Key rules:

  • Never overwrites: Only copies when image field is NULL/empty
  • All-or-nothing for relationships: Performers and tags only inherit if image has NONE
  • Multi-gallery handling: Studio, date, photographer and details come from the first gallery, in gallery ID order, that has a value. Performers and tags come from every gallery the image is in, added together
  • When computed: After every full sync, and after any sync that wrote images (even unchanged ones, whose junction rows sync rewrites) or changed galleries

Note

Image title is NOT inherited — each image keeps its own name.

Example: A gallery has studio "Acme" and tags "Nature", "Landscape". An image in that gallery with no metadata will inherit all three. An image with its own studio but no tags will keep its studio and inherit the tags.


Stash GraphQL Schema Reference

Key entity types from Stash for reference:

  • Scene: id, title, date, studio, performers[], tags[], groups[], galleries[], files[] (contains duration)
  • Performer: id, name, tags[], scene_count, image_count, gallery_count, group_count
  • Studio: id, name, parent_studio, child_studios[], tags[], groups[]
  • Tag: id, name, parents[], children[], various *_count fields
  • Group: id, name, studio, tags[], containing_groups[], sub_groups[], scenes[]
  • Gallery: id, title, date, studio, performers[], tags[], scenes[], image_count, photographer, details
  • Image: id, title, date, studio, performers[], tags[], galleries[], photographer, details
  • SceneMarker: id, scene, primary_tag, tags[], seconds, end_seconds

See also: Technical Overview for architecture details, Sync Architecture for sync details