Guides

Edges: backlinks and authored connections

Which Blocks point at a Block: the Edges the Block index projects from references, Derivations, embeds and tags, the ones a person adds in the inspector, the edges route and the drizzle table.

An Edge is a directed, typed connection from one Block to another Block or to a tag name. The Block index holds the Edges of every Block next to its row, so the question „which Blocks point at this one?“ needs no scan of the Documents.

Where Edges come from

projectDocument (@skaile/forge-editor/core) fills BlockRow.edges while it projects a Document. One Edge exists per kind and target; a Block never lists the same Edge twice.

kindoriginWritten by
referencecontenta reference chip ([@…]) anywhere in the Block, nested Blocks included
derivationcontentthe Block the Block's Derivation was made from (not an uploaded asset)
embedcontenta blockEmbed (![[…]]) in the Block
tagcontenta tag chip or a tag of a Typed block; the target is { tag }, not a Block
any other stringmetaan Edge a person added by hand: meta.edges of the Typed block

A target without a Document resolves to the Document the Block is in. The index never checks that the target exists: an Edge to a deleted Block stays until its source changes.

import { projectDocument } from '@skaile/forge-editor/core'

const [row] = projectDocument(content, 'doc-1')
row.edges
// [{ to: { documentId: 'doc-2', blockId: 'b' }, kind: 'reference', origin: 'content' },
//  { to: { tag: 'klima' }, kind: 'tag', origin: 'content' },
//  { to: { documentId: 'doc-1', blockId: 'c' }, kind: 'supports', origin: 'meta' }]

Ask for the Edges of a Block

BlockIndex.edges(address, direction, scope) answers BlockEdge[]: the row's Edge with from, the Block it is written in.

  • direction: 'out' are the Edges written in the Block itself, tag Edges included.
  • direction: 'in' are the Edges of other Blocks that point at it, in Document order of scope. A tag Edge never points at a Block, so it is never an incoming one.

The backlinks of a Block are its incoming Edges. A Block that does not exist has no Edges ([]), not an error.

Route and client

RouteGET /api/editor/blocks/:documentId/:blockId/edges?direction=in|out (API.blockEdges, port index)
Defaultin
ClientEditorBackend.index.edges(address, direction?, signal?), optional: a backend without Edges leaves it out

The route answers only Edges written in Documents the user may read (the same scope as the Workspace search). It answers 403 when the user may not read the Document of the Block, 400 for a direction other than in or out, 401 without a user and 501 without the index port.

Add Edges by hand

The inspector and the details card of a Typed block show the section Kanten (the edges slot of compositeFeature, EdgesSection in both bindings). It lists the Edges of meta.edges with the kind and the title of the target, and it has a form to add one: the kind as free text, then the target, searched with the same list as the [@ popup (the blocks of this Document, and with the index capability those of other Documents). A click on an entry adds the Edge; the remove button next to an Edge takes it out again. A read-only editor shows the list without the form.

meta.edges is a list of { to: { documentId?, blockId }, kind } (AuthoredEdge, META_EDGES_KEY). The key is written through setBlockProperty, so it follows the same rules as every other Block property: in a collaboration session the server writes it into the open Y.Doc. Entries without a block ID or without a kind are ignored by the projection.

The framework-free model is features/composites/edges-model.ts (edgesSectionOf, addAuthoredEdge, removeAuthoredEdge, edgeTargetChoicesOf); the Vue and React components only render it.

The drizzle table

The drizzle adapter writes the Edges to editor_block_edges in the same transaction as the rows of replace:

Column
from_document_id, from_block_idthe Block the Edge is written in
seqposition in the row's list; with from_document_id the primary key
to_document_id, to_block_idthe target Block; NULL for a tag Edge
tagthe tag name of a tag Edge, else NULL
kind, originas in BlockEdgeRow

An index on (to_document_id, to_block_id) answers the incoming Edges without a scan. blockIndexTables() returns { blocks, edges }, so a host that migrates the tables with drizzle-kit adds both to its schema; bootstrapBlockIndexTables(db) creates both with IF NOT EXISTS. The memory adapter answers from its rows; both pass the same contract suite (test/support/block-index-contract.ts).

Copyright © 2026