All projects
09MOBILE APP2026
Poof app icon: a white seedling with four leaves inside a broken ring on a green background

Poof

A camera-roll cleaner that works like Tinder — swipe left to delete, right to keep, and pay for the whole session with a single delete prompt.

A camera-roll cleaner that works like Tinder. Swipe left to delete, right to keep, review what you marked, then clear it all with one delete prompt. No server, no account, no analytics.

Poof Play Store feature graphic: the wordmark and headline "Let go of what you don't need." beside a phone showing the swipe deck

Overview

Poof is an iOS and Android app for people whose photo library has quietly grown past the point where the Photos app is usable. It shows one item at a time: swipe left to mark it for deletion, right to keep it, up to peek at the date, file size and EXIF metadata, or to play a video with full controls. Nothing is actually removed while you swipe. Decisions pile up in a queue, and when you tap Done you get a grid of everything marked, a running byte total, and one button that deletes the lot.

The product is built around a single deliberate constraint: no server, no account, no analytics. Every preference, the marked queue, the cleanup history and the 24-hour restore log live in the device's own storage, and the app makes no network requests at all — the privacy claim is structural rather than a promise in a policy page. That pushes all the hard work on-device, where a photo library of 12,000 items is an ordinary case. Library reads are paged through the OS photo library with per-sort native filters where they exist and bounded client-side filters where they don't; asset sizing runs eight lookups in flight because each one costs two native round-trips; and the largest-files and storage-breakdown scans cap out, cache for 24 hours, and carry a `covered` flag so the UI can say when a number is a sample rather than presenting a partial total as the whole library.

Around the swipe deck sit the surfaces that make a cleanup session worth starting: a storage breakdown of photos vs videos vs screenshots, a ranked list of the biggest files, burst-duplicate clustering that groups retakes taken within three seconds of each other so one keeper can be picked, month-by-month and album-by-album sweeps, auto-categories such as screenshots older than thirty days and videos longer than a minute, and a "Daily Ten" habit loop. Guardrails run underneath all of it — anything shot in the last seven days, anything in Favourites, and any album the user hides are never offered up for deletion. The app is Expo SDK 54 / React Native 0.81 in strict TypeScript, routed with expo-router, with the deck in zustand and theme, lock and toast in React context, and it ships to both stores through EAS Build and Submit alongside a static marketing site carrying the privacy policy and terms the store review process requires.

Problem

A camera roll with twelve thousand items is unusable in the stock Photos app, and every deletion there raises its own system confirmation — so nobody ever starts.

Solution

A one-card-at-a-time swipe deck backed by a persisted decision queue, plus duplicate, largest-file and storage-breakdown scanners, all running on device with a single batched delete at the end.

Outcome

Version 1.0.1 ships to the App Store and Google Play from one Expo codebase, with twelve entry points into the deck and a full session costing exactly one OS delete prompt.

Screenshots

Hand-drawn illustration of a person carrying a large potted plant with green leaves
Onboarding slide one — "Clean up your camera roll." The metaphor throughout is pruning, not destruction; red is deliberately absent from the palette.
Hand-drawn illustration of a person mid-throw, arms swinging, in a green shirt
Onboarding slide two — "Swipe like Tinder." Left deletes, right keeps, up opens metadata or plays the video.
Hand-drawn illustration of a person kneeling beside a green dog
Onboarding slide three — "Let go of what you don't need." Tapping Done shows everything marked; one confirmation clears it.

Architecture & flows

How it fits together

Press play to walk through a flow — each pseudo-code line lights up the parts of the system it touches.

sort selection
Client

Poof app

Expo · React Native

Client

Marketing site

Static HTML · privacy & terms

Service

Swipe deck & queue

zustand store

Service

Scanners

largest · breakdown · bursts

Service

Review & batch delete

one confirmation

Database

On-device store

AsyncStorage

External

OS photo library

expo-media-library

Infra

EAS Build & Submit

App Store · Play Console

  • Client
  • Service
  • Database
  • External
  • Infra

A hundred swipes, one delete prompt

The core loop. Nothing leaves the library while you swipe — decisions become a durable queue that is spent in a single call at the end.

A query key over sort, album, month and the safety settings tells a real reload from a redundant re-focus.

1 / 8

Decisions

Engineering decisions

Batch the deletes into one confirmation

