Publishing one page of your own story
How the optional /<handle> snapshot works end to end — the ten-stat pack built in your browser, per-stat toggles, private indistinguishable from missing, curation with one approved redacted line, fixed titles, hard-delete withdraw, and why another person's data cannot appear on it.
Every stat tool eventually asks the same thing: share your results. Usually that
means handing a platform your numbers and letting it dress them up. This is the
other design: an optional page at /<yourname> where you publish a small,
encrypted, owner-built summary of your own export — ten numbers you choose from,
moments you approve one line at a time, and a switch that makes the whole page
indistinguishable from a page that never existed.
The premise worth stating first: this page is not a feature you enable, it is a
thing you create — nothing exists until an archive is loaded, a username is
claimed and a pack is built in your own browser.1 If the claim layer is
unconfigured on the deployment, the creation path answers with the same honest
notice as the rest of the account features, and every local feature keeps
working.2 This post walks the pipeline: pack, publication, curation,
withdrawal — and the structural reason another person's data cannot ride along.
The published page shows only your own derived aggregates: a ten-key pack
(years present, best year, posts, reels, stories, comments left, messages sent,
likes given, followers, following) computed in your browser from your loaded
archive, AES-256-GCM encrypted at rest, and — critically — projected onto your
per-stat toggles on the server before it ever reaches a visitor: fields you hid
are not in the response at all, not merely CSS-hidden.3 The page defaults
to showing all ten; flipping visibility to private renders exactly like a page that
does not exist, because "private" and "missing" are deliberately indistinguishable
responses.4 Curation adds four kinds of moments (conversations, creators
you watched, milestones, network facts) — each private until you switch it
public, one optional excerpt per conversation capped at 280 characters and
redacted on your device before transmission ([someone], [link], [email],[number]), with public titles drawn from a fixed map — the schema has no
participant or thread-title column for a name to live in.5 Withdrawal is a
hard delete, per moment or everything at once.6 And the reason no one
else's story can appear: every write is session-scoped to your user id, the
candidate shortlist is built in your browser and never leaves it on its own, and
the pack builder counts only archive.personal.username's own
account.7
- 2minthe pack: ten keys, built locally, projected server-side.
- 4mincuration: four moment kinds, the one approved line, fixed titles.
- 5minwho can write what, what withdraw means, and the limits worth knowing.
The pack: ten numbers, built where the archive already is
Registration does not upload your archive — it uploads a summary. The builder
runs in your browser against the archive you already loaded, counts only your own
account's records, and produces the pack type with its validation contract:1
| Key | Meaning | Label on the page |
|---|---|---|
| `years` | Calendar years the account was active | Years present |
| `bestYear` | Year with the most content shared | Best year |
| `posts` / `reels` / `stories` | Your own creations | Post / reel / story counts |
| `commentsPosted` | Comments you left on others' posts | Comments left |
| `messagesSent` | DMs **you** sent (not received) | Messages sent |
| `likesGiven` | Likes you gave | Likes given |
| `followers` / `following` | The two directional counts | Followers / Following |
Notice the ownership baked into the keys: messagesSent, not "messages"; comments
you left. The pack cannot contain a conversation, a name or an incoming message
— there is no key for one, and the schema for what ships is validated strictly on
the server (unknown fields rejected, counts non-negative integers, years matching^\d{4}$).8 One pack of the ten keys is the entire public payload of this
product.
Two switches shape what a visitor sees: visibility (public/private) and
showStats — all ten by default, any subset once you edit it.9 The
projection happens server-side in the snapshot read: the response object is
rebuilt key-by-key from your show-list, so a stat you turned off is absent from
the wire, and the empty-state line on your own page says so honestly — "Its owner
keeps this page private to the numbers".10 Features page phrasing: "Visitors
never know what's hidden" — because there is no row in the response to hint
with.11
The rendered page is deliberately warm rather than competitive: the hero carries
"a little nostalgic piece of an Instagram journey — reduced to the numbers worth
remembering", a badge stating it was derived from a real export (Meta's Download
Your Information), and a footer that spells the contract: names, messages and
identities never appear; any wording on a card was picked and checked by its
owner.12 The page's own metadata calls it "a warm memory of a journey, not a
leaderboard" — brand copy, but load-bearing: the layout has no rank, no comparison
and no third party to compare against.13
Curation: four moment kinds, one approved line
Stat cards are the fixed part; moments are the curated part, edited at /curate
in a picker that is itself an argument about privacy.14 The three-bullet
"truth" section at the top of that page:
- Always safe to publish: *"counts, dates, and spans. '1,204 messages, 2019 to
2021' needs no names attached."*
- Never published: *"anyone's name, a thread title, participants, photos,
captions, locations or links. The page shows the word 'A conversation' because
the alternative does not exist in our database."* - Only with your explicit approval: *"one line of wording from one message you
pick, and the handle of a public creator you watched. Both are scrubbed of
names, numbers, links and emails before they are stored, and you can withdraw
either at any time."*
The four kinds map to what the export can honestly support: Conversations
(counts and dates only unless you approve a line), Creators you watched (a
public account you approve by choosing the card), Milestones (aggregate facts
about your own years), Your network (headline counts — no list of who).15
The one approved line. Pick a conversation, and an optional excerpt picker
offers the exact sentences — "Publish one line of wording? (optional)" — with the
preview framed as "This is exactly what will be published", and the rule stated
underneath: "Names, numbers, links and emails are replaced automatically."16
The mechanics deserve the close read: redaction runs on your device before the
text is put on the wire, replacing with [someone], [link], [email] and[number]; the server re-runs the same redaction as defence in depth; the stored
excerpt is capped at 280 characters.17 The conversation itself, its title
and its participants are never stored — the picker's own line: "Pick a line to
publish one redacted sentence. The conversation itself is never stored."
Titles are not yours to invent — and that is the guarantee. A published
conversation is always titled "A conversation"; a creator card carries only a
handle you approved; milestone and network titles come from the same fixed
map.5 The schema comment is the design in one sentence: "there is
deliberately no partner, participant, or chat-title column… so the page cannot leak
a name." Free text exists in exactly two places — your optional 160-char caption
per moment and a 160-char headline under your handle — both scrubbed by the same
server-side sanitizer before storage.18 The only other person whose name can
ever be rendered is a public creator whose handle you typed into the approval.
Private until you say otherwise. Every moment row defaults to private; the
picker's two buttons per card are literally "Only me" / "On my page", and a
half-finished curation is safe to leave mid-edit because unpublished rows never
reach the public query — which triple-filters: profile public, moment public,
account not deleted.19
Who can write, and why nobody else can
The owner-only property is not a permission check sprinkled around — it is the
pipeline's shape:7
- The pack is built from your loaded archive, keyed by
archive.personal.username, in your browser. - It is written once at registration onto the row for your signed-in user id.
- Every later write (visibility, stats, design, moments) authenticates a session
and scopes its
WHEREclause to that same user id — the revoke path's comment:
"Scoped to this profile, so one owner can never delete another's card." - Sessions themselves require a verified email (the account gate from the email-
verification design), so an unproven account cannot hold write credentials.
- The candidate shortlist — the one place a conversation partner's name is
ever rendered — "is derived from the archive IN THIS BROWSER… and it never
leave[s] it": the public page can only ever receive the sanitized, approved
output.7
There is also no cross-user surface to graft onto: no follower graph (the
followers-only control doesn't exist, with the gap stated in the UI), no sitemap
entry for public profiles (inventing URLs from handles "would be both wrong and a
privacy problem"), and the analytics beacon rewrites your page's URL to the
literal /[handle] before any page-view leaves the device.20 The public
page's own footer states the boundary for visitors: the snapshot was created by
its owner from their own archive.
What exactly can a visitor see?
Your handle, the stat keys you left enabled, your optional headline, and the
moments you switched to public — each an aggregate, a date span, and at most one
redacted sentence plus your caption. No names, no thread titles, no photos, no
captions from your archive, no incoming messages, no lists of people. If you hide
everything, visitors see your handle and nothing else; if you flip the page
private, they see the same "Nothing here yet" a non-existent page shows.
Do I need an account to use LMKFR?
No — accounts are optional for the whole product; local analysis never requires
one. An account exists for exactly two optional things: this shareable snapshot
page and cross-device sync. If the deployment has its claim layer unconfigured,
the creation path shows the "not enabled on this deployment" notice and nothing
else changes.
Can I take the page down?
Instantly, in two scopes: Withdraw this moment hard-deletes a single card's row
immediately (no soft-delete window to leak from), and Withdraw everything clears
the curation in one action. Flipping visibility to private hides the whole page at
once, and account deletion erases the snapshot, the username and the session —
with the standard 30-day grace on the account record itself.
Could a message I approve quote someone else?
Only as an anonymous redaction: the excerpt picker masks names, numbers, links and
emails on your device before transmission, the server re-runs the mask, and the
stored line is capped at 280 characters. If the sentence's meaning would still
identify someone, don't approve it — the redactor replaces patterns, not context,
and the preview shows you the exact bytes that will be published before you save.
Is the page indexed or recommended anywhere?
No. Public profiles are deliberately absent from the sitemap, the route is
published for people you send the link to, and page-view analytics never receive
your handle (the URL is rewritten to /[handle] first). Treat it as a page you
share, not a page you promote.
Questions this comes up
The keeping-control post for erasure and export controls around this account; the
security-privacy post for the custody model behind the encryption claims; the
local-first post for what happens — and does not happen — when you never create
an account at all.
1: apps/web-next/src/lib/claimSnapshot.ts — buildNostalgicSnapshot(archive, …)
counts only the loaded archive's own-account records; its only callers are the
Sync view's registration path ("Analyze your archive first — the snapshot is
built from it").
2: views/Sync.tsx — claims-disabled notice: "Accounts & sync are not
enabled on this deployment… No account is needed for any feature"; all claim
write endpoints answer 503 "The claim layer is not configured on this deployment."
3: lib/claim/server.ts apiSnapshot — decrypts the pack, then rebuilds
the response by copying only the keys in the owner's show list; hidden keys are
absent from the response object.
4: server.ts — private profiles return null with the comment
"Private profiles render exactly like missing ones — no hint that they exist."
5: db/schema.ts — profile_moments comment: "there is deliberately no
partner, participant, or chat-title column"; packages/shared/src/curation/format.ts
— the fixed MOMENT_TITLES map ("A conversation", "A creator you watched", …).
6: lib/claim/moments.ts — apiRevokeMoment is a hard DELETE,
single or all; Features page: "there is no soft-delete window to leak from."
7: claimSnapshot.ts (owner-keyed build), server.ts (session-scoped
writes: eq(profiles.userId, user.id)), moments.ts ("one owner can never delete
another's card"; session requires emailVerifiedAt), views/Curator.tsx ("IN
THIS BROWSER… never leave it").
8: lib/claim/server.ts snapshotSchema — .strict(), literal v: 1,^\d{4}$ years (max 80), integer counts ≥ 0; sent with the register payload.
9: db/schema.ts profiles — visibility (default public),showStats (default 'all'), design (theme, layout, headline).
10: views/NostalgicSnapshotView.tsx — "Its owner keeps this page private
to the numbers — no stats are shared here."
11: views/Features.tsx — "You Control Every Number": hand-pick which
stats stay; "Visitors never know what's hidden."
12: NostalgicSnapshotView.tsx — hero subline, the derived-from-export badge,
and the footer contract ("Names, messages and identities never appear here — any
wording on a card was picked and checked by its owner").
13: (public)/[handle]/page.tsx — metadata description "A nostalgic memory
of a journey, not a leaderboard"; JSON-LD ProfilePage emitted the same way.
14: app/(app)/curate/page.tsx metadata and views/Curator.tsx — the
picker UI, the three-bullet truth section, scope buttons, excerpt picker,
headline/design controls.
15: Curator.tsx — the four kind labels/blurbs (Conversations, Creators you
watched, Milestones, Your network).
16: Curator.tsx — "Publish one line of wording? (optional)", the
"This is exactly what will be published" preview, and the
"Names, numbers, links and emails are replaced automatically" line.
17: packages/shared/src/curation/redact.ts — device-first redaction ("it
runs on the DEVICE, before the text is put on the wire. The server re-runs it as
defence in depth"), replacements [link]/[email]/[number]/[someone];curation/types.ts — MAX_EXCERPT_CHARS = 280.
18: Curator.tsx (160-char caption and headline fields) andcuration/sanitize.ts — server-side re-scrub including sanitizeDesign().
19: moments.ts public query — profile visibility = 'public' AND momentscope = 'public' AND deleted_at IS NULL; schema.ts scope default private.
20: views/Curator.tsx — the followers-only gap tip; app/sitemap.ts —
"PUBLIC PROFILES ARE DELIBERATELY ABSENT"; lib/analytics.ts — /[handle]
redaction before any page-view is sent.
Footnotes
- build
- offline
- project
- private
- titles
- withdraw
- owner
- schema
- toggles
- empty
- features
- view
- meta
- curator
- kinds
- excerpt
- redact
- scrub
- query
- surface