Legacy compatibility · sd1 integration patterns

Use the legacy sd1 envelope. Keep your architecture.

These practical patterns use the older JSON/Base64 sd1 envelope and its multi-runtime APIs, add one capability at a time, and show where SignedData stops.

Not the SDB1 integration guideThe examples below use legacy sd1 tokens and APIs. New raw-byte integrations should start with SDB1.

Guide index

WASM

Host the portable core

Use the Wasm signer and digest through the friendly JavaScript envelope API.

Read pattern ↓
FORM

Signed form submissions

Bind form policy to a purpose and short lifetime while planning legacy migration separately.

Read pattern ↓
WEB

Browser verification

Inspect locally, select a key from trusted configuration, and verify before displaying authenticated claims.

Read pattern ↓
FLOW

Partial ParcelStreams

Handle arbitrary read boundaries while keeping framing completeness separate from trust.

Read pattern ↓
KEY

Key rotation

Use an opaque key ID as a routing hint, then require the chosen signer's identity to match exactly.

Read pattern ↓
P2P

Recoverable transport

Sign the complete token first, fragment it outside the envelope, reconstruct it, then verify.

Read pattern ↓
CODEC

Custom serializer

Implement three explicit methods and keep code selection in trusted application policy.

Read pattern ↓
TLV

Application-owned parcels

Assign compact type numbers without asking SignedData to know a mailbox, actor, or message schema.

Read pattern ↓

Host the legacy sd1 Wasm core through JavaScript

Let Wasm own deterministic byte-heavy operations and JavaScript own the readable envelope policy. Switching to Web Crypto or the pure-JavaScript fallback does not change the sign/verify API.

const core = await loadSignedDataWasm();
const signer = createWasmHmacSigner(core, secret, {
  keyId: "messages-2026-08"
});

const token = await sign(payload, {
  signer,
  purpose: "example/message/1"
});

Ruby services can create or verify the same token with the gem. Use the live interoperability lab to watch all paths reproduce one fixed vector.

Signed form submissions

Use a purpose that names the action, not merely “form.” Include a short expiry when the signed value authorizes submission.

purpose: "account/contact-form/1"
expires_at: Time.now.to_i + 15.minutes.to_i

Verify with that exact purpose before parsing or applying the payload. The current SignedForm gem has a legacy compatibility token of its own. Keep that verifier isolated; migration belongs in the form profile, not this envelope.

Read the detailed SignedData → SignedForm roadmap →

Decode awkward chunks without inventing partial trust

The incremental ParcelStream decoder can consume a socket-sized chunk and emit only complete parcels. A complete parcel is framed, not necessarily authenticated.

const decoder = new ParcelStreamDecoder();

for await (const chunk of source) {
  for (const parcel of decoder.feed(chunk)) {
    stage(parcel);
  }
}
decoder.finish();

For an sd1 payload, verify the whole envelope before acting on staged values. If the transport authenticates each chunk separately, that transport defines early-trust policy. The interactive stream guide lets you change chunk sizes and trust modes.

Legacy sd1 browser verification

A browser may inspect the header to decide which already-trusted public key should verify a token. The incoming algorithm or key ID must never cause arbitrary code or remote key material to be trusted automatically.

  1. Parse only enough to obtain an untrusted key hint.
  2. Resolve that hint through trusted application configuration.
  3. Verify the exact first three wire segments.
  4. Apply purpose and expiry policy.
  5. Only then render or deserialize the payload.

Watch this trust transition in the workbench →

Application-owned parcels

A Caravan-shaped message could assign 1 to sender bytes, 2 to recipient bytes, and 3 to a body. Those meanings belong to a Caravan schema, never to SignedData.

parcels = [
  [1, sender_address_bytes],
  [2, recipient_address_bytes],
  [3, body_bytes]
]

Unknown types remain skippable. Duplicate types and ordering remain intact. An application can explicitly declare that a parcel contains another ParcelStream.

Independently useful, intentionally uncoupledSignedData and Caravan can share specified bytes later without either repository importing the other today.

Legacy sd1 key rotation

Give each active key an opaque, stable ID. A key ring can use peek only to choose a candidate, then pass that signer into strict verification.

hint = SignedData.peek(token).key_id
signer = trusted_keyring.fetch(hint)

SignedData.verify(
  token,
  signer: signer,
  purpose: "example/message/1"
)

Keep retired verification keys for the lifetime of tokens they signed. Stop signing with an old key before removing verification support.

Recoverable transport

The sd1 token is the authenticated object. Torrenting, erasure codes, fountain codes, QR chunks, and relay frames operate outside it.

  1. Create the complete token.
  2. Fragment or encode that ASCII token for transport.
  3. Reconstruct the exact token bytes.
  4. Verify before trusting payload or description.

Relays can remain “dumb”: they need no signer, serializer, mailbox schema, or application runtime.

Bring a custom serializer to legacy sd1

A serializer has a versioned ID plus dump and load. Keep it boring and explicit.

class LinesSerializer
  def id = "example/lines/1"
  def dump(lines) = lines.join("\n").b
  def load(bytes) = bytes.split("\n")
end

SignedData authenticates the ID and SHA-256 identity of the serialized bytes. The caller still chooses the serializer; an incoming format string never loads a Ruby constant.