Skip to content

Multiple seniors on one device

A concept sketch, not a finished feature. It answers one question: what does the app look like when it holds more than one person's memories?

Today the answer is a radio group in Settings. Pick Tom, Nyna, or Thomas, and the photos, videos, and quizzes all change to that person's. Tom and Nyna carry bundled demo content; Thomas carries none, so everything he plays comes from his memory bank, which is the real-account test case. Nothing authenticates, no Loved One can sign in, and both people live on the same device with no separation beyond the switch. That is the point of a sketch: the shape is real, the plumbing is not.

The finished version of this is Accounts and roles, where a senior's content lives in a memory bank in D1 and Loved Ones reach it through a membership. This page uses the same ids and the same vocabulary, so the sketch can be replaced piece by piece rather than thrown away.

Try it

  1. pnpm launch sim
  2. Open Settings. The first section is Who is using the app.
  3. Switch between Tom and Nyna.
  4. Go back and open Video. The clips are different.

The id is the whole design

One number ties four things together:

Where What it looks like
apps/mobile/assets/1/ The folder holding that senior's files
src/features/seniors/roster.ts { id: '1', name: 'Tom', lovedOnes: [...] }
src/features/media/catalog.ts ownerId: '1' on each asset
src/features/video-quiz/questions.ts seniorId: '1' on each quiz

Pick a senior and every one of those filters down to the same person. Nothing else in the app needs to know a senior exists.

apps/mobile/assets/
  1/                    Tom
    photos/
    videos/             christmas-tie.mp4, oscars.mp4
  2/                    Nyna
    photos/
    videos/             viking-cruise.mp4

Files do not register themselves

Dropping a file into assets/2/videos/ is not enough to make it appear.

Metro resolves require() at build time, so the path has to be a literal written in the source. There is no way to build one from a senior id at runtime:

// Works. The bundler can see the path.
source: require('@/assets/2/videos/viking-cruise.mp4'),

// Never works, at any point, in any form.
source: require(`@/assets/${seniorId}/videos/${filename}`),

So every file is registered by hand in src/features/media/catalog.ts with an ownerId. The per-senior folders each carry a README with the steps, and Media catalog has the full flow.

This is a real limit of bundled assets, not a shortcut taken here. Content that arrives after the build (a photo a Loved One uploads) comes down from the memory bank at runtime instead, which is what src/features/bank/ already does for authored questions and photos.

The people

Senior Id Loved Ones
Tom 1 Lisa (daughter), Marcus (son)
Nyna 2 Lisa (daughter)

Lisa appears under both on purpose. A Loved One curating two banks is the case that makes role-based access insufficient and a membership table necessary, and it is worth being able to see it before building it.

The roster is display data. lovedOnes is rendered as a caption under the switch and nothing more: there is no Loved One sign-in on the phone, and the web app in apps/web is where that work belongs.

Adding a third person

  1. Create apps/mobile/assets/3/photos/ and apps/mobile/assets/3/videos/.
  2. Add them to seniors in src/features/seniors/roster.ts. The Settings radios come from that array, so the third button appears with no UI change.
  3. Register their files in src/features/media/catalog.ts with ownerId: '3'.
  4. Add quizzes in src/features/video-quiz/questions.ts with seniorId: '3'.

Step 2 alone is enough to see the person in Settings. With no media, the Video screen shows "No videos yet" rather than falling back to someone else's clips, which is the behavior worth keeping: an empty screen is correct, and another family's Christmas is not.

What the tests hold in place

  • tests/seniors-roster.test.ts: every asset has an owner in the roster, the per-senior views partition the catalog with nothing left over and nothing double-counted, and neither senior can reach the other's media.
  • tests/media-catalog.test.ts: a quiz's seniorId always matches the ownerId of the clip it plays. That duplication is only safe while it agrees, so it is checked rather than trusted.
  • tests/active-senior.test.tsx: the switch persists, survives a restart, and falls back to the default when the stored id is from a build with a different roster.
  • tests/settings-senior-switch.test.tsx: the radios are reachable by role and label, and carry hints.
  • tests/video-quiz-senior.test.tsx: the screen plays the active senior's clips and never the other senior's.

The memory bank follows the switch

The switch selects a session, not just bundled content. In development, EXPO_PUBLIC_DEV_AUTO_LOGIN maps each roster id to that senior's seeded device token:

EXPO_PUBLIC_DEV_AUTO_LOGIN=1:dev-tom-token,2:dev-nyna-token,3:dev-thomas-token

getDevAutoLoginToken(seniorId) in src/features/bank/api.ts resolves the active senior's token, so Stories fetches from that senior's bank (pnpm seed:api creates one bank per roster senior), and Settings > Manage Quiz Questions manages the same bank. A bare token with no id is read as the default senior's, which is what the old single-token format meant; a senior without an entry plays bundled content only.

Two guards keep the banks from bleeding into each other:

  • The offline cache in src/features/bank/cache.ts is keyed per senior (bank-questions-v2:<id>), so one person's cached bank content can never replay as another's after a switch.
  • The pool in use-story-pool.ts remembers whose it is, so a stored senior id resolving after mount reads as still loading rather than flashing another life's questions.

What this is not

  • Not access control. Anyone holding the phone can switch senior. Real separation needs the auth in Accounts and roles.
  • Not multi-device. The choice is stored in AsyncStorage on this device.
  • Not Loved One accounts. The names under the switch are text. Nobody signs in, and nothing is fetched for them.