Summary
Once a week, on Saturday at 10:00, Scout can remind you of a trip from a past year: "3 years ago this week · Sunset Crater Volcano National Monument and 4 more places". It is built from the dated photos already on your phone, the same ones the camera-roll import filed into your Album. A tap opens a Story of that trip: full screen, photo by photo, the place's patch large beside its name, ending on a postcard you can share. The Album shows the same memory as a card at the top during that week, so a missed notification is not lost. See the trip opens that trip's page.
It was first built for a paid tier that is not live, and made free for everyone on 2026-11-04 as a reason to come back each week (the 2026-11-04 analytics read: 88% of installs finish onboarding, but only 48% come back on a later day).
Status
- Built, not in a released app yet. On branch
feat/retention-alerts(PR todevelop, 2026-11-04). It reaches phones with the next store build. No feature flag: it is on for every signed-in user, behind a per-device switch (default on). - The paid-tier pieces it sat beside stay paid and stay off: the recap reel, chapter share cards and links, cloud backup and the home-screen widgets.
User-facing surfaces
- The weekly notification (local, never pushed from a server): title "N years ago this week"
(or "1 year ago this week"), body the place, or "{place} and N more place(s)". A tap opens
scout://on-this-day?key=<chapter>(onThisDayUrl,mobile/src/domain/onThisDay.ts:41). - The Story (
mobile/app/on-this-day.tsx, route/on-this-day?key=ch-<startMs>): up to 8 photos (STORY_MAX_PHOTOS,onThisDay.ts:134), about 4 s each (STORY_FRAME_MS,useStoryPlayer.ts:7), progress bars at the top, the app's standard glass close button top left (Scout.IconButton variant="glass", 2026-11-05); tap the right half for the next photo and the left half for the previous one. It pauses while anything covers it (the Share sheet). The place with the most photos leads, its patch large to the left of its name, with "and N more places · N places · N photos · N days". The last frame is the postcard, with Share and See the trip. Layout:mobile/src/components/on-this-day/MemoryStoryView.tsx. - Share opens its own route,
/on-this-day-share?key=(mobile/app/on-this-day-share.tsx): a blackRouteSheetover the Story with no scrim, springing up, the store-interest sheet's shape (Alan, 2026-11-05: the scrimmedScout.Sheetit used to be "looks weird"). Tapping off it, a pan down, or a shared link closes it; a key with no trip on this phone closes it at once. ContentShareMemorySheet.tsx, two rows:- Share a picture makes the postcard on the phone (a 1080×1920 JPEG: the cover photo full bleed, the patch beside the place name, the official Scout wordmark and scout-patches.com) and hands it to the share sheet. Nothing is uploaded.
- Share a link · 7 days says, before the tap, exactly what it uploads ("Uploads 5 photos,
shrunk and without location, for 7 days. Anyone with the link can see them; then they delete
themselves.",
linkFinePrint), then sharesscout-patches.com/m/<code>with the line "3 years ago this week: <place>, on Scout:".
- The web page
scout-patches.com/m/<code>(backend/views/memory/page.hbs): the same Story in the browser — the Scout wordmark on a dark band at the top with "3 years ago this week · 6 days left", the progress bars below it, the photos, the patch beside the place name over a strong bottom shade, then the postcard — with Get Scout, free (/get?src=memory) and "Already have it? Open in Scout" (scout://m/<code>). After 7 days: "This memory has expired" (HTTP 410), still offering the app. An unknown code, or one still uploading: "This memory isn't here" (404).noindex; the link preview (og:image) is the postcard (/m/<code>/card.jpg). - A shared link opened in Scout (
mobile/app/m/[code].tsx): the same Story from the link's photos (no See the trip; Share passes the same link on), "This memory has expired" after 7 days, "This memory isn't here" for an unknown code, and Try again on a network error. - See the trip opens the trip's chapter page,
/album-chapter?key=(free since 2026-11-04;mobile/app/album-chapter.tsx:59). - The Album card (
OnThisDayAlbumCard.tsx, placed bythisWeekCardinmobile/app/(drawer)/album.tsx:465): during the week of a memory, a photo card at the top of the Album with "N years ago this week", the place, its patch and Watch, which opens the Story. Both Album layouts (the redesign behindalbum_places_redesign, and today's). - The switch: Settings → Notifications → On this device → On This Day ("A weekly look back
at your trips"), testID
notif-toggle-on-this-day(mobile/app/notification-settings.tsx:514). It shows for everyone.
How it works
-
Chapters, from the phone only.
useDeviceAlbumChapters(mobile/src/hooks/useAlbumChapters.ts:89) builds chapters (runs of dated photos, no gap over 48 h, at least two photos; seedomain/albumChapters.ts) fromvisitPhotos, the camera-roll photos the import filed. It never asks the cloud album, for anyone. -
Scheduling.
useOnThisDaySync(mobile/src/hooks/useOnThisDaySync.ts:29-31), mounted at the app root, is enabled for a signed-in user with the switch on.weeklyMemories(onThisDay.ts:76) picks the next 4 Saturdays that have a memory;syncOnThisDay(mobile/src/services/onThisDayNotifications.ts:34) cancels every pending On This Day notification and schedules those, only when the OS permission is already granted. It never asks. It re-syncs on launch, on return to the app and when the chapters change. -
The rules (unchanged from the paid-tier build): a week is Saturday to Friday, matched by month/day in each day's own year; the most recent past year wins; a Feb 29 photo counts as Feb 28 in a non-leap year; a week with no memory sends nothing.
-
This week's card.
memoryThisWeek(onThisDay.ts:126) finds the memory of the week we are in (the one the notification already announced), andmemoryStory(onThisDay.ts:184) turns its chapter into the Story's content: the lead place, the cover first, the rest spread evenly across the trip. -
A 7-day link (
mobile/src/hooks/useShareMemoryLink.ts). The sheet captures the postcard, then:POST /api/shared-memories(the lead place by patch id, the "years ago" phrase, the trip's first and last calendar day (YYYY-MM-DD) and counts) returns a code and presigned PUT urls; each Story photo still on the phone is shrunk to 1080 px and re-encoded as JPEG (shrinkCameraRollPhotoSized; the re-encode carries no EXIF, so no location) and PUT straight to private S3 (presignedPut), one at a time; a photo no longer on the phone, or whose upload fails, is skipped; the postcard is PUT (if it fails, the link fails and says so);POST /api/shared-memories/:code/commitnames the slots that landed; the server checks each object (there, at most 3 MB,image/jpeg), copies it to alive/key that no upload url can reach, and deletes the upload, before the link goes live. Then the OS share sheet opens with the url. -
On the server (
backend/src/shared-memories/): the place name and patch art come from the catalog bypatchId, and only for a patch the app can show (VISIBLE_PATCH_WHERE, the content sync's own rule); the server writes the date line itself from the two days (formatDateRange: "Oct 27 – 28, 2023"), so any phone language works and no free text reaches a page on our domain; the one phrase the phone sends must be the app's own ("3 years ago this week",isValidOver). At most 10 links per person per rolling 24 h (counted in the database,SHARE_DAILY_LIMIT), at most 8 photo slots (SHARE_MAX_PHOTOS). Commit refuses any upload over 3 MB or notimage/jpeg(a presigned PUT cannot cap size); the page signs only the checkedlive/copies, for an hour, forced to be served asimage/jpeg, so a PUT that lands after commit (the url lives 10 minutes) changes nothing anyone sees. Objects live under the owner:shared-memories/<userId>/<code>/. A daily job at 03:30 CT (shared-memory-sweep) deletes the S3 objects of every link past its 7 days and stampsdeleted_at(or removes the row, once its account is gone). Deleting an account (AccountService,deleteAllForUser) deletes everything under the owner's prefix at once (rows or not) and expires every link that moment, so the page says "expired" immediately; the next sweep clears the prefix again (catching an upload that was still in flight) and removes the rows.
Data model
- On the phone: none new. Chapters are derived from
visitPhotos; the switch isonThisDayEnabled. shared_memories(SharedMemory, migration20261104120000_shared_memories, hand-applied):code(10 random characters, the key),user_id,patch_id,over,date_range,and_more,places,photos,days,photo_slots,uploaded(the slots that landed),created_at,ready_at(set by commit),expires_at(created + 7 days),deleted_at(sweep). The bytes live in private S3 undershared-memories/<userId>/<code>/: the uploads land at<slot>.jpgandcard.jpg, commit copies the checked ones tolive/<slot>.jpgandlive/card.jpg(the only keys the page signs) and deletes the uploads;date_rangeis written by the server from the two days the app sends.
API surface
POST /api/shared-memories— signed in (JwtAuthGuard); per-account burst throttle 20/h plus the 10/day cap (429 with "You can share up to 10 links a day. Try again tomorrow.").POST /api/shared-memories/:code/commit— signed in, the link's owner only (404 otherwise); 400 when a named upload or the postcard is missing, over 3 MB or not a JPEG; 410 once expired.GET /api/shared-memories/:code— public JSON of the same view (live 200, expired 410, else 404), for the app's own/m/<code>route.GET /m/:code— the public page;GET /m/:code/card.jpg— 302 to the signed postcard (or the site card once expired)./mis excluded from the SPA fallback (backend/src/spa-fallback.ts).
Key files
mobile/src/domain/onThisDay.ts— the rules, the url, this week's memory, the Story content (pure).mobile/src/hooks/useOnThisDaySync.ts,mobile/src/services/onThisDayNotifications.ts— scheduling.mobile/src/hooks/useAlbumChapters.ts:89—useDeviceAlbumChapters.mobile/app/on-this-day.tsx— the Story route and its view model;mobile/app/on-this-day-share.tsx— the Share sheet route.mobile/src/hooks/useOnThisDayStory.ts— the Story for a key, read by both routes.mobile/src/components/on-this-day/—MemoryStoryView,MemoryPostcard,ShareMemorySheet,OnThisDayAlbumCard.mobile/app/album-chapter.tsx— the trip page, free.mobile/src/hooks/useShareMemoryLink.ts,mobile/src/api/sharedMemories.ts,mobile/src/lib/presignedPut.ts— the link.backend/src/shared-memories/— rules, service, controller, the daily sweep;backend/views/memory/page.hbs— the page.backend/views/privacy/page.hbs§ Sharing a memory — what the privacy policy says about it.- Design:
mobile/research/on-this-day-lab.html(picks marked).
Configuration and flags
- No flag. The device switch
onThisDayEnabled(default on). APP_LINKS_CLAIM_MEMORIES(backend env, default off) — adds/m/*to the iOS AASA (see What this feature does NOT do).- The unlaunched paid tier's flag only changes the trip page: with it on, the page reads the cloud-merged chapters and shows the recap reel and share-with-backup buttons.
Edge cases and known limits
- Photos deleted from the phone since: the Story shows the patch art instead of an empty frame, and the postcard uses the patch art as its picture. A chapter whose photos are all gone no longer exists, so its key opens "This memory isn't on this phone" with a way to the Album.
- The label depends on when you open it. In the memory's own week it is "N years ago this
week" (the notification's count). Opened any other time (the Album's trip list, a saved link),
it is "N years ago" in whole years since the trip's first day (
memoryStory), and "This trip" when that is under a year. The server accepts only those phrases (isValidOver). - Notifications already scheduled outlive a switch-off until the next app open, when the sync cancels them (they are local).
- It needs dated photos on this phone. Someone who never ran the camera-roll import, or whose photos are on another phone, gets no memories.
- A link made from a trip whose photos are gone uploads only the postcard (the fine print says so); the page then shows the patch art in place of photos.
- iCloud-only originals are not downloaded for a link (the shrinker reads only what is on the phone); they are skipped like a deleted photo.
- "Expired" is not instant everywhere. The page answers 410 from
expires_at, but a shared cache may serve the live page up to ~6 minutes longer, its signed photo urls work for up to an hour after they were issued, and the S3 objects themselves go at the next 03:30 CT sweep. - A presigned upload url works for 10 minutes. A PUT after commit lands on the upload key,
which the page never signs; the sweep deletes it with the link. An upload still in flight when
the account is deleted lands after the prefix was cleared and is removed by the next 03:30 CT
sweep (the privacy policy says "within a day"). Belt and braces, not done: an S3 lifecycle rule
expiring
shared-memories/after 8 days. - A trip's key is its first photo's time. Delete that photo after the notification and the key
moves; the Story finds the trip by its dates instead (
findChapter). - "N years ago" matches the notification, which judges each day of the week in its own year:
a Jan 1 trip is "1 year ago" in the week of Dec 27 (
memoryStorytakes the week's count). - Only a deleted account removes its links early. The other paths that delete user data
(
deleteUserData: anonymous-account cleanup, test-account cleanup, a guest merged into an account, device sync) leave its links to the daily sweep, so they still disappear within 7 days.
What this feature does NOT do
- It does NOT use a cloud copy of your photos. Memories come only from the photos on this phone; a new phone has no memories until the import runs there.
- It does NOT push from a server. The notification is scheduled on the phone, so it can't reach someone who has not opened the app in the last 4 weeks.
- It does NOT ask for notification permission. It uses the permission onboarding asked for.
- It does NOT make a video. The Story plays photos in the app; the recap reel (an MP4) is part of a paid tier that is not live.
- Share a picture does NOT upload anything. Only "Share a link" uploads, and only that trip's Story photos (at most 8, shrunk) and the postcard — never the album.
- A link CANNOT be stopped early from the app. It lives 7 days; deleting the account removes it at once. There is no admin tool for one link either (the privacy policy says to contact us).
- A link is NOT password-protected or private. Anyone with it can see the photos for 7 days.
- It does NOT keep a link's photos after 7 days, and there is no way to extend one.
- A shared link does NOT open in the app yet. The app route (
mobile/app/m/[code].tsx) and both claims are built: Android'spathPattern: /m/.*(mobile/app.json) ships in the next store build; iOS's/m/*is served by the backend's AASA only whenAPP_LINKS_CLAIM_MEMORIES=true(iosClaimedPaths,backend/src/app-links/app-links.config.ts), off by default. iOS reads the AASA for every installed build, so claiming/m/*before most phones have the route would open older builds on a screen they don't have; set the env var once the build is widely installed. Until then the link opens the web page; its "Open in Scout" (scout://m/<code>) works for anyone with a build that has the route. - There are no home-screen widgets for it. Those belong to a paid tier that is not live.
Tests that cover it
mobile/src/domain/__tests__/onThisDay.test.ts— the weekly rules, the Story url, this week's memory (every day of the week, the days either side), the Story's lead place, cover-first order, the 8-photo spread, the label in and out of its week ("N years ago this week", "N years ago", "1 year ago", "This trip"), andfindChapter(exact key, else by dates after the first photo is deleted).mobile/src/hooks/__tests__/useDeviceAlbumChapters.test.ts— every device-only reader (the root sync, the Album card, the Story) shares one chapter build; new photos, a renamed trip or a newly collected patch build again (mutation-checked).mobile/screen-tests/on-this-day.test.tsx— the Story plays the seeded trip, taps through to the postcard and back, See the trip, Share opens/on-this-day-sharefor this trip, the glass close, an unknown key, signed out (falsified against a blank seed); the Share sheet route offers the picture, closes itself for a key with no trip (paired with a real one), and closes on a tap off it or once a link is shared (the Share and glass-close tests each fail against the 11-04 code).mobile/screen-tests/on-this-day-sync.test.tsx— scheduled for a regular user without the paid tier; the tap opens the Story; switch off and signed out cancel; never a cloud album request.mobile/screen-tests/album.test.tsx(On This Day card) — the card in both Album layouts, and none for another week.mobile/screen-tests/album-chapter.test.tsx— the trip page free, no paid-tier buttons, no bounce while flags load, the skeleton while the catalog loads.mobile/screen-tests/notification-settings.test.tsx— the switch shows for everyone.mobile/screen-tests/on-this-day.test.tsx(Share a link · 7 days) — the fine print before the tap, the full create → upload → commit → share round trip, a photo gone from the phone skipped, the daily-limit message with nothing uploaded (mutation-checked).backend/src/shared-memories/shared-memory.rules.spec.ts— the 7 days, codes, keys, the phrase and date patterns, a link's state.backend/src/shared-memories/shared-memories.db.spec.ts— signing, free text refused, the 10/day cap (under the advisory lock), commit checks S3 and copies tolive/, the view signs onlylive/keys, the sweep (one failure does not stop it; a deleted account's rows removed), account deletion expires every link at once (each mutation-checked).backend/api-tests/shared-memories.api.spec.ts— the page live / expired (410) / pending and unknown (404), the preview redirect to thelive/card served asimage/jpeg, a link with no photos playing the patch art, the JSON, auth, free text refused, the SPA exclusion.backend/src/account/account.service.spec.ts— account deletion removes shared memories first, and aborts if that fails.mobile/screen-tests/m-code.test.tsx— a shared link in the app: plays, taps to the postcard, Share passes it on, expired, unknown, a server error with Try again.backend/api-tests/app-links.api.spec.ts— by default the AASA claims exactly/authand/auth/*; booted withAPP_LINKS_CLAIM_MEMORIES=true,/m/*too.
Open questions
- Not yet seen on a device: the Story's photo timing, the postcard's capture, the card on a real camera roll, and a real link round trip to S3. Needs an on-device check before release — including opening an uploaded JPEG and confirming it carries no GPS EXIF (the re-encode is expected to drop it; not verified on a phone).