guide overview· design
bridge flow xtz deposits· fa deposits· xtz withdrawals· xtz fast withdrawals· fa withdrawals
data model l2 operations
bridge indexing · design system

Design system

Color — two axes

Axis 1 · HUE = runtime

L1 / bridge--l1
EVM L2--evm
Michelson L2--mich
alias / NAC--alias (reserved)
system / kernel--sys

Axis 2 · ACCENT = indexed

indexed ✓--idx
planned ◔--planned

Encoding — legend

Legend
Hue L1 / bridge L2 EVM L2 Michelson system / kernel
Decision indexed planned not indexed
Status legacy · remains in mainnet

Source vocabulary

The .s line on every node is source · type · address. The source token names where the indexer (or, for non-indexed nodes, where the reader) can look up the object. Only the labels below are valid in diagrams.

SourceDipDup datasourceWhat's readVisible in diagrams
TzKTtzkt · tezos.tzktL1 operations, head, rollup inboxyes — Tezos-side anchors and rollup inbox
EVM nodeetherlink_node · evm.node + etherlink_subsquid · evm.subsquidL2 events and transactions (node for tip, subsquid for backfill)yes — every EVM anchor
Rollup noderollup_node · httpOutbox messages and proofsyes — Rollup outbox message
Tezos nodetezos_node · httpProtocol constants (one-shot at bootstrap)no — out-of-band lookup, no on-chain object
Metadatametadata · tzip_metadataTZIP-21 token metadatano — out-of-band lookup during ticket resolve
Kernel— (not a data source)Kernel-internal precompile or routine with no observable EVM traceyes — pedagogical nodes (e.g. SendOutboxMessage, parse_routing)
Kernel ≠ data source. Kernel · … means the object lives only inside the rollup kernel — no event, no separate tx, no standard JSON-RPC anchor. Such nodes are always faded (never indexed) and exist on the diagram to keep the flow legible (e.g. show how EVM Withdrawal becomes Rollup outbox message).

Layout and verification

one object = one node

One on-chain object = one node in the standard format (§3 · Node dictionary). Paired events (Token Mint/Burn) — as a separate node.

self-check before showing

Mermaid captures the geometry (overflow / collisions / a “missed arrow” are impossible). Before showing — verify the render has no console errors and that line statuses/colors are in place; screenshot the diagram by uid (take_snapshot → take_screenshot).

Mermaid

flowchart TD
  U(["User"]):::actor
  K["<b class='t'>Kernel parse_routing</b>"]:::sys
  subgraph R["L2 · EVM · ACTUAL"]
    direction TB
    E["<span class='glyph'>✓</span><b class='t'>Value Transfer</b><span class='s'>EVM node · transaction · 0x…feed</span><span class='ix'>✓ etherlink.on_xtz_deposit</span><span class='md'>✚ EtherlinkDepositOperation</span>"]:::evm
    F["<b class='t'>Event Deposit</b><span class='s'>EVM node · event · 0xff…01</span>"]:::evm
    E --> F
  end
  M["<span class='glyph'>◔</span><b class='t'>Event Deposit</b><span class='s'>TzKT · event · tz1Ke2h7…</span><span class='pln'>◔ planned: tezos.events</span>"]:::mich
  L["<b class='t'>Value Transfer</b><span class='s'>EVM node · transaction · 0x00…00</span>"]:::evm

  K -->|has code → queue| E
  K -->|Michelson · tz1| M
  K -->|old kernel| L
  U -->|initiates deposit| K

  class E indexed
  class M planned
  class F faded
  class L legacy
  class L faded
  class U faded
  class K faded

  classDef l1 fill:#e8836b1f,stroke:#e8836b,stroke-width:1.5px;
  classDef evm fill:#5aa6e01f,stroke:#5aa6e0,stroke-width:1.5px;
  classDef mich fill:#6fbf731f,stroke:#6fbf73,stroke-width:1.5px;
  classDef sys fill:#8b919c1f,stroke:#8b919c,stroke-width:1.5px;
  classDef indexed stroke-width:2px;
  classDef planned stroke-width:2px;
  classDef faded stroke-width:1.5px;
  classDef legacy stroke-width:1.5px;
  classDef actor fill:transparent,stroke:#e8836b,stroke-width:1.5px;

  linkStyle 0 stroke:#5aa6e0,stroke-width:2.4px;
  linkStyle 1 stroke:#5aa6e0,stroke-width:2.4px;
  linkStyle 2 stroke:#6fbf73,stroke-width:2.4px;
  linkStyle 3 stroke:#5aa6e0,stroke-width:2.4px;
  linkStyle 4 stroke:#e8836b,stroke-width:2.4px;

1 · Node HUE — classDef, applied inline

// fill = hue-bg (.12 alpha, 8-digit hex), stroke = hue
classDef l1   fill:#e8836b1f,stroke:#e8836b,stroke-width:1.5px;
classDef evm  fill:#5aa6e01f,stroke:#5aa6e0,stroke-width:1.5px;
classDef mich fill:#6fbf731f,stroke:#6fbf73,stroke-width:1.5px;
classDef sys  fill:#8b919c1f,stroke:#8b919c,stroke-width:1.5px;

