npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@notionhq/custom-blocks-host

v0.1.20

Published

Reusable host runtime for Notion custom blocks.

Readme

@notionhq/custom-blocks-host

Host runtime for the custom block bridge. This package owns the generic postMessage protocol loop used by hosts such as the local dev shell. It does not own iframe policy, permissions, persistence, analytics, or host UI state. Host implementations such as the dev shell and notion-next depend on this package.

getAutoBoundPropertyIdsByKey()

getAutoBoundPropertyIdsByKey() matches manifest properties to schema properties for initial property binding. For each manifest property:

  1. Consider only schema properties of the same type.
  2. A schema property is a candidate if its ID equals the manifest key (exact string: no trim, no case folding) or its display name equals the manifest display name after trim() and lowercasing.
  3. Bind when there is exactly one candidate. Otherwise leave it unmapped.

Wrong-type properties never enter the candidate set.

import { getAutoBoundPropertyIdsByKey } from "@notionhq/custom-blocks-host"

const propertyIdsByKey = getAutoBoundPropertyIdsByKey({
  manifestProperties: {
    title: { name: "Task", type: "title" },
    estimate: { name: "Estimate", type: "number" },
  },
  propertySchemasById: {
    title: { name: "Different label", type: "title" },
    "property-1": { name: " estimate ", type: "number" },
  },
})

// { title: "title", estimate: "property-1" }

createCustomBlockHost()

createCustomBlockHost() attaches a message listener, performs the connect / init / initResult handshake, validates sandbox messages, routes requests to handlers, manages query subscriptions, and emits live state updates.

import { createCustomBlockHost } from "@notionhq/custom-blocks-host"

const host = createCustomBlockHost({
  iframe,
  sandboxOrigin: "https://example.com",
  initialState: {
    theme,
    contrastMode,
    blockId,
    parent,
    page,
    currentUser,
    manifest,
    dataSources: { bindings },
  },
  handlers: {
    queryDataSource: async message => ({
      status: "success",
      items: await queryRows(message),
      hasMore: false,
    }),
    createPage: async message => createPage(message),
    getPage: async message => getPage(message),
    openPage: async message => openPage(message),
    updatePage: async message => updatePage(message),
    getUser: async message => getUser(message),
    listUsers: async message => listUsers(message),
    resize: message => resizeIframe(message.height),
  },
  onManifest: manifest => {
    console.log("Selected manifest", manifest)
  },
  onInitialized: state => {
    console.log("Initialized manifest", state.manifest)
  },
  onInitializationFailure: error => {
    showInitializationError(error)
  },
  onRestartRequired: () => {
    remountHostAndIframe()
  },
  onLog: (direction, payload) => {
    console.log(direction, payload)
  },
})

minBridgeProtocolVersion is optional and defaults to the oldest protocol version the host runtime supports. Set a higher value to retire older SDKs. Lower values cannot enable versions the runtime no longer supports. The host does not enforce a maximum protocol version.

Each host instance initializes once:

  • onManifest provides the selected manifest as soon as it is available after connect, before binding validation and onInitialized. Use it to configure bindings even when initialization cannot yet succeed.

  • onInitialized reports success after the sandbox accepts initialization. It provides the selected manifest and the sandbox's optional initialHeight.

  • onInitializationFailure reports an initialization error or timeout.

  • onRestartRequired reports a connection attempt after the host accepts its first connection or stops waiting. Recreate the host and iframe together to accept a new connection. This can occur after the iframe document changes or a connection arrives after a timeout.

The host uses the manifest from connect, or initialState.manifest if connect omits it. If neither exists, initialization fails with manifest_unavailable.

queryDataSource is required. Other request handlers are optional. Every request handler may return its result synchronously or as a promise. Results use a status discriminator: successful results carry their response payload, while errors carry { code, message, isRetryable }.

Request types

TypeScript infers request types for handlers passed to createCustomBlockHost(). For separately defined handlers and helpers, import the corresponding request and page-property types directly from @notionhq/custom-blocks-host.

Request types such as GetPageMessage describe validated messages that handlers receive. The corresponding *MessageInput types accept plain string IDs for adapters and test fixtures that construct requests. These types do not validate values at runtime. The host runtime validates incoming messages before it calls handlers. Your implementation remains responsible for permissions and record access.