Context
Both iOS and Android raise their own system dialog for every photo-library deletion, which would turn a hundred-swipe session into a hundred modal interruptions.
Decision
Treat a swipe as a decision, not an action: accumulate marked ids and their assets in a zustand store mirrored to disk, and call `deleteAssetsAsync` exactly once from a review screen the user reaches by tapping Done.
Consequence
The whole session costs one prompt and stays reversible until the final tap, at the price of owning a durable queue, an undo history and a review grid where any tile can be deselected.

No server, no account, no telemetry

Context
The app is pointed at the most private folder on the phone, and a cleanup tool that phones home is a hard sell no matter what its policy says.
Decision
Ship with zero network calls and no backend at all — onboarding state, sort preferences, hidden albums, the marked queue, cleanup statistics and the 24-hour restore log live in AsyncStorage on the device.
Consequence
The privacy guarantee is structural and the app works fully offline; the trade is no crash reporting, no funnel analytics and no way to observe a bug that only reproduces on a stranger's library.

Cluster duplicates by capture time, not by pixels

Context
Real perceptual deduplication would mean decoding and hashing thousands of full-resolution images on a phone, which is slow, battery-hungry, and needs iCloud-only assets downloaded first.
Decision
Detect the case that actually dominates a camera roll — burst shots and retakes — by walking roughly four hundred recent photos and grouping any run captured within three seconds of the previous one, then asking the user to pick a single keeper per group.
Consequence
The scan finishes in seconds with no downloads and no false deletions, but it will not catch a re-saved or re-downloaded copy that shares no timestamp with the original.

Guardrails before the deck, not warnings after it

Context
Irreversible-feeling actions on personal photos need a much wider safety margin than a confirm dialog provides, and the items most likely to be regretted are the ones just taken or explicitly loved.
Decision
Filter at the query layer: fold a seven-day recency cutoff into the native `createdBefore` filter, resolve and exclude the Favourites smart album across nine locale spellings, and drop any album the user marks hidden.
Consequence
The deck simply never offers a photo the user is likely to regret, and the settings that relax the rails are part of a cache key so changing them reloads the deck; the cost is an extra album walk and a deck that can look emptier than the library really is.

Expo managed workflow with EAS Build and Submit

Context
The app needs media-library, biometric, video and font native modules on two platforms, but it is a solo project where signing infrastructure and CI are pure overhead.
Decision
Stay in the Expo managed workflow with config plugins declaring the permission strings and granular media permissions, and drive releases through EAS Build and Submit with remotely managed credentials and an auto-incrementing production profile.
Consequence
One `app.json` produces correctly configured iOS and Android builds and a release is two commands per platform, in exchange for a prebuild step whenever native configuration changes and a dependency on a hosted build service.

Integrations

Notable integrations

expo-media-library

The only source of truth in the product — reads photos and videos, resolves albums, and performs the actual deletion.

Configured through a config plugin that declares the photo and video granular permissions and the usage strings, then queried page by page with native sort, media-type, album, subtype and creation-date filters; deletion goes through a single `deleteAssetsAsync` call.

expo-file-system

Answers "how big is this file", which the SDK does not expose on an asset directly.

The modern `File` API handles `file://` URIs and the legacy `getInfoAsync` covers the `content://` URIs Android returns for video, with both paths failing soft so an unsized iCloud asset never breaks a scan.

expo-local-authentication

Optional app lock — every photo in the library is put on full-screen display, so an unattended phone is a real risk.

Hardware and enrolment are probed before the toggle is offered, and when enabled a lock overlay requires Face ID, Touch ID or the device passcode before the deck is reachable.

expo-video & expo-video-thumbnails

Videos are usually the biggest single wins in a cleanup, so they have to be judgeable inside the deck.

First-frame thumbnails render on deck and review tiles, and the metadata sheet swaps in a full player with controls, with background playback and picture-in-picture explicitly disabled.

EAS Build & Submit

Produces and ships the signed iOS and Android artifacts.

Development, preview and production profiles in `eas.json` with a remote app-version source; production auto-increments, EAS manages signing credentials, and submit uploads to App Store Connect and the Play Console internal track.

Google Fonts via @expo-google-fonts

Bricolage Grotesque for the wordmark and display type, Inter for the interface.

Both families are bundled into the binary and loaded through expo-font at startup behind the splash screen, so the typography ships with the app rather than being fetched at runtime — which keeps the zero-network guarantee intact.

Stack

  • Expo
  • React Native
  • TypeScript
  • expo-router
  • zustand
  • expo-media-library
  • EAS Build