Skip to content

The Activity Layer: workspace feed + home teaser - #310

Open
an1va wants to merge 2 commits into
mainfrom
feat/activity-layer
Open

The Activity Layer: workspace feed + home teaser#310
an1va wants to merge 2 commits into
mainfrom
feat/activity-layer

Conversation

@an1va

@an1va an1va commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Persists a new activity table (denormalized like notification, mirrored across SQLite/D1/Postgres) fed from 6 existing action routes: publish (×2 sites), comment create/resolve, proposal approve/request-changes, share, and a named viewer's first read of a version.
  • Gated to org/link/public visibility — an unlisted draft or private doc never leaks its existence into the shared feed.
  • New GET /v1/activity (cursor-paginated, same keyset shape as listArtifacts) and a new /activity page: day-grouped, coalescing consecutive same-actor/same-artifact/same-kind rows into one story ("published 2 revisions of Q3 roadmap · v2 → v3", not two rows).
  • Home gets one quiet link, not a module: a TriageBar-style line showing the single latest story with a "View" link to the full feed — sits below the existing feedback triage line, disappears entirely when nothing's happened.

Follows the design in "The Activity Layer: making Derive feel alive" and its mockups — this PR is stages 1+2 (spine + feed) plus the requested home link. Following-lens, proposals-in-the-bell, and the weekly digest are named follow-ups, not in scope here.

Walkthrough with real screenshots: https://derive.to/artifacts/the-activity-layer-shipped-walkthrough-iorihh89. (Images currently 404 on Chrome — unrelated platform bug found while testing this, fixed separately in #313.)

Test plan

  • pnpm run ci green (biome + design-tokens + frontend + testids + api + schema + hyperdrive + filesize + anchor-client + deadcode)
  • pnpm typecheck green across all 8 workspace packages
  • pnpm test — 683 pre-existing tests pass, 0 regressions
  • Manually verified end-to-end: signed up, published an artifact, set it to workspace visibility, commented, republished twice — confirmed the feed coalesces the two republishes into one "2 revisions" story and the home teaser links to it
  • Following-lens / proposal-into-bell / digest — deliberately out of scope, tracked as follow-ups

🤖 Generated with Claude Code


Self-review before merge

Ran an 8-angle review (correctness, removed-behavior, cross-file, reuse, simplification, efficiency, altitude, conventions) and verified the top findings directly against the code and a real ephemeral Postgres. Two real bugs fixed as a follow-up commit:

  • deleteArtifact never cleaned up the new activity table. All three dialect implementations were missing it — on Postgres this threw a hard FK-violation and aborted the delete of any artifact that ever had activity recorded; on SQLite/D1 it silently orphaned rows. Fixed in all three (packages/db/src/repos.ts, sqlite.ts, pg.ts), verified against a real Postgres container.
  • A visibility downgrade didn't retroactively hide already-recorded activity. The write-time gate stopped new rows for a private/unlisted/password artifact, but nothing re-checked an artifact's current visibility on read, so a doc downgraded after the fact kept leaking its title + comment previews to the whole workspace forever. Fixed by making listActivity join against artifact and filter on live visibility, sourced from one new shared constant (FEED_VISIBLE_TIERS) so the write and read gates can't drift apart.
  • Also: republishing with the resolves field (bundling thread resolution into a republish) wasn't recording a "resolve" activity, unlike the dedicated resolve endpoint doing the same state change.
  • Added test coverage for GET /v1/activity (there was none): auth gate, both visibility gates, deletion, resolves-via-republish, cursor pagination — run against both embedded SQLite and a real Postgres container.

Two other findings were investigated and intentionally left as-is: activityActor returning null for the token principal matches actingUser/principalActor's existing behavior, and the ?limit=0 falsy-zero coercion is the same pattern already used verbatim in the established GET /v1/artifacts handler — fixing either in isolation would diverge from convention rather than fix a bug unique to this PR.

Persists a new activity table (denormalized like notification, mirrored
across SQLite/D1/Postgres) fed from the existing action routes: publish,
comment create/resolve, proposal approve/request-changes, share, and a
named viewer's first read of a version. Gated to org/link/public
visibility so unlisted drafts and private docs never leak into the feed.

Adds GET /v1/activity (cursor-paginated) and a new /activity page that
day-groups and coalesces consecutive same-actor/same-artifact/same-kind
rows into one story. Home gets one quiet TriageBar-style link to the
latest story, not a whole module — disappears when nothing's happened.

Follows the design in the "Activity Layer" concepts doc + mockups
published earlier on Derive.
@an1va
an1va force-pushed the feat/activity-layer branch from 3633114 to a634ea6 Compare July 7, 2026 18:46
… don't hide it

Found via a self-review before merge (8-angle code review, verified
against the actual code and against a real ephemeral Postgres, not just
sqlite):

- deleteArtifact never deleted from the new `activity` table before
  deleting the artifact row, despite `activity.artifact_id` being a
  real FK. All THREE dialect implementations were missing it
  (packages/db/src/repos.ts for D1, sqlite.ts's own transactional
  override, and pg.ts) — on Postgres this threw a hard FK-violation
  and aborted every delete of an artifact that ever had activity
  recorded; on SQLite/D1 (no FK enforcement) it silently orphaned rows.

- recordActivity's visibility gate only stopped NEW rows from being
  written for a private/unlisted/password artifact; nothing re-checked
  an artifact's CURRENT visibility on read. Downgrading a doc from
  org/link/public to private after the fact left its title and comment
  previews visible in the whole workspace's feed indefinitely,
  contradicting the code's own stated invariant. Fixed by making
  listActivity join against `artifact` and filter on its live
  visibility (packages/core/src/ports.ts's new FEED_VISIBLE_TIERS is
  the single shared source both the write-time gate and this read-time
  filter use, so the two can't drift independently).

- Republishing with the `resolves` field (thread resolution bundled
  into a republish) never recorded a "resolve" activity, unlike the
  dedicated resolve endpoint doing the identical state change.

Adds test coverage for the new /v1/activity route (auth gate, the
visibility write/read gates, deletion, resolves-via-republish, cursor
pagination) — there was none before. Verified against both embedded
SQLite and a real ephemeral Postgres container (pnpm test:pg).

Two other review findings were investigated and NOT changed, since
they match pre-existing codebase conventions rather than being novel
to this PR: activityActor returning null for the token principal
mirrors actingUser/principalActor's existing behavior, and the
`Number(limit) || 30` pattern on ?limit=0 is copied verbatim from the
established GET /v1/artifacts handler.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant