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.
19 KiB
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:
- 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.
- 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
ptag 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 andrenderDesignObjectIntoElementrenderer already accept any payload and render via the existingrenderDesignPayloadPreviewcompact-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 forkinds: [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
eventIdis written topinnedEventId. Renderer queries byids: [<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: