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

@nominalso/vibe-bridge

v0.2.0

Published

Iframe-side SDK for building Nominal Vibe Apps — connects an embedded app to its Nominal host over a typed postMessage bridge (context, data fetch, file upload, subroute deep-linking).

Readme

@nominalso/vibe-bridge

Iframe-side SDK for building Nominal Vibe Apps — standalone web apps (typically built with Lovable) embedded in the Nominal platform via a cross-origin <iframe>. The bridge connects your app to its Nominal host over a typed postMessage protocol: read Nominal data, submit Close-Management task outputs, upload files, and keep deep-link routing in sync.

For AI agents / Lovable: this is a browser-only SDK. The wiring is always the same — construct VibeAppBridge with the host parentOrigin, await bridge.connect() once, then call data methods. Copy the quickstart below verbatim; it is the complete happy path.

Install

npm install @nominalso/vibe-bridge

Zero runtime dependencies. Ships ESM + CJS and self-contained TypeScript types.

Quickstart

import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'

// 1. Construct with the origin of the Nominal app embedding this iframe.
//    Must match exactly — drive it from an env var.
const bridge = new VibeAppBridge({
  parentOrigin: import.meta.env.VITE_PARENT_ORIGIN,
})

// 2. Connect ONCE on init and await it before any other call.
//    Resolves with the tenant/user context (or rejects after 10s).
const ctx: ContextPayload = await bridge.connect()
// ctx.tenant, ctx.subsidiaryId, ctx.subsidiaries, ctx.user, ctx.lastClosedPeriodSlug

// 3. Read Nominal data (any of the ~46 named operations).
const accounts = await bridge.getChartOfAccounts({
  path: { subsidiary_id: ctx.subsidiaryId },
})

// 4. Upload a file through the host.
const uploaded = await bridge.upload(file, {
  entityType: 'JOURNAL_ENTRY',
  entityId: '123',
  onProgress: (p) => console.log(`${p.progress}%`),
})

// 5. Submit a Close-Management task output — the ONLY write path into Nominal.
await bridge.postTaskOutput({ path: { task_instance_id: 'task-1' }, body: {} })

// 6. Tear down on unmount.
bridge.destroy()

parentOrigin via environment variable:

# .env (committed — standalone/dev default)
VITE_PARENT_ORIGIN=http://localhost:5173

# .env.local (git-ignored — override when testing inside nom-ui)
VITE_PARENT_ORIGIN=http://localhost:3000

API

new VibeAppBridge(options)

| Option | Type | Default | Description | | ---------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | parentOrigin | string | — | Origin of the Nominal app embedding this iframe. Must match the host origin exactly. | | requestTimeout | number | per-op | Global timeout (ms) before a call rejects with BridgeError code 'TIMEOUT'. Omit to use per-operation defaults: API 30000, UPLOAD_FILE 120000, INVALIDATE_CACHE 10000, GET_CONTEXT 5000. |

connect(): Promise<ContextPayload>

