Skip to content

feat(family): derive family relationships from unions, projected per viewer - #1064

Open
Deeds67 wants to merge 9 commits into
mainfrom
feat/family-relationships
Open

Deeds67 wants to merge 9 commits into
mainfrom
feat/family-relationships

Conversation

@Deeds67

@Deeds67 Deeds67 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Gallery can now show how the people in your photos relate to you, not just who they are. You record two kinds of fact — unions (a partnership between people) and children (attached to a union, never to a single parent) — and every relative term is derived at read time: cousin, aunt, grandparent, step-parent, in-law, ex-wife. Nobody ever types the word "cousin".

There is deliberately no family-tree object. No document to own, name, share or delete. The graph is just a set of unions, and each viewer sees labels computed relative to themselves, so the same data reads as "your niece" for one person and "Anna's daughter" for another.

Design: specs/2026-08-31-family-relationships-design-and-slices.md. User docs: docs/docs/features/family-relationships.md. Mockups: specs/mockups/2026-08-31-family-relationships.html.

Access — off by default, two independent layers

Neither layer involves Shared Spaces.

Layer Where Default
Instance familyTree.enabled + familyTree.defaultAccess in admin settings false / none
Per user a family_access row overriding the instance default no row = inherit

enabled: false forces none for everyone regardless of grants. A row with level: 'none' is a real override, so an admin can deny one user on an otherwise permissive instance.

Admins get no implicit bypass. An admin who wants to read the graph grants themselves view like anyone else, and that grant shows up in the same list as everybody else's. Silent admin visibility into family structure is exactly what this design exists to avoid.

Write authority is the grant, not a space role. A space editor gains nothing from their space role; a contribute user may fix a union somebody else created, so a wrong relationship isn't stuck until its author gets around to it. createdById is kept for provenance.

Visibility — the graph is projected per viewer

Access grants capability; content is scoped separately and automatically:

A union is visible to you if you can resolve at least two of its participants.

"Resolve" reuses the fork's existing profile resolution (your own person row, or a shared_space_person in a space you belong to) rather than re-deriving it. Hidden people don't resolve. Pets are rejected as participants at write time.

Two rules in here are security requirements, not polish, and both are covered by medium tests:

  • Anonymous seats carry an opaque per-union slot index, never the real identityId — leaking the id would let a client correlate the same hidden person across unions and across users, reconstructing the structure redaction is meant to withhold.
  • Labels are derived from the viewer-projected subgraph only, never from the full graph. Deriving on the full graph and filtering the output still leaks, because the label is the disclosure. deriveRelationLabel takes an already-projected graph and never a repository, so it's hard to get this backwards.

On a single-family instance everyone shares spaces, everyone resolves everyone, and this behaves indistinguishably from "granted users see everything". It only diverges on an instance with strangers on it — which is where it should.

Label derivation

Computed from the viewer's root — the identity they nominated as themselves, held in user preferences. Rules apply in order: no root → plain names; target is the root → "that's you"; a path exists → the relative term; no path → described via the nearest person the viewer can reach ("Pierre's sister"), which is what stops the feature feeling broken for someone marginal to the graph; beyond the supported degree → "relative", rather than attempting "third cousin twice removed".

Gender is never inferred — from a name, a photo, or anything else. Unset yields the neutral term. Ambiguity (someone who is both your cousin and your sister-in-law) resolves deterministically: shortest path, then a fixed precedence, then identity id, so it can't vary between requests. Union status drives ex-partner wording.

Surfaces

  • /family canvas — the editing surface. Cluster chips, generation-banded layout, drag a person from the people rail onto a seat, edit a union's status and dates inline. Read-only for view, editable for contribute, absent from the sidebar for none.
  • Person page — a relations panel with derived labels.
  • Asset viewer people strip — "your niece" under each face (web and mobile).
  • Mobile — a family focus card on the person page, and relation labels in the asset details people row.
  • Admin system settings — the instance toggle plus per-user grant management.

Schema

Four fork tables in server/src/schema/migrations-gallery/1795000000000-AddFamilyRelationships.ts: family_union, family_union_partner, family_union_child, family_access. All four are in scripts/revert-to-immich.sql with the migration-row deletion, and the partnerKey deduplication carries the required migration_overrides row. Relationships survive an identity merge (identity-merge-propagation.service.ts), with a medium test proving it.

Tests

  • Server unit: family.service.spec.ts, family.controller.spec.ts, family-labels.spec.ts, family.dto.spec.ts, plus asset/person/config coverage.
  • Server medium (real DB): family-schema, family-write, family-visibility, family-identity-merge.
  • Web unit: canvas, layout, link dialog, union editor, page load, relations panel, detail panel people, sidebar, admin settings, i18n coverage.
  • Mobile widget/unit: family focus card, relation labels, provider, API repository.
  • E2E: e2e/src/specs/web/family.e2e-spec.ts.

All nine required locales are translated in the same commits, guarded by web/src/lib/i18n/family-i18n-coverage.spec.ts.

https://claude.ai/code/session_011pxc3xiWnK74Fs6GAPns6r

@Deeds67 Deeds67 added the changelog:feat Feature change for changelog label Sep 3, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation 🗄️server 🖥️web 📱mobile labels Sep 3, 2026
@Deeds67 Deeds67 added the rc Auto-build a release-candidate server image and post it on the PR label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

🧪 Release candidate build

Latest RC pr-1064-rc.17, built from c39a64a · build run

Images published

  • ghcr.io/open-noodle/gallery-server:pr-1064-rc.17 (linux/amd64 + linux/arm64)
How to run this RC

In the directory containing your docker-compose.yml, create (or append to) a docker-compose.override.yml:

