A photo and video library for PhoenixKit, in the spirit of Apple Photos and Google Photos.
The timeline is the first view and the spine of the product: albums, people,
map and memories are scopes and views over the same date-ordered, virtualized
engine. See dev_docs/plans/2026-09-21-phoenix-kit-photos.md for why this is
a separate package rather than a MediaBrowser view mode in core.
This is the PhoenixKit module entry point: PhoenixKit.ModuleDiscovery finds
it by scanning beam attributes of deps that depend on :phoenix_kit, so a
host application needs no configuration beyond the dependency itself.
What this module is, and is not
Three media widgets exist and must not be conflated:
PhoenixKitWeb.Components.MediaBrowser— file work: folders, ingest, search.PhoenixKitWeb.Components.MediaGallery— picking and ordering a set of images for a form.PhoenixKitPhotos.Components.PhotoTimeline— a person's library ordered by capture date, virtualized and scrubbable.
Status
Stage 1 reads one storage library, ordered by capture date. The scope is
{:library, uuid}. Capture date and the library partition live in
PhoenixKit core (V200 and V202); this package does not migrate either.
Summary
Functions
The tools agents get through the machine API: see PhoenixKitPhotos.API.
Tailwind source roots, so classes used only in this package's HEEx survive the host's CSS build.
The prebuilt hook bundle, built by mix assets.build into this app's priv/.
Permissions, specified before the first route ships rather than after.
Claim photos so no other module's routes take it.
The package's default page, at /photos.
Functions
The tools agents get through the machine API: see PhoenixKitPhotos.API.
Tailwind source roots, so classes used only in this package's HEEx survive the host's CSS build.
Without this the :phoenix_kit_css_sources compiler warns that a
phoenix_kit-dependent dep contributes zero @source lines, and any
responsive or variant utility used only here is silently missing from the
host bundle.
The prebuilt hook bundle, built by mix assets.build into this app's priv/.
js_sources/0 does not compile anything — it declares a bundle that already
exists, which is why building it is a Stage 1 deliverable and not a packaging
afterthought.
Permissions, specified before the first route ships rather than after.
An owner sees their own library; photos.view_any additionally
allows viewing another user's. Nobody else sees anything: a signed thumbnail
URL is not authorization (URLSigner tokens are 4 hex characters and never
expire), so the window API is what enforces scope.
Claim photos so no other module's routes take it.
The package's default page, at /photos.
A user_dashboard_tabs/0 entry carrying a live_view becomes a real route
in PhoenixKit's authenticated surface, which is emitted twice — once at
the root and once locale-scoped — so this one callback yields both /photos
and /:locale/photos, permission-gated.
That is also why there is no route_module/0 here: it would emit a third,
duplicate /photos and the host router would warn that the clause can never
match. The one trade is that this page depends on the user dashboard being
enabled in the host.
The path is absolute on purpose — a relative one resolves under
/dashboard/.
A host wanting its own branded page declares /photos before
phoenix_kit_routes(); first match wins, so the host shadows the root shape
without this package knowing about it.