Background
nostter currently handles bookmarks using the deprecated NIP-51 format:
kind: 30001
d: "bookmark"
The current NIP-51 specification defines:
kind: 10003 as the standard uncategorized/global bookmark list
kind: 30003 as user-defined categorized bookmark sets
kind: 30001 / d: "bookmark" as a deprecated bookmark list format
Automatically migrating the legacy list to kind: 10003 would make those bookmarks unavailable to clients that still read only the legacy format.
Instead, nostter should support the legacy and current formats side by side and let the user migrate bookmarks explicitly through a bookmark list management UI.
Goal
Introduce bookmark list management that can handle:
- the standard
kind: 10003 bookmark list
- the legacy
kind: 30001 / d: "bookmark" bookmark list
- multiple
kind: 30003 bookmark sets
Bookmarks should be movable between these lists. Moving bookmarks out of the legacy list serves as the explicit migration mechanism rather than performing an automatic migration.
This also supersedes #1285, which tracks categorized bookmarks.
Bookmark lists
Treat the following as separate lists and do not merge their contents automatically.
Bookmarks
The standard global bookmark list:
kind: 10003
- one replaceable event per user
New bookmarks created through the normal bookmark action should use this list.
Legacy bookmarks
The deprecated bookmark list:
kind: 30001
d: "bookmark"
Continue reading and displaying this list so bookmarks remain accessible while users still use clients based on the old format.
Do not automatically copy or migrate its contents to kind: 10003.
nostter should not add new bookmarks to this list except when updating it as the source of an explicit move operation.
Bookmark sets
User-defined categorized bookmark lists:
kind: 30003
- one addressable event per
d identifier
Support multiple bookmark sets.
Use NIP-51 set metadata where available:
d
title
image
description
The d value identifies the set and should not be derived from a mutable display title.
List management UI
Update the bookmarks UI to expose the different lists separately.
For the logged-in user, provide management operations such as:
- view the standard bookmark list
- view the legacy bookmark list when present
- view existing bookmark sets
- create a bookmark set
- edit bookmark set metadata
- move bookmarks between lists
The legacy list should be clearly identified as a deprecated format.
Other users' bookmark pages should remain read-only and display only information that can be read publicly.
Moving bookmarks
Implement moving as a common list operation rather than as a special legacy migration path.
The same operation should support moves such as:
- legacy
30001 → standard 10003
- legacy
30001 → bookmark set 30003
- standard
10003 → bookmark set 30003
- bookmark set
30003 → standard 10003
- bookmark set
30003 → another bookmark set 30003
A bulk move from the legacy list to the standard bookmark list may be provided as an explicit migration action.
Do not automatically union lists when the same bookmark appears in multiple lists.
Publish order
A move requires updating two replaceable/addressable events and cannot be atomic across relays.
To avoid losing a bookmark:
- Add the bookmark to the destination list and publish it.
- Confirm that the destination update succeeded.
- Remove the bookmark from the source list and publish it.
If the source update fails after the destination succeeds, temporary duplication is preferable to losing the bookmark.
If the destination already contains the bookmark, skip adding a duplicate and proceed with removing it from the source.
Public and private bookmarks
Preserve whether a bookmark is public or private when moving it.
NIP-51 stores:
- public items in event tags
- private items as encrypted tags in
content
Moving a private bookmark therefore requires decrypting the source list and updating/re-encrypting the destination list rather than copying the encrypted content as an opaque value.
Continue supporting existing NIP-04-encrypted list content when reading legacy data, while using the current NIP-44 format for newly created current-format list content.
Do not discard unsupported or unrelated tags when rewriting a list.
Data model
The current bookmark implementation assumes a single bookmark event.
Refactor it so nostter can keep the following states simultaneously:
- optional
kind: 10003
- optional legacy
kind: 30001 / d: "bookmark"
- zero or more
kind: 30003 bookmark sets
The effective state of one list must not overwrite another list simply because an event arrives later from a relay.
The bookmark indicator on a note should consider membership across the supported bookmark lists rather than only the current single bookmark event.
Fetching and caching
Add kind: 10003 to normal replaceable-event handling.
Continue fetching the legacy kind: 30001 / d: "bookmark" event for compatibility.
Fetch all kind: 30003 bookmark sets belonging to the relevant user. Since their d identifiers are not known in advance, do not model them only through the existing fixed-identifier author event configuration.
Ensure cached state can represent multiple bookmark sets without losing sets whose identifiers were not known at application startup.
Concurrency
Before modifying a list, validate that the local source and destination events have not become stale compared with relay state.
For a move involving two lists, avoid overwriting a newer destination or removing an item from an outdated source snapshot.
Reuse the existing bookmark update serialization where appropriate, but extend it to account for operations involving multiple lists.
Out of scope
This issue does not require making every NIP-51-compatible event type bookmarkable from the nostter UI.
Existing bookmarkable content support may remain unchanged, but list updates must preserve NIP-51-compatible tags that nostter does not currently render or edit.
Acceptance criteria
kind: 10003 bookmarks are read and displayed.
- Legacy
kind: 30001 / d: "bookmark" bookmarks continue to be read and displayed separately.
- Legacy bookmarks are not automatically migrated.
- Existing
kind: 30003 bookmark sets are discovered and displayed.
- Users can create and manage bookmark sets.
- New bookmarks use
kind: 10003 by default.
- Users can explicitly move bookmarks between supported lists.
- Moving a bookmark from the legacy list provides a manual migration path.
- Destination updates are published before source removals.
- A failed move cannot silently lose a bookmark.
- Public/private status is preserved when moving bookmarks.
- Unsupported list tags are not discarded when nostter rewrites an event.
- Receiving or updating one bookmark list does not overwrite the state of another.
- Existing clients that still depend on the legacy bookmark event remain usable until the user explicitly moves bookmarks away from that list.
Related issues
References
Background
nostter currently handles bookmarks using the deprecated NIP-51 format:
kind: 30001d: "bookmark"The current NIP-51 specification defines:
kind: 10003as the standard uncategorized/global bookmark listkind: 30003as user-defined categorized bookmark setskind: 30001/d: "bookmark"as a deprecated bookmark list formatAutomatically migrating the legacy list to
kind: 10003would make those bookmarks unavailable to clients that still read only the legacy format.Instead, nostter should support the legacy and current formats side by side and let the user migrate bookmarks explicitly through a bookmark list management UI.
Goal
Introduce bookmark list management that can handle:
kind: 10003bookmark listkind: 30001/d: "bookmark"bookmark listkind: 30003bookmark setsBookmarks should be movable between these lists. Moving bookmarks out of the legacy list serves as the explicit migration mechanism rather than performing an automatic migration.
This also supersedes #1285, which tracks categorized bookmarks.
Bookmark lists
Treat the following as separate lists and do not merge their contents automatically.
Bookmarks
The standard global bookmark list:
kind: 10003New bookmarks created through the normal bookmark action should use this list.
Legacy bookmarks
The deprecated bookmark list:
kind: 30001d: "bookmark"Continue reading and displaying this list so bookmarks remain accessible while users still use clients based on the old format.
Do not automatically copy or migrate its contents to
kind: 10003.nostter should not add new bookmarks to this list except when updating it as the source of an explicit move operation.
Bookmark sets
User-defined categorized bookmark lists:
kind: 30003didentifierSupport multiple bookmark sets.
Use NIP-51 set metadata where available:
dtitleimagedescriptionThe
dvalue identifies the set and should not be derived from a mutable display title.List management UI
Update the bookmarks UI to expose the different lists separately.
For the logged-in user, provide management operations such as:
The legacy list should be clearly identified as a deprecated format.
Other users' bookmark pages should remain read-only and display only information that can be read publicly.
Moving bookmarks
Implement moving as a common list operation rather than as a special legacy migration path.
The same operation should support moves such as:
30001→ standard1000330001→ bookmark set3000310003→ bookmark set3000330003→ standard1000330003→ another bookmark set30003A bulk move from the legacy list to the standard bookmark list may be provided as an explicit migration action.
Do not automatically union lists when the same bookmark appears in multiple lists.
Publish order
A move requires updating two replaceable/addressable events and cannot be atomic across relays.
To avoid losing a bookmark:
If the source update fails after the destination succeeds, temporary duplication is preferable to losing the bookmark.
If the destination already contains the bookmark, skip adding a duplicate and proceed with removing it from the source.
Public and private bookmarks
Preserve whether a bookmark is public or private when moving it.
NIP-51 stores:
contentMoving a private bookmark therefore requires decrypting the source list and updating/re-encrypting the destination list rather than copying the encrypted
contentas an opaque value.Continue supporting existing NIP-04-encrypted list content when reading legacy data, while using the current NIP-44 format for newly created current-format list content.
Do not discard unsupported or unrelated tags when rewriting a list.
Data model
The current bookmark implementation assumes a single bookmark event.
Refactor it so nostter can keep the following states simultaneously:
kind: 10003kind: 30001/d: "bookmark"kind: 30003bookmark setsThe effective state of one list must not overwrite another list simply because an event arrives later from a relay.
The bookmark indicator on a note should consider membership across the supported bookmark lists rather than only the current single bookmark event.
Fetching and caching
Add
kind: 10003to normal replaceable-event handling.Continue fetching the legacy
kind: 30001/d: "bookmark"event for compatibility.Fetch all
kind: 30003bookmark sets belonging to the relevant user. Since theirdidentifiers are not known in advance, do not model them only through the existing fixed-identifier author event configuration.Ensure cached state can represent multiple bookmark sets without losing sets whose identifiers were not known at application startup.
Concurrency
Before modifying a list, validate that the local source and destination events have not become stale compared with relay state.
For a move involving two lists, avoid overwriting a newer destination or removing an item from an outdated source snapshot.
Reuse the existing bookmark update serialization where appropriate, but extend it to account for operations involving multiple lists.
Out of scope
This issue does not require making every NIP-51-compatible event type bookmarkable from the nostter UI.
Existing bookmarkable content support may remain unchanged, but list updates must preserve NIP-51-compatible tags that nostter does not currently render or edit.
Acceptance criteria
kind: 10003bookmarks are read and displayed.kind: 30001/d: "bookmark"bookmarks continue to be read and displayed separately.kind: 30003bookmark sets are discovered and displayed.kind: 10003by default.Related issues
References
https://github.com/nostr-protocol/nips/blob/master/51.md
https://github.com/SnowCait/nostter/blob/main/web/src/lib/author/Bookmark.ts
https://github.com/SnowCait/nostter/tree/main/web/src/routes/%28app%29/%5Bslug=npub%5D/bookmarks
https://github.com/SnowCait/nostter/blob/main/web/src/lib/List.ts
https://github.com/SnowCait/nostter/blob/main/web/src/lib/Author.ts
https://github.com/SnowCait/nostter/blob/main/web/src/lib/Constants.ts