services:
  immich-server:
    image: ghcr.io/open-noodle/gallery-server:pr-1064-rc.17

Then pull and restart:

docker compose pull immich-server
docker compose up -d

Each push publishes a new numbered tag, so update the image line to move to a newer RC. To roll back, point it at an earlier rc.<n> or delete the override and run docker compose up -d again.

Before rolling back to an older image, undo the newer Gallery migrations with this RC image: https://docs.opennoodle.de/install/upgrading#rolling-back-to-an-earlier-release

Previous builds (15)
Tag Commit Built
pr-1064-rc.16 2f0182a 2026-10-28 14:16 UTC
pr-1064-rc.15 ad8ce75 2026-10-15 10:22 UTC
pr-1064-rc.14 3d00188 2026-10-15 09:31 UTC
pr-1064-rc.13 b07333a 2026-10-15 09:18 UTC
pr-1064-rc.11 8ad9d9e 2026-10-15 08:34 UTC
pr-1064-rc.10 8e21e1e 2026-10-09 18:32 UTC
pr-1064-rc.9 8a2baa6 2026-10-08 23:51 UTC
pr-1064-rc.8 ee4c4b4 2026-10-08 23:16 UTC
pr-1064-rc.7 cbeaab7 2026-10-08 22:50 UTC
pr-1064-rc.6 ad25a6b 2026-10-08 22:06 UTC

5 older build(s) omitted. Their tags are still in GHCR until this PR closes.

Last updated Wed, 28 Oct 2026 15:14:52 GMT. Every push while the rc/rc-ml label is set publishes a new numbered RC. All pr-1064 RC images are deleted when this PR closes.

…viewer

Squashed from 56 commits when rebasing onto the v3.3.3 main (originals on backup/pr-1064-2026-10-15):

- docs: design family relationships as a graph with no family-tree object
- feat(family): add the union, membership and access tables
- fix(family): keep the union schema in step with its decorators
- feat(family): add the family tree instance config block
- feat(family): resolve effective family access from config and grant
- feat(family): create, edit and delete unions with their validation rules
- feat(family): deduplicate unions on their partner pair and start date
- feat(family): derive relationship labels from the projected graph
- fix(family): keep relationships intact across an identity merge
- docs: correct the family-relationships spec against the codebase
- feat(family): project the relationship graph onto what each viewer may see
- feat(family): expose family relationships over the API
- feat(family): derive relation labels over the API and add GET /family/me
- feat(family): close five API gaps found by consuming UI slices
- feat(family): extend familyRelationLabel to AssetResponseDto.people
- feat(family): add the family tree admin settings section
- feat(family): administer per-user family access grants
- feat(family): show how each face relates to you in the asset viewer
- feat(family): show a person's relationships on their person page
- feat(family): add the family page with its cluster chips
- feat(family): render the relationship graph around the viewer
- feat(mobile): load family relationships from the server
- feat(mobile): show a person's family on their person page
- feat(mobile): show how each face relates to you in the asset viewer
- fixup(mobile): render family relation labels via a person field, not a client-side join
- fix(family): align the family UI with the generated client
- feat(family): build the family graph by dragging people onto the canvas
- feat(family): edit a union's status and dates from the canvas
- feat(family): let a contributor actually edit the family canvas
- test(family): prove the feature end to end
- fix(family): accept UUID v7 identity ids in the DTO layer
- fix(family): attach relation labels to the asset-viewer people strip
- docs(family): document family relationships and guard the translations
- style(family): format the i18n coverage spec
- test(family): give the asset medium spec the config it now needs
- fix(web): meet AA contrast on the page header description in dark mode
- feat(family): let people start a family tree from an empty /family page
- feat(family): draw the family canvas the way the mockup does
- fix(family): make the canvas editable, legible and correctly connected
- fix(family): join up the connectors and make the canvas's dead ends work
- feat(family): open a person from their card, and stop clipping their label
- test(family): reach the card before expecting its drop zones
- fix(family): prune a union that removal has emptied out, and let one be deleted
- fix(family): meet AA contrast on primary buttons in dark mode
- chore: drop a scratch file committed by mistake
- chore: drop a slice report that does not belong in the repo
- fix(family): adapt to the v3.2.0 base's person_group and config changes
- chore(api): regenerate the OpenAPI spec, TypeScript SDK and SQL docs
- fix(mobile): finish the v3.2.0 migration in the family-relations strip
- test(mobile): give the family fixtures the birthDate PersonResponseDto requires
- fix(rebase): carry family relationships onto the v3.2.2 base
- test(family): give the relations panel's person fixture the fields PersonResponseDto now requires
- fix(family): satisfy the unicorn group-by and early-return lint rules in the family repository
- test(family): pass the now-required onDelete prop in the union editor spec
- fix(family): cover the family tables in the revert-to-immich guards and override cleanup
- test(mobile): give the family API fixture the person fields PersonResponseDto now requires
Family's controller, repository and service join the gallery registry, its enum
moves to src/gallery/enum.ts, its user foreign keys join the upstream FK
registry, and person and space-asset access checks, media serving and person
identity reads use main's fork seams.
Fork tables are listed in src/schema/gallery.ts on main, which is what the
upstream-FK registry check reads.
Fork strings live in the i18n/gallery overlay on main.
@Deeds67
Deeds67 force-pushed the feat/family-relationships branch from ad8ce75 to 2f0182a Compare October 28, 2026 13:51

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:feat Feature change for changelog documentation Improvements or additions to documentation 📱mobile rc Auto-build a release-candidate server image and post it on the PR 🗄️server 🖥️web

1 participant