---
path: /profile/data
surface: customer
domain: account
auth: signed-in
source: apps/oshun/web/src/app/profile/data/page.tsx
status: walked
last_walked:
  '2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data
  (2xx), console/page-errors, expected content, screenshot verified; live
  screen-reader, touch, offline, and telemetry-delivery checks pending a manual
  AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md'
last_verified:
  '2026-06-24 Playwright profile-data-readback.spec.ts — authenticated empty
  readback, real-BFF export/deletion snapshot rendering, and handoff to
  /profile?path=data-rights controls.; 2026-06-25 addendum — data-rights consent
  read moved from domain-stub fixture to the real consent-store projection, plus
  anonymous gate, capped export history, download aria label, and mobile 44px
  control verification.; 2026-06-27 addendum — real service-worker offline
  replay of the server-rendered /profile/data document with live BFF export and
  deletion rows kept visible.; 2026-06-29 addendum — standalone PWA mobile
  launch verifies display-mode, live BFF export/deletion rows, 44px controls,
  and no horizontal overflow.; 2026-06-29 telemetry addendum — the completed
  export Download link and all four footer navigation controls are asserted to
  expose no data-telemetry/data-analytics hooks.'
---

# Profile · Data rights

## Purpose

The privacy/DSAR surface — "the levers, in plain language." Renders three
read-only sections sourced from the real privacy BFF routes: which consents the
user has granted (`/v1/data-rights/consent`, a projection of
`customerConsentStateStore`), prior data exports (with download links), and any
deletion request in flight. The page explicitly states the actual
export/deletion mutations live behind two BFF POST endpoints
(`/v1/data-rights/exports`, `/v1/data-rights/deletions`) — this page is the read
surface; the write controls ship on `/profile?path=data-rights`.

## Entry points

- Cross-domain footer button from `/profile/notifications` ("Data rights")
- Cross-domain footer button from `/profile/persona` ("Data rights")
- "Back to profile" / sub-route navigation from `/profile`
- Direct URL / bookmark — yes; auth required (`proxy.ts` redirects no-session to
  `/welcome`)

## Layout regions

`page.tsx` is an async server component wrapped in
`<LWebShell top={<LCustomerNav active="library" />}>`, constrained to
`maxWidth: 880`. Three `Section` components stack below the masthead.

- **Header**: `LCustomerNav` (Lilith customer nav, `active="library"`; Lilith
  wordmark, Today/Explore/Activity/Library tabs, presentational ⌕ Search / ⌘K /
  Ask Lilith + avatar)
- **Masthead**: `LMasthead` — left `Profile · data`, right `rights`, kicker "The
  levers, in plain language", italic title "Your _data rights_."
- **Intro**: `LEyebrow` "How this opens" + "Three sections, no surprises."
- **Main**: three `Section`s
  (`data-profile-data-section="consents" | "exports" | "deletions"`) —
  "Consents", "Exports", "Deletion requests"
- **Footer / actions**: a row of four `LBtn`s — "Open data request controls"
  (primary, `/profile?path=data-rights`), "← Back to profile" (ghost),
  "Notifications" (ghost), "Open Memory controls" (ghost). Each footer action is
  forced to a 44 px high tap target on this route.

## States

- [x] **Loading** — server `Promise.all` of three `bffGet` calls blocks render;
      no client skeleton from this page
- [x] **Empty (consents fallback)** — `consents.length === 0` → dashed
      `EmptyRow` (`data-profile-data-empty="consents"`) "No consent state
      available right now." In healthy real dev infra this does not appear
      because the consent-store projection seeds three rows.
- [x] **Empty (exports)** — no export requests → `EmptyRow` "You have never
      requested an export. Open the data request controls to start one."
      (`data-profile-data-empty="exports"`)
- [x] **Empty (deletions)** — no pending requests → `EmptyRow`
      (`data-profile-data-empty="deletions"`) "Nothing in flight. Deletion
      happens with a thirty-day quiet window."
