Files
Artstr-Studio/docs/LINKED_DESIGNS_FEATURE.md
T
thePR0M3TH3AN 87dd9426c2 Draft Linked Designs spec
Spec for the next big arc: reference-based cross-design imports.
Adds a `kind: 'linked'` flavor of the existing design layer that
stores only a NIP-01 addressable-event coordinate
(30078:<pubkey>:<dTag>) and resolves to the latest source on
render. Sits alongside the existing embedded import path; user
picks at import time.

Key decisions baked in (per scoping conversation):

- Versioning: always-latest by default, with an opt-in "Pin to
  this version" toggle exposed per linked layer.
- Nesting: up to 3 levels deep with cycle detection + visible
  depth-exceeded placeholders.
- Encrypted: Phase 1 ships public-only; Phase 2 adds premium
  (renders if viewer's unlocked) and private (share-list aware).
- Attribution chain is first-class — host publish emits one
  ['p', '<contributor pubkey>', '<relay-hint>', 'design-contributor']
  tag per distinct linked-tree creator, plus an in-app Credits
  panel surfacing co-creators. Lets larger collaborative projects
  carry honest attribution natively.

Architectural shape:

- Resolver IIFE (linkedDesigns) with session cache, multi-relay
  union (mirrors _vaultRelayUnion), and a manual Refresh button
  to bypass cache.
- renderDesignObjectIntoElement gets a linked branch that calls
  the resolver before the existing render path.
- Layer-options panel exposes pin / refresh / unlink / replace.
- "Publish + link" action converts an embedded layer to a freshly-
  published source + linked reference (Phase 3 polish).
- Convert-back-to-embed action snapshots the current resolution.

Phased implementation:

- Phase 1: Public-only linking, schema, resolver, rendering, UI.
- Phase 2: Premium + private link resolution with access control.
- Phase 3: Disk cache, attribution `p`-tag emission, Credits panel,
  Publish + link workflow.

Acceptance criterion for Phase 1: a public cover design linked into
a public slide deck renders correctly; editing the source updates
the host's render on next refresh. 10-link host event stays under
typical relay size limits.

Outstanding open questions to revisit during implementation:

- Whether the depth limit should be 3 forever or configurable.
- Whether the 256 KB per-source payload cap is the right number.
- Whether to surface "last resolved" times in the layer name itself
  or only on hover.
2026-05-31 16:38:26 -04:00

19 KiB
Raw Blame History

Artstr Studio Linked Designs

Cross-Reference Imports + Auto-Update + Attribution Chain

Status

Spec draft. Not yet implemented. This document is the canonical plan; when work begins it follows this doc, and updates land here first.


1. Feature Summary

Artstr currently supports embedded cross-template imports: any design can be pulled into another as a type: 'design' layer (or as a type: 'pixelart' layer, or as a deck slide, or into a disc-sheet slot). Every existing import is a deep JSON.parse(JSON.stringify(...)) clone — once placed, the new layer has no relationship to its source event. Source edits don't propagate; host-event size grows linearly with embedded content.

This spec adds a parallel linked flavor of design imports. A linked layer stores only the NIP-01 addressable-event coordinate (30078:<creatorPubkey>:<dTag>) of its source. The renderer fetches the latest event at that address on demand and renders it in place.

Two architectural wins this unlocks:

  1. Auto-update. The source author edits their design — every host design that links to it picks up the change next render. Brand assets, logos, common components stay in sync without coordination.
  2. Smaller host events. A host with N linked layers is the size of its own metadata + N coordinates, not N × source-payload. Side- steps the per-event relay size limit for large composite designs.

A third win, called out by the user during scoping: attribution chain. Every linked layer's source pubkey is computable at render time. The host's published event carries a p tag for every distinct contributing creator, so Nostr-aware clients + relays can surface "co-created by" without manual tagging.

Embedded imports stay as the default for everything that ships today. Linked imports are an opt-in alternative offered alongside, with a clear UI distinction.


2. Goals

  • A new design.kind: 'linked' variant of the existing design layer.
  • Address-coordinate-based resolution that returns "always latest" by default, with an opt-in "pin to this version" toggle exposed in the layer-options panel.
  • Three levels of nesting allowed (linked design can itself contain linked layers, up to depth 3) with cycle detection.
  • Attribution chain: published host events carry one p tag per distinct contributing creator, plus an in-app "Credits" panel that lists who contributed.
  • Convert-direction commands: "Unlink to embed" (snapshot to current version) and "Publish + link" (extract an embedded layer into its own event and replace it with a link).
  • Graceful degradation when relays don't return the source (loading state → placeholder + optional cached fallbackPreview).

3. Non-Goals

  • Linked premium / private designs in Phase 1. Encrypted-design link resolution requires the viewer's unlock state for premium and the share-list for private; both ship in Phase 2.
  • Real-time push when the source updates. The renderer polls on each render + offers a manual Refresh button; no relay subscription is held open for linked sources.
  • Editing the linked source through the host. A linked layer is read-only at the host level; to modify the source, the original creator edits the source event.
  • Cross-relay write replication. Source-author publishes as they do today; linked rendering reads from the host viewer's normal relay set.

4. Existing Artstr Fit

  • The type: 'design' layer type and renderDesignObjectIntoElement renderer already accept any payload and render via the existing renderDesignPayloadPreview compact-mode path. Linked layers reuse the same renderer; only the source of the payload changes.
  • The purchase-vault arc already established multi-relay union reads via _vaultRelayUnion() for kind-30078 events. Linked-design resolution borrows the same pattern (NIP-65 write relays + a default fallback set), keyed by address.
  • Address-tolerant lookup helpers (premiumAddressCoord, findUnlockedEntry) already exist; the linked-design resolver follows the same shape.

5. Schema

Linked design layer:

{
  id: 'layer-...',
  type: 'design',
  name: 'Linked: <source title>',
  target: 'canvas',
  x, y, w, h,
  rotate, opacity, z,
  design: {
    kind: 'linked',                                  // disambiguates from embedded
    ref: '30078:<creatorPubkey>:<dTag>',             // NIP-01 addressable coord
    mode: 'cover',                                   // hint for pre-fetch render
    pinnedEventId: null,                             // optional, locks to a specific event id
    fallbackPreview: 'data:image/jpeg;base64,...',   // optional cached preview
    lastResolvedAt: 1748656800000,                   // ms timestamp of last successful fetch
    lastResolvedSize: { w: 1920, h: 1080 },          // hint for layout
  },
}

Embedded design layer (unchanged from today, made explicit for contrast):

{
  type: 'design',
  design: {
    kind: 'embedded',                  // implicit / absent on legacy layers
    mode: 'cover',
    payload: { /* full design JSON */ },
  },
  ...
}

Legacy layers without design.kind are treated as embedded for backwards compatibility.

Top-level host-event tags (added at publish time for linked-design attribution):

['p', '<contributor pubkey>', '<relay-hint>', 'design-contributor']
['p', '<contributor pubkey>', '<relay-hint>', 'design-contributor']
...

One p tag per distinct contributing pubkey discovered by walking the host's linked layers (and their linked layers, up to depth 3). The fourth element marks the tag as design-contributor specifically so it doesn't collide with p tags emitted for replies / mentions in other Nostr contexts.


6. Versioning Model

PPTX-style "always latest" is the default. Pinning is the advanced opt-in.

  • Default (always latest): pinnedEventId === null. Renderer queries relays for kinds: [30078], '#d': [dTag], authors: [pubkey] and uses the most recent event. NIP-01 addressable semantics guarantee relays only retain the latest event per address, so this is essentially free.
  • Pinned to version: user toggles "Pin to this version" in the layer options. The current resolved eventId is written to pinnedEventId. Renderer queries by ids: [<eventId>] instead of by address — only that exact event is rendered. Source-author edits don't propagate until the host author manually un-pins or re-pins to a newer event.

The toggle is visible on every linked-design layer's right-panel. Switching states is non-destructive; flipping back to "always latest" just nulls pinnedEventId.

UI surfaces:

  • Layer-options panel: "Source: