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

@businessdash/sdk

v0.9.90

Published

BusinessDash developer SDK — the data layer that lets you build customer-facing surfaces on your own domain while BusinessDash handles the operations stack behind the scenes. 0.9.x ships a comprehensive programmatic surface: storefront (products, categori

Readme

@businessdash/sdk

Formerly published as @businessdash/sdk (≤0.9.53) — same API, new name.

Alpha — 0.9.53. This package declares, validates, and orchestrates your application's database schema, form layout, and type-safe field rules in a single declarative pipeline — and ships the answer-engine surface (llms.txt + product feed, MCP connector proxy, AI-crawler analytics) that makes the site you build with it legible to AI assistants.


Install

pnpm add @businessdash/sdk

Imports

import {
  collection,
  bd,
  Enums,
  createFormSchemaOrchestrator,
  layoutContainers,
  SchemaBoundary,
} from "@businessdash/sdk";

// Destructure visual layout wrappers for intuitive, HTML-like inline grouping
const { FormStep, ConditionalBlock, MultiPopulator, OrCondition } = layoutContainers;

Core bd Primitive Modifiers

The compact bd factory namespace eliminates boilerplate. Each chain method implements fluent builders to define data rules, UI presentation hints, database constraints, and cryptographic security features.

Core Field API Reference

bd.text().formElement("short_text")       // Map raw DB type to a front-end input component
  .validate({ minLength: 3, maxLength: 100 })  // Append structural validation parameters
  .helper("The display name of the item")       // Descriptive metadata for tooltips
  .regex(/^[A-Z][a-z]+$/)                      // Absolute input string validation
  .check("name != ''")                          // Low-level SQL CHECK constraint
  .encrypted()                                  // Per-user cryptographic isolation
  .encryptedLight()                             // Master key encryption-at-rest
  .private()                                    // Strip from public log telemetry
  .required();

Available .validate() rules:

| Rule | Applies to | Description | |---|---|---| | minLength | TEXT, ARRAY | Minimum characters or entries | | maxLength | TEXT, ARRAY | Maximum characters or entries | | lessThan | NUMBER, NUMERIC | Strictly less than | | greaterThan | NUMBER, NUMERIC | Strictly greater than | | round | NUMBER, NUMERIC | Decimal places to round to | | positive | NUMBER, NUMERIC | Must be >= 0 | | notNull | Any | Reject null/empty | | floor | NUMBER, NUMERIC | Floor the value | | isDecimal | NUMBER, NUMERIC | Must have decimal component | | regex | TEXT | Pattern match | | checkExpression | TEXT, NUMBER | SQL CHECK expression | | lessThanToday | DATE, DATE_TIME | Must be historical | | greaterThanToday | DATE, DATE_TIME | Must be future |

Form Elements

.formElement("short_text")     // TEXT → short/long/email/url/dropdown/radio/color/…
.formElement("currency")       // NUMBER/NUMERIC → currency/range/star_rating/…
.formElement("yes_no_toggle")  // BOOLEAN → yes_no_toggle / yes_no_checkbox
.formElement("dropdown")       // SELECT → dropdown / radio_group
.formElement("measurement")    // RAW_JSON → measurement/address_group/geo/object
.formElement("formFileValue")  // FILES → file upload

Complex Data Fields

bd.measurement(options) — Explicit physics metrics compiler block:

memoryBusSpeed: bd.measurement({
  category: "storageSpeed",
  unitPicker: "fixed",
  default_unit: "MHz",
  resolve_to_your_unit: "MT/s",
  allow_decimals: true,
}).required()

Supported categories: sound, pressure, torque, speed (incl. Mach, ly/yr), time, area, volume, power, energy, storage, storageSpeed, frequency, temperature, mass, length ( incl. µm, nm), currency.

bd.object(properties) — Structural nested JSON with storage strategy:

metadata: bd.object({
  sku: bd.text().required(),
  warehouse: bd.text(),
}).strategy("flattened")  // or "jsonb" | "separateTable" (spawns 1:1 side-table)

All RAW_JSON object elements support JSONB output, not only bd.object():

shippingAddress: bd.address().strategy("jsonb"),
availability: bd.json().formElement("availability").strategy("jsonb"),
bookingWindow: bd.json().formElement("date_range").strategy("jsonb"),
coordinates: bd.geo().strategy("jsonb"),

strategy and separateTableName are included in toJSONSchema().

bd.file(options) — File uploads:

architectureSpecs: bd.file({
  max_size_mb: 15,
  allowed_mimetypes: ["application/pdf"],
}).optional()

Optional Schema Boundaries

SchemaBoundary() is feature metadata, not form layout. It accepts collections or nested boundaries directly and does not require a step, wizard, repeater, or group:

const projects = collection("projects", {
  fields: { title: bd.text().required() },
});

const direct = createFormSchemaOrchestrator([projects]);

const tagged = createFormSchemaOrchestrator([
  SchemaBoundary(
    { featureId: "projects", name: "Projects" },
    projects,
  ),
]);

Both declarations compile the same collection. The second additionally sets tagged.featureTags.project to "projects".

High-Utility Form Layout Containers

Layout containers are optional semantic wrappers. Add them only when the UI needs steps, conditional visibility, repetition, or branching.

FormStep(name, options, ...children)

Wraps schema portions into sequential steps for multi-step wizards:

FormStep("taxonomy_setup", {
  showTitle: true,
  helperText: "Configure high-level project taxonomy.",
},
  collection("categories", { fields: { name: bd.text().required() } }),
  collection("feature_groups", { fields: { name: bd.text().required() } }),
)

ConditionalBlock(config, ...children)

Dynamically toggles visibility based on matching logic gates:

ConditionalBlock(
  {
    matchStrategy: "AND",
    showTitle: true,
    rules: [
      { fieldRef: "project.isActive", op: "equals", values: [true] },
    ],
  },
  collection("features", { fields: { name: bd.text().required() } }),
)

Supports matchStrategy: 'AND' | 'OR' and operators: equals, not_equals, any_of, none_of.

MultiPopulator(config, ...children)

Generates an N+1 array row repeater grid. A single child collection is inferred as a secondary-table output:

MultiPopulator({ max_entries: 5, button_label: "➕ Register Node" },
  collection("hardware_nodes", {
    fields: {
      nodeSerial: bd.text().required().private(),
      memoryBusSpeed: bd.measurement({
        category: "storageSpeed",
        unitPicker: "fixed",
        default_unit: "MHz",
        resolve_to_your_unit: "MT/s",
        allow_decimals: true,
      }).required(),
    },
  }),
)

To keep the repeated array on a parent record instead, bind the repeater to a JSONB field and declare the shape of one entry:

const projects = collection("projects", {
  fields: {
    name: bd.text().required(),
    teamMembers: bd.array().strategy("jsonb"),
  },
})

createFormSchemaOrchestrator([
  projects,
  MultiPopulator({
    max_entries: 10,
    output: {
      strategy: "jsonb",
      fieldRef: "project.teamMembers",
    },
    entryFields: {
      name: bd.text().required(),
      role: bd.text().optional(),
    },
  }),
])

The orchestrator rejects missing targets, scalar JSONB targets, fields without .strategy("jsonb"), and ambiguous implicit secondary-table outputs.

OrCondition(options, branches)

Multi-path router layout engine for custom branching (tabbed selections):

OrCondition({ showTitle: true, button_label: "Select Target Pathway" }, [
  {
    name: "Hardware Validation Path",
    helperText: "Provisions system telemetry benchmarks.",
    nodes: [
      MultiPopulator({ max_entries: 5, button_label: "➕ Register Node" },
        collection("hardware_nodes", { fields: { /* ... */ } }),
      ),
    ],
  },
  {
    name: "Standard Baseline Deployment Only",
    nodes: [ /* ... */ ],
  },
])

Compile-Time Type Safety & Form Alignment

The SDK leverages TypeScript type inference to turn common engineering mistakes into instant IDE compile errors.

Invalid Field Refs

A typo inside a conditional block path (e.g. projects.isActveeee) fails validation at orchestration time:

⚠️ [BusinessDash SDK] Conditional rule references field "isActveeee"
   in collection "projects" which does not exist.

Data Type Mismatches

Conditional fieldRef values are validated when orchestration runs. The current public type is string, so invalid collection or field paths are runtime alignment errors rather than template-literal compile-time errors.

Implicit Key Generation

Primary keys are optional. The orchestration engine pre-scans collections, automatically injecting type-safe sequential integer or secure random uuid primary identifiers.


Unified Schema & Form Orchestration Blueprint

The complete declaration — combining database layers, field constraints, RLS policies, and layout:

import {
  collection, bd, Enums,
  createFormSchemaOrchestrator,
  layoutContainers,
} from "@businessdash/sdk";

const { FormStep, ConditionalBlock, MultiPopulator, OrCondition } = layoutContainers;

export const ProjectStatus = Enums(["planned", "in_progress", "beta", "completed"] as const);

export const applicationMasterContext = createFormSchemaOrchestrator([

  FormStep("taxonomy_setup", {
      showTitle: true,
      helperText: "Configure high-level project taxonomy and categorization mappings.",
    },
      collection("categories", {
        fields: {
          name: bd.text().formElement("short_text").required()
            .validate({ minLength: 3 }).helper("Category name."),
          icon: bd.text().formElement("short_text").optional(),
        },
      }),

      collection("feature_groups", {
        fields: {
          name: bd.text().formElement("short_text").required(),
          category_id: bd.relation()
            .references("categories").onDelete("CASCADE").required(),
        },
      }),
    ),

  FormStep("feature_engineering", { showTitle: true },

      collection("features", {
        rlsPolicies: [
          { roles: ["authenticated"], actions: ["read"], expression: "owner_id = auth.uid()" },
        ],
        fields: {
          name: bd.text().formElement("short_text").required().regex(/^[A-Z][a-z]+$/),
          status: bd.text().formElement("dropdown").required(),

          estimatedBudget: bd.number().formElement("currency")
            .check("estimated_budget >= 0").validate({ round: 2 }),

          feature_group_id: bd.relation()
            .references("feature_groups").onDelete("CASCADE").required(),

          related_features: bd.relation().belongsToMany("features", {
            joinCollectionName: "feature_references",
            fields: {
              isBidirectional: bd.boolean().formElement("yes_no_toggle").default(false),
            },
          }),
        },
      }),
  ),

  FormStep("routing_and_sandbox_studio", { showTitle: true },

      OrCondition({ showTitle: true, button_label: "Select Target Pathway" }, [
        {
          name: "Hardware Validation Path",
          helperText: "Provisions system telemetry benchmarks using physical metrics components.",
          nodes: [
            MultiPopulator({ max_entries: 5, button_label: "➕ Register Network Hardware Node" },
              collection("hardware_nodes", {
                fields: {
                  nodeSerial: bd.text().formElement("short_text").required().private(),
                  memoryBusSpeed: bd.measurement({
                    category: "storageSpeed",
                    unitPicker: "fixed",
                    default_unit: "MHz",
                    resolve_to_your_unit: "MT/s",
                    allow_decimals: true,
                  }).required(),
                  architectureSpecs: bd.file({
                    max_size_mb: 15,
                    allowed_mimetypes: ["application/pdf"],
                  }).optional(),
                },
              }),
            ),
          ],
        },
        {
          name: "Standard Baseline Deployment Only",
          nodes: [],
        },
      ]),
  ),

], {
  defaultIdType: "uuid",        // Auto-injects unique string identifiers for missing primary keys
  consecutive: true,            // Forces strict wizard sequence stepping rules
  progress_style: "progress_bar", // Instructs the layout renderer to generate progress indicators
});

Deployment Validation Lifecycle

When createFormSchemaOrchestrator compiles, it parses the layout tree and splits it into two isolated channels:

  • databaseSchema — A pure array of raw collection models, stripped of UI components, ready to stream to Prisma, Drizzle, or raw storage pipelines.
  • uiFormTree — The complete visual rendering graph mapping steps, multi-populators, validation constraints, and helper text fields for your form engine.

If a layout node points to an unmapped collection or field, orchestration stops with a focused diagnostic:

[BusinessDash SDK] Conditional rule references field "isActive"
in collection "project" which does not exist.

AI Distribution — llms.txt & Product Feed on Your Domain

Orgs curate products/services for answer engines under Dashboard → Marketing → AI Distribution; the platform keeps two public artifacts current per site — an OpenAI merchant-feed-shaped product feed and an llms.txt. The llms.txt convention only works at the site's own root (an AI crawler asks for https://yoursite.com/llms.txt), while the source of truth lives on BusinessDash where the org curates it. @businessdash/sdk/distribution proxies the two together:

// app/llms.txt/route.ts
import { llmsTxtHandler } from '@businessdash/sdk/distribution'

export const GET = llmsTxtHandler({
  siteId: process.env.BD_SITE_ID!,
  baseUrl: process.env.NEXT_PUBLIC_BD_APP_URL!, // e.g. https://www.biab.app
})
  • llmsTxtHandler(options) serves the curated llms.txt from your own domain root, with a 5-minute s-maxage + stale-while-revalidate. An upstream 404 (feed disabled or entitlement lapsed) passes through untouched; network failure returns a plain 503 — it never throws into the framework.
  • productFeedUrl(options) / llmsTxtUrl(options) build the public BD feed URLs. The product feed needs no proxy — submit its BusinessDash URL directly to merchant/feed programs; productFeedUrl builds it for display/submission.

Pair the feed with the product() JSON-LD builder from @businessdash/sdk/seo: the feed gets your product INTO the engines' catalogs; the markup is what their crawlers verify on the landing page itself.

MCP Connector on Your Domain

The platform's host-resolved /api/mcp only exists on sites the platform serves. If you host your own app with the SDK, mount the two @businessdash/sdk/mcp handlers and your domain gains the same connector surface — so the URL an org hands to Claude / ChatGPT / Gemini is their own site:

// app/api/mcp/route.ts
import { mcpHandler } from '@businessdash/sdk/mcp'
export const { POST, GET } = mcpHandler({
  siteId: process.env.BD_SITE_ID!,
  baseUrl: process.env.NEXT_PUBLIC_BD_APP_URL!, // https://www.biab.app
})

// app/.well-known/mcp.json/route.ts
import { mcpManifestHandler } from '@businessdash/sdk/mcp'
export const GET = mcpManifestHandler({
  siteId: process.env.BD_SITE_ID!,
  baseUrl: process.env.NEXT_PUBLIC_BD_APP_URL!,
})
  • mcpHandler(options) — POST proxies JSON-RPC to the platform's public per-site connector; GET mirrors the spec's 405. An unreachable upstream answers with a proper JSON-RPC -32603 error and a 502.
  • mcpManifestHandler(options) — serves .well-known/mcp.json with the endpoint URL rewritten to YOUR domain (the requesting origin).
  • Thin by design. The body is forwarded verbatim and the platform still enforces the org's MCP opt-in and per-tool write gates — a proxy can't widen anything.

AI-Crawler Visibility (AEO Analytics)

AI crawlers (GPTBot, ClaudeBot, PerplexityBot, …) fetch your HTML without executing JavaScript, so <BDAnalytics /> never sees them. Your server does — report them from middleware via @businessdash/sdk/analytics-server:

// middleware.ts
import { isAiCrawlerUserAgent, reportAiCrawlerHit } from "@businessdash/sdk/analytics-server";

export function middleware(request: Request) {
  const ua = request.headers.get("user-agent");
  if (isAiCrawlerUserAgent(ua)) {
    // Fire-and-forget — never block or fail the crawler's request.
    void reportAiCrawlerHit({
      siteId: process.env.BD_SITE_ID!,
      baseUrl: process.env.NEXT_PUBLIC_BD_API_URL!,
      apiKey: process.env.NEXT_PUBLIC_BD_PUBLISHABLE_KEY!,
      userAgent: ua,
      path: new URL(request.url).pathname,
    });
  }
}

Read it back with bd.site(siteId).analytics.aiVisibility({ days: 30 }) — per-crawler fetch counts, distinct pages read, vendor, intent (training / search / user), and last-seen timestamps. Branch on .available; same plan gating as pageViews().


Entrypoints

| Import path | Contents | |---|---| | @businessdash/sdk | Main barrel — all schema, layout, client, auth, SEO exports | | @businessdash/sdk/contracts | Zod schemas + inferred types for every API surface | | @businessdash/sdk/enums | Enum helpers — label formatting, enum entry utilities | | @businessdash/sdk/collections | Schema primitives — collection(), createSchema(), bd, Enums(), SchemaBoundary | | @businessdash/sdk/forms | Form layout + runtime — layoutContainers, createFormSchemaOrchestrator(), createSingleResourceForm(), controller, formula engine | | @businessdash/sdk/static | Static data — Enums(), enum types | | @businessdash/sdk/react | React bindings — the full UI surface: forms, chat, auth, storefront, cart, checkout, blog, customer portal, marketing pages, followers, social links, verification | | @businessdash/sdk/vue | Vue binding — <BdForm> + shared client | | @businessdash/sdk/svelte | Svelte binding — <BdForm> + shared client | | @businessdash/sdk/solid | Solid binding — <BdForm> + shared client | | @businessdash/sdk/qwik | Qwik binding — <BdForm> | | @businessdash/sdk/angular | Angular binding — <bd-form> component + shared client | | @businessdash/sdk/element | <bd-form> custom element — drop-in web component | | @businessdash/sdk/vanilla | Framework-free imperative DOM binding — mountBdForm() | | @businessdash/sdk/react-analytics | <BDAnalytics /> — browser page-view tracker for React | | @businessdash/sdk/react-attribution | "Powered by BusinessDash" attribution badge | | @businessdash/sdk/analytics-core | Framework-agnostic analytics core — initBdAnalytics() | | @businessdash/sdk/analytics-server | Server-side AEO reporting — isAiCrawlerUserAgent(), reportAiCrawlerHit() | | @businessdash/sdk/distribution | llms.txt proxy + feed URLs — llmsTxtHandler(), productFeedUrl(), llmsTxtUrl() | | @businessdash/sdk/mcp | MCP connector proxy — mcpHandler(), mcpManifestHandler() | | @businessdash/sdk/proxy | BdApiClient / customer-portal client re-exports for proxy setups | | @businessdash/sdk/seo | JSON-LD builders — localBusiness(), product(), … | | @businessdash/sdk/data-model-schema | defineDataModel — org custom database (tables + enums) as code | | @businessdash/sdk/alpha | Legacy/deprecated pre-0.9 schema API, re-exported for compatibility | | @businessdash/sdk/marketing-schema | defineSiteMarketingSchema() — schema-driven marketing-page flow | | @businessdash/sdk/next/revalidate | One-line Next.js route handler for BusinessDash revalidation webhooks | | @businessdash/sdk/adapters/revalidate | Framework-agnostic revalidation handler builder | | @businessdash/sdk/bd-forms.css | Default form stylesheet |

Framework parity — read this before picking a stack

Forms work identically on all eight bindings, and every binding renders them natively — real components in your own DOM, styled by your own CSS.

Beyond forms, two questions matter: does the binding have the surface at all, and does it render into your DOM or into an iframe?

| Capability | react | solid | vue | svelte | qwik | angular | vanilla | element | |---|---|---|---|---|---|---|---|---| | Forms | native | native | native | native | native | native | native | native | | Shared client | native | native | native | native | — | native | — | native | | AI chat | both | both | native | native | native | native | native | native | | Storefront | both | both | native | native | native | native | native | native | | Cart | both | both | native | native | native | native | native | native | | Blog | both | both | native | native | native | native | native | native | | Checkout | both | both | native | native | native | native | native | native | | Customer portal | both | both | native | native | native | native | native | native | | Customer auth | native | native | native | native | native | native | native | native | | Followers | native | native | native | native | native | native | native | native | | Marketing pages | native | native | native | native | native | native | native | native | | Social links | native | native | native | native | native | native | native | native | | Email verification | native | native | native | native | native | native | native | native | | Phone verification | native | native | native | native | native | native | native | native | | Surfaces | 14 | 14 | 14 | 14 | 13 | 14 | 13 | 14 |

iframe means a BusinessDash-hosted page in a sandbox. It works and it ships fast, but it renders our markup, not yours.

Two things worth reading against the obvious summary:

Every capability is available on every binding, via ten shared cores (forms, store, chat, blog, checkout, account, portal, marketing, verify, social) — all as data you render yourself. React and Solid also offer the hosted iframe widgets, hence both.

The only — cells are the shared-client primitive on qwik and vanilla, and neither needs one: both take the client per form instead.

The customer portal is the largest surface — 34 methods. sessionToken is required (without one it renders empty rather than signed-out); there are no list endpoints for invoices, quotes, contracts or shipments, so work carries the counts and you open each by id; and a submitted review is queued for staff approval, so watch reviewAwaitingModeration rather than expecting it in reviews.

React remains ahead on <ChatbotInline> — a rendered chat UI over the same shared controller.

For anything a binding lacks, BdClient from the root export is framework-agnostic: client.storefront(), client.cart(), client.customerPortal() and the rest work anywhere JavaScript runs. You write the markup; the data layer is already there.

React-compatible targets get the full surface, because they render React: Next.js (App Router included — the adapter ships its own "use client" boundary), Astro via @astrojs/react, Remix / React Router 7, TanStack Start, and Preact via the standard preact/compat alias all import @businessdash/sdk/react. React Native uses @businessdash/sdk/native.

The matrix lives in @businessdash/sdk/framework-support as data, and release gates assert that the code, the manifest and the published docs table all agree — including the level, so an iframe can never be published as if it were a native component.

Releases

Versioning convention (0.9.50+): Significant feature groups bump the minor field directly. Previous releases used the 36-band patch series (0.9.36, 0.9.35…). The 0.9.35 docs are archived at docs-legacy/0.9.35/ — this is the first supported SDK baseline. 0.9.50 introduces the unified Schema, Validation, and Form orchestration pipeline with createFormSchemaOrchestrator, layoutContainers, and the full bd field builder namespace.

0.8.x = schema-driven marketing flow. Major surface bump (still alpha). New entrypoints: defineSiteMarketingSchema(), @businessdash/sdk/seo for typed JSON-LD builders, and a bd CLI.

0.9.x = full programmatic consumer surface. The largest expansion yet: native storefront / cart / checkout / coupons / subscriptions, customer portal + tenant auth (createAuthHandler, getTenantSession, <SignIn/> <SignUp/> <SignOut/> useUser), blog, a paginated reviews wall, address autocomplete + shipping, programmatic local SEO (defineParallelPage()), the revalidation webhook channel (@businessdash/sdk/next/revalidate + @businessdash/sdk/adapters/revalidate), privacy-conscious analytics (@businessdash/sdk/react-analytics + @businessdash/sdk/analytics-core), and a three-state billing-lifecycle degradation contract. Detailed per-version notes below.

The 3 most recent releases are below. For the complete release history, see the changelog.

0.9.90

Every Biab* export is now Bd* — a breaking rename with no aliases

biab was the old application name (Business In A Box). The product is BusinessDash, and the old name survived across the SDK's whole public surface long after that stopped being true.

Renamed as a clean break rather than with a deprecation layer, because nothing outside this repo consumes these names yet. Waiting would only have made the alias layer larger.

BiabDevClient          -> BdApiClient      (the HTTP transport)
BiabClient             -> BdClient         (the composed client interface)
BiabForm, BiabProvider -> BdForm, BdProvider
BiabApiError and every other error class
createBiabClient, initBiabAnalytics, mountBiabForm, getBiabEnv, …
.biab-* CSS classes    -> .bd-*            (54 of them)
BIAB_* env vars        -> BD_*
@biab-dev/* packages   -> @businessdash/*

Two names needed a decision rather than a pattern. BiabClient (the interface a consumer holds) and BiabDevClient (the HTTP transport under it) would both have become BdClient; the interface keeps the plain name and the transport is now BdApiClient.

Upgrading. Every rename is mechanical — a find-and-replace of Biab to Bd and biab to bd covers all of it except BiabDevClient, which becomes BdApiClient. If you style the form components, the CSS hooks moved from .biab-* to .bd-* in the same way.

./biab-forms.css still resolves. The stylesheet subpath is exported under both names, so a stylesheet import does not have to move in lockstep with the code.

Three things deliberately did NOT change. biab.app is the API host and is served forever. The biab.app/schemas/* URLs are JSON-Schema $ids — identifiers rather than addresses — so changing one would break validation for anything pinned to it. And the historical note that this was formerly Business In A Box stays true.

Proposals: the document a customer is sent, not just the quote

customerProposalSchema, listProposals() and getProposal().

A proposal is the document built around a quote — either a rendered PDF or a live page — and kind tells you which, so a UI can offer a download or frame a page rather than guessing and being wrong half the time. Only rendered proposals appear: before that it is a rep's working copy, possibly at a price nobody has agreed, and a customer refreshing the list should not watch it take shape.

It carries no template, bindings or resolved snapshot. Those describe how an org builds proposals; a customer's client has no business reading them.

The customer quote detail carries the proposal narrative

customerQuoteDetailSchema was dropping sections — the cover letter, scope of work and terms that make a quote a proposal — so a consumer could fetch one and receive only line items. It now carries them, along with the decision state (canAccept, expired, and the sent/accepted/rejected timestamps) so a client does not have to re-derive whether accepting is still possible.

items was z.array(z.unknown()). It is typed now: no consumer should have to guess a line item's shape before rendering a row.

React's marketing pages run on the shared core

useMarketingPage, useMarketingPagesList and useMarketingPagePreloader reimplemented the fetch, the status flag and the in-flight guard that marketing-core already had, so a fix in the core never reached React. They are thin shells over createMarketingController now, with the snapshot-to-view derivation moved into the core as marketingPageView() and marketingListView() for every binding to share.

Public signatures are unchanged. The synchronous cache read stays in React, because the controller does not duplicate the client's cache and without that read an already-cached page flashes "loading".

0.9.81

The createBdClient facade serves the legal and sitemap routes

The facade lacked legal and site, so any starter handing it to resolveLegalPage() or buildSitemap() failed to typecheck against the published package — the template fleet caught T3-App, Qwik and Svelte on 0.9.80. client.legal and client.site(siteId) now exist on the facade, mirroring the raw BdApiClient, and compile-time contract guards in sdk.ts fail typecheck if a helper-required surface is ever dropped again.

Sitemaps actually include blog posts and products now

SitemapClientLike described responses as { posts } / { products }, but the real client serves { items } — the collector's destructure came back undefined, the loop threw, and every real sitemap silently reported those sections as "unreachable". The contract and collector now consume the shapes the API actually serves.

Shipped source compiles on consumers' older TypeScript

push-core used the Uint8Array<ArrayBuffer> generic, which only parses on TS 5.7+ — consumers whose toolchains typecheck the shipped source (Qwik's does) failed to build. The annotation is gone; the one BufferSource call site casts instead.

0.9.80

Tracking consent — @businessdash/sdk/consent

There was no consent mechanism anywhere: initBdAnalytics() started unconditionally and analytics, attribution and AEO all collected regardless.

loadConsent() returns what a visitor currently allows, at THIS org, and recordConsent() stores their answer. Gate analytics on it:

const consent = await loadConsent({ client })
if (consent.allows('analytics')) initBdAnalytics({ siteId, baseUrl, apiKey })
if (consent.mustAsk) showYourBanner(consent)

initBdAnalytics is deliberately NOT made to call this itself — a gate that fires implicitly is one nobody can see in review, and what happens before consent (render nothing, render a placeholder, collect essential-only) is the site's decision, not ours.

Consent is per-org and never travels. A visitor who refuses at org 2 has said nothing to org 1, and one who consents at org 1 has NOT consented to org 2 — that second half is the one with legal teeth. The visitor key lives in a first-party cookie on the org's own domain, so two sites cannot see each other's, and the isolation is the browser's rather than something we remember to scope.

The gate fails closed. Any failure returns deny-everything: a gate that fails open collects data nobody agreed to, and only one of those outcomes is an incident.

Also ships client.consent.get() / .record() as the transport underneath, and browserOptOut() for DNT and Global Privacy Control.

Legacy Docs

The 0.9.35 docs are archived at docs-legacy/0.9.35/.