- [x] **Populated (short)** — real BFF consent rows, one completed export row,
      and one scheduled deletion row render in the stacked grid
- [x] **Populated (long)** — export history is capped by the BFF at 50 rows per
      user (`MAX_REQUESTS_PER_USER`); Playwright seeds 52 completed exports,
      verifies 50 render, verifies the newest and oldest-kept rows remain, and
      verifies the two evicted FIFO rows are absent. No pagination or
      virtualization is rendered.
- [x] **Export ready vs in progress** — `status === 'completed'` → "Ready —
      <bytes>" + "Download →" link when `downloadUrl` exists; pending /
      in-progress states show "In progress — OSHUN will write when it lands."
- [x] **Scheduled deletion** — accent-bordered row with requested date,
      scheduled effective date, scope/category count, grace period, and
      cancellation handoff to the data request controls
- [x] **Error (recoverable)** — N/A: `bffGet` returns `null` on any failure;
      page falls back to empty arrays (quiet empty, no retry UI)
- [x] **Offline** — the real service worker treats `/profile/data` as an exact
      shell-navigation document, warms the last server-rendered HTML while
      online, and replays that document while `navigator.onLine === false`; the
      completed export and scheduled deletion rows remain visible without the
      generic offline-fallback card
- [x] **Gated / anonymous** — `proxy.ts` redirects to `/welcome` before render
- [x] **Standalone PWA** — 390 px standalone launch renders the live BFF-backed
      read surface cleanly with display-mode asserted, consent/export/deletion
      rows visible, 44 px controls, and no horizontal overflow

## Interactions

This page renders no write controls (consents/exports/deletions are all static
reads). The only interactive elements are the per-export download link and the
footer navigation buttons.

- [x] **Open data request controls** (LBtn primary, href
      `/profile?path=data-rights`)
  - Function: navigates to the canonical export/deletion write surface in
    `ProfileSettingsPanel`
  - Keyboard: in document order before the footer sibling links; Enter activates
  - Screen reader: announces button text; rendered as an `<a>` (LBtn with href)
- [x] **Download →** (anchor, per completed export row)
  - Function: `<a href={row.downloadUrl} download>` — downloads the export
    artifact; only rendered when `status === 'completed'` and `downloadUrl`
    exists
  - Keyboard: in document order within its export row; Enter activates
  - Screen reader: `aria-label="Download export requested <date>"`
  - Touch target: ≥ 44 px tap height on mobile route coverage
  - Mobile (≤ 640 px): stacks within the export card
  - Offline behavior: download fails if `downloadUrl` is remote and offline
  - Telemetry: none wired (verify)
- [x] **← Back to profile** (LBtn ghost, href `/profile`)
  - Function: navigates to the account hub
  - Screen reader: announces button text; rendered as an `<a>` (LBtn with href)
- [x] **Notifications** (LBtn ghost, href `/profile/notifications`)
  - Function: navigates to channel/cadence prefs
- [x] **Open Memory controls** (LBtn ghost, href `/profile/memory`)
  - Function: navigates to the Memory controls surface

## Data & contracts

- **Reads** (all via `bffGet`, server-side, session cookie forwarded):
  - `/v1/data-rights/consent` (`apps/oshun/bff/src/routes/consent.ts`, projected
    from `customerConsentStateStore`) →
    `ConsentResponse { generatedAt, consents: [{ id, label, scope: 'analytics'|'research'|'third-party-models', state: 'opted-in'|'opted-out', updatedAtIso }] }`
  - `/v1/data-rights/exports` →
    `OshunCustomerDataExportSnapshotResponse { generatedAt, userId, revision, availableCategories, availableFormats, requests: [{ exportId, format, includeMetadata, categories, artifactTypes, status, requestedAt, startedAt, completedAt, cancelledAt, expiresAt, downloadUrl, bundleSizeBytes, integrityManifest, consentContext, failureReason }] }`
  - `/v1/data-rights/deletions` →
    `OshunCustomerDataDeletionSnapshotResponse { generatedAt, userId, revision, gracePeriodDays, validConfirmationTokens, availableCategories, requests: [{ deletionId, mode, status, scope, categories, artifacts, derivedArtifactJobs, tombstones, reason, confirmationToken, requestedAt, scheduledFor, cancelDeadlineAt, completedAt, cancelledAt, failureReason }] }`
    — page filters to `status === 'scheduled'`