Query requests

The queryDataSource handler receives raw data source and property IDs. Its optional filter and sorts fields have already been resolved by the SDK. The host validates that sorts contain at most 10 unique property IDs, checks property IDs and currently supported property types against the bound collection schema when available, and applies valid sorts in the order provided by the array.

When a data source binding includes collectionSchema, the runtime validates select, multi-select, and status option names before invoking queryDataSource. Bindings without a collection schema cannot perform this check, so their query handlers remain responsible for validating option names.

initialState.dataSources may also be a function of the successful connect message. This lets a host derive bindings from the sandbox-provided manifest.

Set sandboxOrigin to the iframe's exact origin. The runtime targets outbound messages to that origin and rejects inbound messages from any other origin. The runtime also accepts noConnectTimeoutMs and noInitResultTimeoutMs; both default to five seconds.

Initialization and malformed messages

createCustomBlockHost() performs one handshake for each host and iframe pair:

sandbox connect → host init → sandbox initResult

The sandbox creates the initializationId. The host copies that ID into init. The sandbox copies it into initResult. The host sends live state changes only after it receives a matching initResult.success.

The host must check that the initializationId in initResult exactly matches the ID in the init message. The host ignores a result with a different ID. If no result arrives, or only a stale valid result arrives, before the timeout, initialization ends with no_init_result.

The host validates each inbound sandbox message with sandboxToHostMessageSchema before it handles the message. If parsing fails, the host uses readIncomingType() to identify the attempted message. It replies with invalidSandboxMessage, a negative acknowledgment (NACK). The host never sends a NACK in response to a NACK.

Malformed sandbox messages do not change the host's initialization state or timeout, except in these two cases:

  • The host expects connect and receives a malformed connect with a readable initializationId. The host sends init.error with invalid_connect_payload instead of a NACK, then fails initialization.
  • The host expects initResult and receives a malformed initResult with the active initializationId. The host sends a NACK and fails initialization with invalid_init_result_payload. This error code describes a local host failure. The host does not send it in initResult.error.

A malformed initResult with a missing or stale ID does not end initialization or reset its timeout. Malformed messages do not restart initialization after it succeeds or fails.

The host accepts a valid initResult.error as a terminal initialization failure. It logs and ignores a valid initResult with a stale initializationId or an unexpected state.

Hosts do not retry malformed messages. A host that receives a connect after initialization has settled must use onRestartRequired to recreate the host and iframe together.

Updating live state

The returned handle exposes:

  • setTheme()
  • setContrastMode()
  • setParent()
  • setPage()
  • setDataSources()
  • setCurrentUser()
  • refreshQuery()
  • post()
  • stop()

State updates are sent after initResult.success. Updates made earlier are held until initialization completes. refreshQuery() re-runs queryDataSource for every active subscription with the supplied data source ID, preserving each subscription's filter, sorts, and limit.

The runtime removes a subscription when it receives unsubscribeDataSourceQuery. Repeated unsubscribe messages are safe. The runtime also ignores results from a query that was unsubscribed while its handler was running.

Call stop() when the iframe is removed. It detaches listeners, clears timeouts, and discards active query subscriptions.

Wire protocol

createCustomBlockHost() implements the protocol: it validates inbound messages, runs the initialization handshake, correlates requests, manages query subscriptions, and emits bridge NACKs. Use its state-update methods and handlers instead of posting bridge messages directly.

The bridge protocol is maintained in this repository's protocol/ package. The published host package includes a compiled copy of that protocol and does not expose it as a public subpath. Consumers should use the host APIs above instead of importing protocol implementation files.

Host source in this repository imports protocol schemas and types from the workspace package. The publish build rewrites those imports to the bundled copy.

Handler implementations must preserve message identity: one-shot operations return one result with the request's requestId, while queryDataSource is a long-lived subscription keyed by subscriptionId. The runtime handles the connect → init → initResult lifecycle, including waiting for initResult.success before live updates.

Error payloads use { code, message, isRetryable }. The runtime accepts any string for code, including codes that this package does not know yet. Host code should preserve unknown codes and use isRetryable to decide whether a retry is appropriate.