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.
Gallery Relationships¶
| 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
inheritedTagIdsJSON column onStashSceneand asSceneInheritedTagrows (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.
Image Gallery Inheritance¶
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[](containsduration) - 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*_countfields - 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