- **Writes**: none from this page. Per the file header, mutations live behind
  POST `/v1/data-rights/exports` and POST `/v1/data-rights/deletions` (not
  invoked here)
- **Realtime**: none
- **Caching**: `bffGet` default `next.revalidate: 60` s; SW caches the rendered
  `/profile/data` document after an online visit and replays that same document
  while offline. Auth/session and non-Nisaba `/v1/*` reads remain network-only;
  this route's offline support is document replay, not API response caching.
- **Auth/role check**: `proxy.ts` protected-by-default; no-session → `/welcome`
- **Metadata**: `title: 'Profile · data rights'`, description re consent /
  export / deletion levers, `alternates.canonical: '/profile/data'`

## Cross-references

- Feature spec: [`V1/features.md#account`](../../../V1/features.md#account)
- Architecture:
  [`V1/ARCHITECTURE.md#account`](../../../V1/ARCHITECTURE.md#account)
- Journeys:
  [`../../journeys/account-deletion-and-dsar.md`](../../journeys/account-deletion-and-dsar.md)
- Sibling routes:
  - [`profile.md`](./profile.md), [`profile-memory.md`](./profile-memory.md),
    [`profile-safety.md`](./profile-safety.md),
    [`profile-notifications.md`](./profile-notifications.md)
- Component sources:
  - `apps/oshun/web/src/app/profile/data/page.tsx`
  - `apps/oshun/web/src/design-system/lilith/shells.tsx` (`LWebShell`,
    `LCustomerNav`, `LMasthead`)
  - `apps/oshun/web/src/lib/server/bff-fetch.ts` (`bffGet`)

## E2E coverage

- [`apps/oshun/web/e2e/profile-data-readback.spec.ts`](../../../apps/oshun/web/e2e/profile-data-readback.spec.ts)
  — authenticates a customer, verifies the real consent-store projection
  (including a research consent grant reflected on the page), verifies empty
  export/deletion readback and the `/profile?path=data-rights` handoff, seeds a
  completed export and scheduled deletion through real BFF POSTs, verifies the
  server-rendered snapshots with status hooks/truncated manifest hash/download
  link/category count/grace-period copy, proves anonymous redirect gating, seeds
  52 completed exports to verify the real 50-row history cap, checks mobile 44
  px touch targets/no horizontal overflow with the shared axe gate, and asserts
  the completed export download link plus all four footer navigation controls
  expose no `data-telemetry-event`, `data-analytics-event`, or `data-event`
  hooks. The spec also runs a Chromium real-service-worker path: it warms
  `/profile/data` online from live BFF export/deletion rows, verifies the
  document is cached under the real SW, switches the browser offline, reloads
  `/profile/data` from the SW cache, and asserts `navigator.onLine === false`,
  the same rows remain visible, and the generic offline-fallback document is not
  used. A separate mobile standalone PWA launch asserts
  `(display-mode: standalone)`, live BFF-backed consent/export/deletion rows, 44
  px controls, and no horizontal overflow.
- [`apps/oshun/bff/src/__tests__/consent-route.test.ts`](../../../apps/oshun/bff/src/__tests__/consent-route.test.ts)
  — verifies `/v1/data-rights/consent` is the authenticated projection of the
  canonical consent store and reflects a real `research_study` grant.

## Open questions / known gaps

- [x] No telemetry on the Download link or footer navigation: asserted in
      `profile-data-readback.spec.ts` against the completed export download link
      and all four footer action links.
- [x] Offline/PWA replay for this server-rendered account sub-route is covered
      by a dedicated real-service-worker run in `profile-data-readback.spec.ts`.