Call once on init and await it before anything else. Polls the host every 500 ms until context arrives; rejects with Bridge connect timed out after 10 s (usually a parentOrigin mismatch or the host hasn't mounted). Concurrent calls return the same promise.

Data operations

Every operation has a typed named method (e.g. bridge.getChartOfAccounts(payload)); payloads and return types come straight from the Nominal API. For any operation, you can also call the generic escape hatch:

// `type` autocompletes to every operation name and narrows payload + return.
const accounts = await bridge.request('GET_ACCOUNTS', { query: { account_ids: ['acc-1'] } })

See the operation catalog for the full list.

upload(file, options): Promise<UploadResponse>

Uploads a File through the host (converts to ArrayBuffer first — sandboxed iframes cannot pass File handles across windows). Resolves with { attachmentId, name } once Nominal has stored it.

| Option | Type | Description | | ------------ | ----------------------------- | ----------------------------------------------------------- | | entityType | string | Domain entity the file attaches to, e.g. 'JOURNAL_ENTRY'. | | entityId | string \| number (optional) | Id of the entity, when applicable. | | onProgress | (p: UploadProgress) => void | Called with incremental progress (0100). |

Subroute deep-linking

  • reportSubroute(subroute, { replace? }) — manually report the current subroute. Usually unnecessary — standard SPA navigation (history.pushState/replaceState) is auto-detected. Use it for hash-based or non-standard routers.
  • onSubrouteRequest(cb): () => void — register a callback for host-initiated navigation (browser back/forward). Returns an unsubscribe function. If you don't register one, the SDK falls back to history.pushState + a popstate event, which works for most SPA routers.

destroy()

Removes listeners, rejects pending requests, and restores patched history methods. Call on unmount.

Exports

VibeAppBridge, BridgeError, BRIDGE_VERSION, and the types VibeAppBridgeOptions, UploadOptions, ContextPayload, BridgeSubsidiary, UploadResponse, UploadProgress, RequestRegistry.

Common recipes

React — connect on mount, tear down on unmount:

import { useEffect, useState } from 'react'
import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'

function useVibeBridge() {
  const [ctx, setCtx] = useState<ContextPayload | null>(null)
  useEffect(() => {
    const bridge = new VibeAppBridge({ parentOrigin: import.meta.env.VITE_PARENT_ORIGIN })
    bridge.connect().then(setCtx).catch(console.error)
    return () => bridge.destroy()
  }, [])
  return ctx
}

Upload with a progress bar:

await bridge.upload(file, {
  entityType: 'JOURNAL_ENTRY',
  onProgress: (p) => (progressBar.style.width = `${p.progress}%`),
})

An operation without a named method — use the typed request():

const events = await bridge.request('GET_AUDIT_EVENTS', {})

Handle a failure by its code:

A rejected operation throws a BridgeError carrying the host's code ('RATE_LIMITED', 'FILE_TOO_LARGE', 'TIMEOUT', …); branch on it instead of string-matching the message. A failed Nominal API call throws the HttpBridgeError subtype (code: 'REQUEST_FAILED'), which adds the HTTP status — branch on err.status (e.g. 404).

import { BridgeError, HttpBridgeError } from '@nominalso/vibe-bridge'

try {
  await bridge.getAccounts({})
} catch (err) {
  if (err instanceof HttpBridgeError && err.status === 404) {
    // handle not-found
  }
  if (err instanceof BridgeError && err.code === 'RATE_LIMITED') {
    // back off and retry
  }
  throw err
}

Common mistakes

// ❌ WRONG — calling a data method before connect() resolves.
const bridge = new VibeAppBridge({ parentOrigin })
const accounts = await bridge.getChartOfAccounts({ path: { subsidiary_id: 1 } })

// ✅ CORRECT — await connect() first; it establishes the session context.
const bridge = new VibeAppBridge({ parentOrigin })
const ctx = await bridge.connect()
const accounts = await bridge.getChartOfAccounts({ path: { subsidiary_id: ctx.subsidiaryId } })
// ❌ WRONG — parentOrigin must be an ORIGIN, not a URL with a path, and must match exactly.
new VibeAppBridge({ parentOrigin: 'https://app.nominal.so/some/path' })
// ❌ a trailing slash or wrong port also fails → connect() times out after 10s.
new VibeAppBridge({ parentOrigin: 'http://localhost:3000/' })

// ✅ CORRECT — scheme + host + port only, exact match to the embedding app.
new VibeAppBridge({ parentOrigin: 'https://app.nominal.so' })
// ❌ WRONG — upload takes a File object, not a path or FormData.
await bridge.upload('/tmp/report.pdf', { entityType: 'JOURNAL_ENTRY' })

// ✅ CORRECT — pass the File (e.g. from an <input type="file">).
await bridge.upload(fileInput.files![0], { entityType: 'JOURNAL_ENTRY' })

Operation catalog

All operations are reachable as bridge.<method>(payload) or bridge.request('<OPERATION>', payload).

Accounting — chart of accounts

| Method | Operation | | ---------------------------------- | ------------------------------------- | | getChartOfAccounts | GET_CHART_OF_ACCOUNTS | | getCoaTree | GET_COA_TREE | | getCoaFlatSimple | GET_COA_FLAT_SIMPLE | | getCoaGrouped | GET_COA_GROUPED | | getCoaAccount | GET_COA_ACCOUNT | | getSubsidiaryAvailableCurrencies | GET_SUBSIDIARY_AVAILABLE_CURRENCIES | | getAccounts | GET_ACCOUNTS | | getAccount | GET_ACCOUNT |

Accounting — exchange rates

| Method | Operation | | -------------------------- | ----------------------------- | | getConversionRates | GET_CONVERSION_RATES | | getEffectiveExchangeRate | GET_EFFECTIVE_EXCHANGE_RATE | | getExchangeRateByDate | GET_EXCHANGE_RATE_BY_DATE |

Accounting — dimensions

| Method | Operation | | -------------------------------- | ----------------------------------- | | getDimensions | GET_DIMENSIONS | | getDimensionValues | GET_DIMENSION_VALUES | | getDimensionValuesHierarchical | GET_DIMENSION_VALUES_HIERARCHICAL | | getDimensionAccountAssignments | GET_DIMENSION_ACCOUNT_ASSIGNMENTS |

Accounting — journal entries

| Method | Operation | | ------------------- | --------------------- | | getJournalEntries | GET_JOURNAL_ENTRIES | | getJournalLines | GET_JOURNAL_LINES | | getJournalEntry | GET_JOURNAL_ENTRY |

Activity — period instances

| Method | Operation | | ---------------------------- | ------------------------------- | | getPeriods | GET_PERIODS | | getPeriodInstance | GET_PERIOD_INSTANCE | | getPeriodInstanceBySlug | GET_PERIOD_INSTANCE_BY_SLUG | | getPeriodProgressBreakdown | GET_PERIOD_PROGRESS_BREAKDOWN |

Activity — activity definitions & instances

| Method | Operation | | ----------------------------- | --------------------------------- | | getActivityDefinitions | GET_ACTIVITY_DEFINITIONS | | getActivityDefinition | GET_ACTIVITY_DEFINITION | | getActivityInstances | GET_ACTIVITY_INSTANCES | | getActivityInstance | GET_ACTIVITY_INSTANCE | | getActivityInstanceByPeriod | GET_ACTIVITY_INSTANCE_BY_PERIOD | | getActivityInstanceTasks | GET_ACTIVITY_INSTANCE_TASKS | | getActivityPeriodTasks | GET_ACTIVITY_PERIOD_TASKS |

Activity — task definitions & instances

| Method | Operation | | ---------------------------- | -------------------------------- | | getTaskDefinitions | GET_TASK_DEFINITIONS | | getTaskDefinition | GET_TASK_DEFINITION | | createTaskDefinition | CREATE_TASK_DEFINITION | | updateTaskDefinition | UPDATE_TASK_DEFINITION | | getTaskDefinitionsByFilter | GET_TASK_DEFINITIONS_BY_FILTER | | getTaskInstances | GET_TASK_INSTANCES | | getTaskInstance | GET_TASK_INSTANCE | | getTaskInstancesByFilter | GET_TASK_INSTANCES_BY_FILTER | | postTaskOutput | POST_TASK_OUTPUT |

Audit trail

| Method | Operation | | ---------------------- | ------------------------- | | getAuditEvents | GET_AUDIT_EVENTS | | getEntityAuditEvents | GET_ENTITY_AUDIT_EVENTS |

Period manager — fiscal calendars

| Method | Operation | | -------------------- | ---------------------- | | getFiscalCalendars | GET_FISCAL_CALENDARS | | getFiscalCalendar | GET_FISCAL_CALENDAR |

Tenancy

| Method | Operation | | ------------------------------- | ---------------------------------- | | getSubsidiaries | GET_SUBSIDIARIES | | getSubsidiary | GET_SUBSIDIARY | | getSubsidiaryParentCurrencies | GET_SUBSIDIARY_PARENT_CURRENCIES | | getTenantUsers | GET_TENANT_USERS |

connect() (context) and upload() (file upload) have dedicated methods and are not in this table.

How it fits together

The host side is @nominalso/vibe-host, used by the Nominal app (nom-ui). See the repository for the full protocol, architecture, and connect() semantics. Bundled agent docs ship in this package under docs/ and AGENTS.md.

License

UNLICENSED — proprietary. © Nominal. All rights reserved.