NODE["…"]:::evm   // hue — inline on the node
⚠️ Pitfall: do not write color: in classDef. Mermaid turns it into the rule .class span{color … !important}, which overrides all per-line colors inside the node (handler/source turn white). Text color is set only by themeCSS (§3); classDef is the node's border/fill.

2 · DECISION — one line per node

class NODE indexed   // ✓ teal glyph + teal handler line
class NODE planned   // ◔ amber glyph
class NODE faded     // not indexed (opacity .45)
class NODE legacy    // dashed border; multiple lines per node allowed
⚠️ Pitfall: a comma in class is a list of NODES, not classes. class NODE evm,indexed reads as “class indexed for nodes NODE and evm” — the hue won't arrive. That's why hue is inline :::evm and decision is one line per class.

3 · Node dictionary — HTML label + themeCSS

NODE["<span class='glyph'></span><b class='t'>Event Withdrawal</b><span class='s'>EVM node · event · 0xff…02</span><span class='ix'>✓ etherlink.on_withdraw</span><span class='md'>✚ EtherlinkWithdrawOperation</span>"]:::evm

// lines: .t name · .s source (source · type · address) · .ix handler (teal) · .md model · .pln planned (amber)
// themeCSS (inside mermaid.initialize) — line colors:
.nodeLabel .t   { display:block; white-space:nowrap; font-weight:600; color:#eef0f3; padding-right:20px } // name
.nodeLabel .s   { display:block; white-space:nowrap; font-size:9.5px; color:#7c828e } // source
.nodeLabel .ix  { display:block; font-size:9.5px; color:#3ed6c0 }   // handler (teal)
.nodeLabel .md  { display:block; font-size:9.5px; color:#7c828e }   // model
.nodeLabel .pln { display:block; font-size:9.5px; color:#d9a83c }   // planned (amber)
.nodeLabel .glyph { position:absolute; top:-2px; right:0; font-size:17px; font-weight:700 }
.node.indexed .nodeLabel .glyph { color:#3ed6c0 }  // ✓ teal
.node.planned .nodeLabel .glyph { color:#d9a83c }  // ◔ amber
.node.faded   { opacity:.45 }
.node.legacy rect { stroke-dasharray:5 4 }
Node lines do not wrap. white-space:nowrap on every line + keeping them short: the node grows wider, not taller, and doesn't break the layout. source — strictly source · type · address, nothing more. The trailing name (method, event, entrypoint, notes like input=0x / 4-arg / → receiver) lives in the title and is not repeated in source: it only widens the node. The shorter the source line, the narrower the node — and node width, multiplied across side-by-side columns, is what drives the whole diagram's width.
Address — hash or alias, no strict rule. Both read fine: a truncated hash (0xff…01, tz1Ke2h7…) or a role alias (rollup, gateway, tez-ticketer). A hash earns its place when it distinguishes several similar contracts (0xff…01 XTZBridge vs 0xff…02 FABridge); an alias is clearer when there's a single well-known contract and the hash would distinguish nothing. Multi-word aliases are hyphenated.
Title stays short. Forms: Call Contract.method, Event Name, or a role label (Transfer Ticket, Value Transfer, Rollup cement). No runtime prefix (Tezos … — the hue already encodes the runtime) and no parenthetical qualifiers ((mint) / (burn) — disambiguation belongs in the source address).
No stray "why / when" lines. A node says what is indexed. Free-text notes about conditions or phase (only if code present, Block 3, proxy specified?, bridge.rs:401-507) don't read at node size and just clutter — if something needs explaining, extend the graphic, not the node. This isn't a ban on conditions or roadmap: recurring structural facts get a defined encoding here first (a hue, a glyph, a dedicated line class — the way roadmap is already carried by color + status glyphs), and only then appear in nodes. Until it's in this standard, it doesn't go in a node.

4 · ARROW — linkStyle by declaration order

linkStyle 0 stroke:#5aa6e0,stroke-width:2.4px;                      // evm — solid
linkStyle 4 stroke:#e8836b,stroke-width:2.4px,stroke-dasharray:6 5; // flow break — dashed (see §4.2)
Mermaid trade-off: the condition label on the arrow stays neutral gray (per-label color matching the runtime hue isn't available out of the box). A deliberate concession for the mermaid medium.
Uniform opacity. All edges render at full opacity — no per-edge opacity. Dimming is a node property (faded / legacy, §2), never an edge one.

4.1 · Arrow labels — what value moves

A -->|xtz| B                // xtz moves along the edge — type only, from/to obvious from nodes
A -->|ticket| B             // ticket moves along the edge — same rule
A -->|User → Helper| B      // token transfer: B is a `Call Token.transfer` node — write from → to, no `Token:` prefix
A -->|mint → User| B        // ERC-20 mint: B is `Call Token.mint`
A -->|User → burn| B        // ERC-20 burn: B is `Call Token.burn`
A -->|approve Ticketer| B   // approve: B is `Call Token.approve`, write target spender
A -->|NAC| B                // system routing labels stay as short action words
xtz / ticket — type only, no from/to. When xtz or a ticket moves along the edge, just write xtz or ticket — node positions already say who sends and who receives. Tokens, by contrast, always materialize as a dedicated Call Token.* node (transfer / mint / burn / approve); the arrow leading INTO that call carries from → to with no Token: prefix — already implicit from the call name. System and routing labels (NAC, SP sees commitment, with proxy → queue) stay as short action words.

4.2 · ARROW — solid vs dashed

A --> B    // solid — the flow continues on-chain
A -.-> B   // dashed — the flow leaves the chain before continuing
Solid — one object leads to the next on-chain, nothing off-chain in between. Dashed — the flow leaves the chain before it continues: an off-chain actor must observe and submit the next tx (watchtower sees queued deposit), or a protocol delay must elapse (⏳ refutation window).

5 · Actor — capsule (stadium node)

SP(["Service Provider"]):::actor
class SP faded
classDef actor fill:transparent,stroke:#e8836b,stroke-width:1.5px;

6 · Init — canonical block (extracted into mermaid-init.js)

import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({
  startOnLoad:false, securityLevel:'loose', theme:'base',
  themeCSS,                                  // the CSS string from §3
  themeVariables:{
    fontFamily:'JetBrains Mono, monospace', fontSize:'12px',
    lineColor:'#565c67', clusterBkg:'transparent',
    clusterBorder:'#1e212a', edgeLabelBackground:'#0e1014',
  },
  flowchart:{ htmlLabels:true, curve:'basis', nodeSpacing:42, rankSpacing:58, padding:10, useMaxWidth:false },
});
await mermaid.run({ querySelector:'.mermaid' });

Field ledger

A table lens for DB models — one row per field, live on the data model · l2 operations page. Where flow diagrams map on-chain objects, the ledger maps a model's columns: scope, per-runtime meaning, UI use, and writer. Shared tokens (runtime hues, ⚠/◔ glyphs, legendbox) are reused; the table-specific mechanics below apply where a page renders a .led table.

F1 · Columns & layout

field · type · dep · wd · UI★ · EVM holds · Michelson holds · written by
Rows group by scope via a full-width header row: shared (the abstract base) · deposit-only · withdrawal-only. The two runtime columns carry the same field's meaning per runtime; a runtime-agnostic value spans both (colspan=2). The table is width:auto (sized to content, never stretched) and centered via a fit-content wrapper; the legend sits in that same wrapper so it matches the table width. Both live inside the .mapwrap, which scrolls horizontally only if the table outgrows the viewport.

F2 · Badges — fixed order, half-badges for composites

// type-column badges — ALWAYS in this order:
PK / FK   (teal / orange)   // key role — stays beside the type word
null      (faint)           // nullable
uq        (green)           // unique

// a multi-column constraint is NOT a prose row — one half-badge per member:
uq ↓      // upper member
uq ↑      // lower member — both carry the SAME title = the full constraint, on hover
Fixed badge order: type-role → nulluq. Never reorder per row; the consistency is the readability. A unique_together(a, b) becomes uq ↓ on a and uq ↑ on b, the full constraint in a shared title tooltip — then the prose row is deleted.

F3 · Scope dots

●  on   — field present on this side
◐  half — present, but the FK is nullable on this side
○  off  — absent on this side

F4 · Cell hue = runtime · ⚠ = a real smell, not polymorphism

.evm   blue text   // EVM-runtime meaning / writer
.mich  green text  // Michelson-runtime meaning / writer
Hue is per-cell, per-runtime — not per-field. The two runtime columns and the written by tokens (event / op, handler / michelson_matcher) are tinted independently, so a divergence reads at a glance.
⚠ is reserved for a real smell — not mere polymorphism. A field that just holds a different value per runtime (level, address, l2_account) is honest and gets no ⚠. ⚠ marks a defect: a name that lies (transaction_hash holding an op-hash), a wrong unit (transaction_index = op.counter), or one concept named two ways across tables (ticket_owner vs l2_ticket_owner). If everything is flagged, nothing is.

F5 · ★ real consumers · ◔ proposed · detail on hover

marks a field a real downstream consumer actually reads — verified against captured traffic, not assumed (here: the live bridge UI's GraphQL query). Its value is the negative space it exposes — how few fields are truly needed.
◔ proposed (gold). A not-yet-built field lives inline in its natural group (gold row tint + ◔ proposed writer), never in a separate "future" block. A proposed change to an existing field is shown on that field — e.g. a gold null badge with the rationale on a title hover — not as a duplicate row. Keep cells terse; push explanations into title tooltips. Mechanics for not-yet-shipped planes (e.g. the matcher plane) stay in a separate experimental design page, out of this committed vocabulary.