Changelog
The complete release history of @biab-dev/sdk. The current published version is 0.9.34.
Versioning convention. The patch field's tens digit identifies the
feature group (e.g. 0.7.5x = chatbot work). Hot-fixes within a group stay
in the same band — 0.7.51, 0.7.52, … — so consumers on ^0.7.5 pick
them up automatically; new features bump the next band.
0.9.x — Full programmatic consumer surface
0.9.34
-
Read your visitor counts back. Analytics used to be write-only — the
<BIABAnalytics/>tracker recorded pageviews and you could only see the totals in the dashboard. Newclient.site(siteId).analytics.pageViews()returns them:// Site-wide, all time const all = await biab.site(siteId).analytics.pageViews() if (all.available) console.log(`${all.total.views} views`) // A batch of pages in one round-trip (e.g. a view count per portfolio item) const each = await biab.site(siteId).analytics.pageViews({ paths: ['/work/atrium', '/work/harbor'], days: 30, })Pass
pathsfor a per-path breakdown (every requested path present,0if unvisited) plus a combinedtotal; omit them for a site-wide total.dayswindows it.visitorsapproximates unique visitors (distinct daily-rotating anon id);viewsis the exact pageview number. Branch on.available— visitor analytics is a plan feature. -
“Powered by BusinessDash” footer, now in the SDK. New export
@biab-dev/sdk/react-attribution:import { BusinessDashFooter } from '@biab-dev/sdk/react-attribution' // In a Server Component — reads your plan's policy and renders accordingly <BusinessDashFooter client={biab.site(SITE_ID)} companyName="Acme Co" />BusinessDashFooterBanneris the presentational badge (passsiteId);BusinessDashFooteris a policy-aware Server Component. The badge carries abiab.app/?ref=<siteId>backlink — server-render it so it's visible.client.site(id).attributionexposes the policy read + an (optional) heartbeat. On Launch the badge is required; Growth/Scale may remove it — or keep it for a discount, toggled from Settings → Billing. -
sync-recordsshows live progress. A large seed used to sit silent between “Discovered N rows” and the final tally. It now prints a progress line (rows done, running created/updated) — a single updating line in a terminal, a bounded trail in CI.
0.9.33
-
Read your custom database back. The data model used to be write-only from the SDK — you could push a schema and seed rows, then never read either. New
client.site(siteId).dataModel.listRecords({ object })returns a page of a custom object's rows, newest first, keyset-paginated (nextCursor);listAllRecords()walks every page for you. Relations come back expanded as links —record.relations.<field>is[{ recordId, object }], since a relation value lives in a link table, not on the row. Every declared relation field is present,[]when it has none, so an empty relation is never mistaken for a missing one.Not the same as
collections/rows, which read Site Data. Rows seeded withsync-recordslive in the custom database and are reachable only throughdataModel. -
visibilityper object — decide who can read a table.defineObject()takes an optionalvisibility:private(default) — your server only, with a secret key.authenticated— any signed-in customer (tenant auth).public— anyone, from a publishable (pk_…) key in page JS.
So a table you mark
publicis readable straight from the browser; a private one never is. Because it's part of your model, opening a table is a reviewed change — a push shows it on the dashboard's Review tab as an exposure, confirmed separately from destructive changes, and a human promotes it. You can also flip it from Site Builder → Site Data → Database → Schema.⚠️
authenticatedmeans every signed-in customer can read every row of that table. It is not per-customer ownership — never use it to keep one customer's data away from another's. -
sync-data-modelplans now flag exposure. Opening a table to a wider audience shows up as its own EXPOSURE group in the printed plan, distinct from destructive changes — a change that loses no data but can't be undone once the data's been read shouldn't read as "you might lose rows". -
New scope
metadata:read_records. Reads from the custom database need it. Scopes freeze when a key is minted, so if you get "Missing scope metadata:read_records", issue a fresh key from Site Builder → Developer — it grants every org-safe scope.
0.9.32
- Corrected the dashboard locations the CLI prints.
sync-data-modelpointed at "CRM → Settings → Data Model", which was wrong twice over: the review + promote surface moved to Site Builder → Site Data → Database (beside Schema, since both are artifacts pushed from a repo for a human to review), and "Static Data" is now Site Data.sync-schema/sync-contentupdated to match.
0.9.31
-
CLI docs fix —
npx biab-dev, notbiab-dev. The help text and docs showed the bare command as though it were globally installed. It's a package bin: run it vianpx biab-dev <command>(orpnpm exec). -
Point
BIAB_PACKAGE_API_BASE_URLat the APEX host.https://www.biab.app307-redirects tohttps://biab.app, andfetchdrops theAuthorizationheader across that cross-origin hop — so the server reports "Missing bearer API key" as if the key were absent, when it never arrived. Usehttps://biab.app. Now called out in--help. -
Note on scopes: they're frozen when a key is minted. A key issued before the
metadata:*scopes existed reports "Missing scope metadata:read_schema" — mint a fresh one from Site Builder → Developer.
0.9.30
-
Data model as code — declare your custom database in your repo. New
defineDataModel()/defineObject()/defineField()(from@biab-dev/sdkor@biab-dev/sdk/data-model-schema) let you declare an org's custom tables, fields, and enums in abiab.data-model.config.ts, push them from CI, and promote them from the dashboard. Deliberately separate from the marketing flow — its own config file, commands, endpoint, and API scopes, so a key for one can never reach the other. -
Four new CLI commands.
biab-dev view-data-model— what's live in BIAB right now. Not "what I last pushed": the dashboard writes this model too, so the two drift.--jsonfor CI,--difffor the migration plan without pushing,--outto scaffold a config from an existing dashboard-built model.biab-dev print-data-model— what your local config resolves to. No network.biab-dev sync-data-model— push to the draft slot. Prints the migration plan first;--dry-runprints it and stops;--pruneopts into full reconciliation.biab-dev sync-records— upsert rows from JSON (--records-dir, default./biab-records). Keyed on row identity, so re-running converges instead of duplicating.
-
A key proposes, a human promotes. Pushing never changes your database. It writes a draft; promoting is a dashboard action behind a separate permission. That's what makes it safe to hand CI a push key — and it's why the CLI has no promote command.
-
The migration plan. Every change is classified before it applies:
safe(no data touched),backfill(rewrites stored rows),destructive(may lose or invalidate data — needs an explicit confirmation). Knowing what a promote will do before you do it is the point of the feature. -
Identity over names. Objects and fields carry a
universalIdentifier, so renaming a field is a rename — one backfill — rather than a drop-and-recreate that quietly destroys a column of data. -
Enums with optional identity.
SelectOptiongains an optionaluniversalIdentifier. Supply it and renaming an option'svalueis understood as a rename and backfilled; omit it and behavior is unchanged (a rename reads as remove-plus-add). Additive — nothing existing breaks. -
Relations that propagate.
RELATIONfields take arelationTarget(another object'suniversalIdentifier) and anonDeletepolicy (restrict— the default — /setNull/cascade). Links store the target's id rather than a copy, so changing the original is reflected everywhere with nothing to sync. -
New scopes:
metadata:read_schema(view + plan),metadata:write_schema(push a draft),metadata:write_records(seed rows). None of them can promote.Docs:
/docs/developer/byFeature/custom-db.
0.9.20
- Invoice payment flow — confirmation number, receipt PDF, richer reads.
customerPortal().getInvoice(id)now also returnspayments[](each withamount/method/date/reference/status),confirmationNumber(the most recent completed payment reference — the Stripe PaymentIntent id for cards), andpdfPath. NewcustomerPortal().downloadInvoicePdf(id)returns the invoice as a PDF — decodedbytesplusfilename/contentType; once paid it carries a PAID marker and a payment-history summary. This closes the email → pay → dashboard/confirmation → download loop end to end (the paid confirmation email, with the confirmation number, is still sent automatically by BIAB). See the customer-portal docs for the full walkthrough + edge cases (failed, refund, dispute). - Return / refund requests (
ecommerce.shipping). NewcustomerPortal().submitReturnRequest(orderId, { reason?, customerNote?, items? })opens a return/refund request the org reviews; the org's approval (not the customer) triggers the Stripe refund + refund notifications. The order detail (getOrder) now includesreturnRequests[]so the customer can track status (requested → approved → refunded/denied). - Digital-goods downloads. New
customerPortal().getDownloads(orderId)returns one entitlement-checked link per paid line whose product has a file attached — a short-lived presigned URL for BIAB-hosted files (expiresInSecondsset) or the org's external link. Empty until the order's payment settles.
0.9.19
-
Chat file uploads (
chat.file_uploads). Visitors can attach files in the chatbot.getConfig()now returnsfileUploadsEnabled(true when the org'schat.file_uploadsentitlement is active) andemailCaptureMode("off" | "request" | "require"). Newclient.chatbot.uploadFile({ sessionId, file })presigns a direct-to-R2 PUT (7 MB cap, 5 files/hour per session) and returns a{ url, name, type, size }ref; the file lands in the org'schatUploadsFiles & Media folder (2-day TTL unless the org keeps it). Also surfaced on theuseChatbot()hook. -
<ChatbotInline>gained an upload affordance + email-capture gate. WhenfileUploadsEnabled, it renders a 📎 button;emailCaptureModedrives an email ask ("require"blocks the first message,"request"is skippable). Uploaded files render as a file bubble. All overridable via the existingclassNames/renderMessageAPI. The<Chatbot>iframe drop-in already supports uploads via the hosted embed. -
<ChatbotInline>is now a polished drop-in, not just headless. The inline (non-iframe) chatbot ships a built-in default theme — message bubbles, a shimmer "typing" indicator, styled input + inline form cards, and a scroll-aware bottom edge fade — plus a smarter scroll model matching the iframe<Chatbot>: it follows the live edge only while you're at the bottom (no more yanking you down while you read), anchors each new turn near the top with a peek of the previous message, and shows a "↓ Latest" jump button when you've scrolled away.- Still fully yours to style. Defaults are injected as zero-specificity
:where()rules keyed to per-slotdata-slotattributes, so anyclassNamesslot you pass already overrides them — no!importantneeded. Accent + surface colors read from the--biab-chatbot-accent/--biab-chatbot-bgCSS variables. Newunstyledprop renders the previous bare-div baseline; newjumpButtonclassName slot styles the jump control. - No API breaks.
classNames,renderMessage,renderPresenceStrip, and theuseChatbot()hook are unchanged; reactions remain internal-only and are not part of the consumer chatbot. The iframe<Chatbot>already gets the matching UI via the hosted embed.
- Still fully yours to style. Defaults are injected as zero-specificity
0.9.18
- Canonical social-profile support.
storefrontMeta().socialsis a free-form record keyed by BIAB's exact field keys (meta,twitterX,tikTok, …), so consumers kept hand-mapping keys→icons and silently dropping platforms (e.g. mappingfacebook/xand missingmeta/twitterX). NewresolveSocialProfiles(socials)SOCIAL_PLATFORMSregistry are the single source of truth: every platform in render order, with a stable simple-icons slug, handle→URL normalizer, and alias resolution. Also exportsresolveSocialPlatform,socialHref, and theSocialPlatformKey/SocialPlatformMeta/ResolvedSocialProfiletypes.
<SocialLinks>(@biab-dev/sdk/react). Drop-in that renders an icon + link for every configured profile (branded glyphs via the simple-icons CDN; a globe fallback for platforms without one).<SocialLinks socials={meta.socials} color="white" />.
0.9.17
- Blog post likes from consumers.
client.blog().withSession(token)now exposestoggleLike(slug)(POST) andlikeStatus(slug)(GET →{ liked, likesCount }), mirroringpostComment. Previously consumers could read like counts and read/post comments, but had no way to like a post — that was a platform-only mutation.likeStatusreports whether the session visitor has already liked, so you can render a filled heart on load.
0.9.16
- Inline chat-card forms now render nested inputs.
InlineFormCardrecurses into container field types (input_group,condition_block,or_condition,flex_row,multi_populator) so chat forms that group inputs render their leaf fields instead of nothing. (Previously a consumer-side patch; folded into the SDK so every consumer gets it — no patch needed.) - Products can carry their resolved category. Storefront product rows, cards,
and detail now accept an optional
category { id, name, description }alongsidecategoryId, so a storefront can label/group products without a secondlistCategories()join. (listCategories()and thecategoryIdfilter are unchanged.) - Reminder: live staff presence is already available via
client.chatbot.getAvailability()→{ staffAvailable, onlineHandlers, … }.
0.9.15
-
Payment-method brand icons — framework-agnostic. The SDK now ships the SVG data for every payment method a Company Profile can enable, plus
paymentIconSvg(), which returns a ready-to-inject<svg>markup string that works in any framework — React (dangerouslySetInnerHTML), Vue (v-html), Svelte ({@html}), Angular ([innerHTML]), or the DOM (el.innerHTML).paymentIconData()returns the raw{ viewBox, paths }for full control.- Theming — the icon is "the color of the text" by default. It renders with
fill="currentColor", so it inherits the surrounding text color and adapts to light/dark automatically. Lock it permanently withtheme: "light" | "dark", or set any CSS color withcolor: "#1a1a1a"(wins overtheme). Text-only still works — just render the label and skip the icon. Unknown / custom ("additional") methods returnnullso you fall back to text. - Covers
cash,visa,mastercard,discover,americanExpress,personalCheck,paypal,venmo,cashApp,zelle,applePay. Icon art: Font Awesome Free (CC BY 4.0) + Simple Icons (CC0).
import { paymentIconSvg } from "@biab-dev/sdk"; el.innerHTML = paymentIconSvg("visa") ?? ""; // currentColor (light/dark safe) paymentIconSvg("applePay", { theme: "dark", size: 28 }); // forced light icon, 28px paymentIconSvg("zelle", { color: "#6d1ed4" }); // explicit brand color - Theming — the icon is "the color of the text" by default. It renders with
0.9.14
- Forms: "Live" is now decoupled from "Chatbot access." A form is reachable
over the SDK when it is Live (
isActive, the master switch).chatbotAccessis now purely additive — it only governs whether the AI chatbot may surface the form. You no longer have to opt a form into the chatbot just to embed it on your own page or share it.client.forms.schema(slug)/client.forms.submit(slug, …)(and the class-basedgetForm/submitForm) now fetch & submit over the non-chatbot/forms/{slug}path, gated on Live only. The file-upload, payment-intent, and schedule-slots sub-resources moved with them. No code change required — just make sure the form is Live. New publishable keys include the newforms:read/forms:submitscopes by default; reissue an older key if a fetch/submit returnsmissing_scope.- New
getChatForm/submitChatFormkeep the chatbot path (Live andchatbotAccess) for theshow_formchat flow —submitChatFormstill carries the chat transcript to the resulting CRM inquiry. The ReactuseChatbot()getForm/submitFormhelpers use these automatically.
- Per-form public link. A Live form can be published to a hosted, shareable
page at
{orgSlug}.<platform>/f/{formSlug}via a new "Public link" toggle in the form's settings (a third, independent axis from Live and Chatbot). No SDK call — it's a BIAB-hosted page you can link anywhere. ^0.9.12/^0.9.13consumers pick this up automatically; the behavior change is strictly a loosening (a form that worked before still works).
0.9.13
-
Completes the 0.9.12 release. 0.9.12 went to npm before its README finished documenting the storefront listing-with-meta surface and the cross-adapter form autofill, and before the final non-React autofill wiring landed. 0.9.13 re-publishes the complete set — the features themselves are described in the 0.9.12 notes below.
^0.9.12consumers pick this up automatically. -
Customer-portal org pin no longer asks for a provider-specific id. The proxy handler now takes
organizationId— your BIAB org id (theidfrom the customer-portal context) — instead of an auth-provider org id. The platform resolves theX-BIAB-Customer-Portal-Orgpin by BIAB id or the legacy id, so this is backwards compatible:export const GET = createCustomerPortalProxyHandler({ biabBaseUrl: "https://biab.app/api/package/v1", apiKey: process.env.BIAB_API_KEY!, - workosOrganizationId: process.env.WORKOS_ORG_ID!, + organizationId: process.env.BIAB_ORG_ID!, });The old
workosOrganizationIdoption still works (now deprecated). TheworkosOrganizationIdfield on the customer-portalorganization/ other-orgs payloads is likewise deprecated — readid/orgIdinstead. Internal auth-provider names were scrubbed from the public JSDoc. No runtime break; migrate whenever it's convenient.
0.9.12
-
Followers / subscribe from the browser — new
useFollowershook. A publishable token can now subscribe a visitor (newsletter / "follow"), read their status, edit, and unsubscribe — allfollowers:self, no backend:import { useFollowers } from "@biab-dev/sdk/react"; const f = useFollowers({ token: pk, siteId }); if (!f.subscribedLocally) { /* show the form */ await f.join({ email, source: "footer" }); } const { follower } = await f.me(user.email); // authoritative status (logged-in) await f.leave(email); // unsubscribeSource of truth for "already subscribed": logged-in → pass
user.emailtome()(authoritative, cross-device); anonymous →subscribedLocally, a per-browser flag the hook sets onjoin()(instant, no network, no PII). Email is the real key; the local flag is just a UX hint so an anonymous visitor isn't shown the form forever. Re-subscribing is idempotent. (Server:followers.joinmoved fromfollowers:write→followers:self, matchingfollowers.leave— self-service subscribe is a public action like form submit.) -
Storefront listing with facets, ratings, reviews, related & cross-sell. New
client.storefrontdata methods power a full platform-style shop (sidebar filters, star ratings, "you may also like", a companion-addon rail) without hand-rolling queries:// Faceted grid: filters + per-card price/ratings/badges + sidebar facets. const { items, categoryCounts, priceRange } = await client.storefront.listProductsWithMeta({ search, categoryId, minPriceCents, maxPriceCents, minRating, // 1..5 sort, // featured | newest | price-asc | price-desc | rating-desc }); await client.storefront.listCategories(); // sidebar categories await client.storefront.getProductReviews(id, { limit }); // { items, avgRating, totalCount, nextCursor } await client.storefront.getRelatedProducts(id, { limit }); // "you may also like" await client.storefront.getProductAddons(id); // companion / cross-sell railgetProduct(id)detail is enriched too — variants, per-image rows (images/variantImages), cross-variant pricing (crossVariantTitles/crossVariants), and structuredspecifications. Card prices are cents (cheapestPriceCents/comparePriceCents);categoryCountsis{ categoryId, count };priceRangeis{ minDollars, maxDollars }. -
<BiabForm>inputs now carry HTML autofill (autocomplete) tokens. Each field gets a sensible token inferred from its type and label/key (a "Full name" text field →name, email →email, phone →tel, address sub-fields →street-address/address-level2/address-level1/postal-code, …), so browsers offer autofill. Controls:<BiabForm slug="contact" token={pk} siteId={id} autoComplete={false} // global off → autocomplete="off" on all fieldAutoComplete={{ // per-field overrides (id → token|true|false) [companyFieldId]: "organization", [secretFieldId]: false, // off }} />Precedence: per-field override → schema token (
field.autocomplete) → globalautoComplete→ inference. Helpers exported from@biab-dev/sdk/forms(inferAutocompleteToken,resolveFieldAutocomplete). Works across every adapter — React, Vue, Svelte, Solid, Qwik, Angular, vanilla, and the<biab-form>custom element (which exposesauto-complete/field-auto-completeattributes) — all honoring the sameautoComplete/fieldAutoCompleteprops. -
biab-forms.cssnow styles the file-upload field and the multi-step progress header (both the numbered-steps and the bar variants), in addition to the date-range / slot pickers. Theme-aware (reads your shadcn/COSS vars, neutral fallbacks). Containers are intentionally background-less — add a surface fill in your own app if you want one; the SDK won't impose one. -
Fix:
<BiabForm>no longer hangs on "loading" under React StrictMode. The form controller'sload()bailed on adestroyedguard, but StrictMode runs effects mount → cleanup → mount — the cleanup destroyed the controller, so the remount'sload()returned early and the schema fetch's 200 was discarded, leaving the form spinning forever.load()now revives the controller (it's idempotent by contract). Affects any client-load path (publishable token or proxy) in dev; SSR/pre-fetched-schema was unaffected. -
Fix: browser publishable-token clients now default to the apex host. The default package-API base URL was
https://www.biab.app/…, butwww301/307-redirects to the apex (https://biab.app/…) and a CORS preflight does not follow redirects — so<BiabForm token siteId>failed withPreflight response is not successful. Status code: 307and spun forever. The default is now the apex in bothreact.tsx(DEFAULT_PACKAGE_BASE_URL) andsdk.ts(DEFAULT_BASE_URL). If you pinned abaseUrl/NEXT_PUBLIC_*base URL, point it at the apex too. No API changes.
0.9.11
-
Publishable tokens (
pk_…) — drop<BiabForm>on a public page with zero backend. A publishable token is browser-safe: it's origin-locked, rate-limited, and scoped to public read/submit actions only (never CRM writes or admin). Generate one in Site Builder → Developer → Publishable tokens, expose it as aNEXT_PUBLIC_*env, and pass it to<BiabForm>:import { BiabForm } from "@biab-dev/sdk/react"; export function QuoteForm() { return ( <BiabForm slug="service-quote" token={process.env.NEXT_PUBLIC_BIAB_PK!} siteId={process.env.NEXT_PUBLIC_BIAB_SITE_ID!} /> ); }Or set it once at the root and drop
<BiabForm slug="…" />anywhere below:import { BiabFormProvider, BiabForm } from "@biab-dev/sdk/react"; <BiabFormProvider token={process.env.NEXT_PUBLIC_BIAB_PK!} siteId={process.env.NEXT_PUBLIC_BIAB_SITE_ID!} > <BiabForm slug="service-quote" /> <BiabForm slug="send_us_a_message" /> </BiabFormProvider>No proxy route and no secret
BIAB_API_KEYin the browser.<BiabForm>calls BIAB directly; the browser sendsOriginautomatically, which satisfies the token's allowed-origin lock.baseUrldefaults to production — override only for staging. The secret-key path (createBiabClient/ same-origin proxy) still works for server-side use and write scopes.
0.9.10
-
Form-element refinements. New input types — URL (with link verification), multi-select, a date-range picker, and a color picker — plus validation and option upgrades to existing fields (extra
measurementcategories, business-only email, address ZIP/state handling, richer availability windows). All rendered + validated by<BiabForm/>. -
Auto-book scheduling (
schedulefield,mode: "auto"). Ascheduleelement can now book a real meeting on submit instead of only capturing availability.<BiabForm/>renders a live day/time slot picker (BiabScheduleAuto) that pulls open times from the form's assigned staff's real calendars via the SDK'sforms.scheduleSlots(slug, { fieldId, from, to })endpoint, and submits the chosen slot as{ slotStartAt, timezone }. On submit the meeting books server-side — calendar event, video link, and confirmation emails all fire. Gated by the org'sscheduling.calendlyentitlement; without it the field falls back to availability capture.import { BiabForm } from "@biab-dev/sdk/react"; // The schedule field renders as a slot picker automatically — no wiring. export default function BookPage() { return <BiabForm slug="book-a-consult" />; }The propose-back path (visitor states availability → staff offer up to 3 times → visitor confirms via a tokenized link) is handled entirely on the platform side; the same
schedulefield the SDK already renders captures the availability, and the confirm page is a platform-hosted link, so no extra consumer surface is needed. See the Form Scheduler guide.
0.9.9
- Two display elements (static — carry no submitted value, rendered by
<BiabForm/>with stable BEM classes):- Body Text (
type: "body_text") — a plain paragraph fromfield.content(biab-field__body). - Bullet List (
type: "bullet_list") —field.listConfigwithvariant: "bullets"(optionaltitle+items[].text) or"details"(one row per point with an optional Lucideiconvia adata-iconhook, atextheading, and adescription). Classes:biab-field__bullets/biab-field__points+…__point-icon/-title/-desc. - Table (
type: "table") —field.tableConfig(columnswithheader/type"text"|"check"/align,rows[].cells, optionalhighlightColumn). A "check" column renders ✓/– from a truthy/falsy cell. Rendered as a real<table>withdata-align/data-highlight/data-checkhooks underbiab-field__table. FormFieldDefnow also exposescontent(display HTML/plain text),listConfig, andtableConfigso consumers building their own UI can read them.
- Body Text (
- Measurement field (
measurement). A number with a unit the visitor picks (or a fixed unit). Submitted value is{ value, unit, base, baseUnit }—baseisvaluein the category's base unit, recomputed authoritatively server-side so downstream tables can sort/filter on a single number without per-row conversion.unitConfigcarriescategory,units,mode(select|fixed),defaultUnit,outputUnit(the org's preferred unit — convertbase→outputUnitto resolve internally), andallowDecimal.<BiabForm/>renders the input + unit picker with a live "≈" conversion hint. Catalog + converters live in@biab-dev/sdk(forms-core) — length, mass/weight, temperature, storage, energy, power, volume, area, speed, duration. - Currency field (
currency). A money amount with a symbol;currencyConfighassymbol,allowNegative,allowDecimal. - Number
allowDecimal. Thenumberfield gains anallowDecimalflag (whole-numbers-only when off), enforced in validation. - Multi-Populator (repeater). A new
multi_populatorfield type — a container whose children are an instance template the visitor repeats with "Add another". The submitted value is an array of instance records, each keyed by the child field ids (a scoped sub-submission).forms.schema(slug)exposespopulatorConfig({ maxInstances, addButtonLabel, itemLabel }) and the template onchildren.<BiabForm/>renders a working repeater (biab-populator/…__item/…__add); validation enforces the instance cap, rejects unknown keys inside instances, and checks required child fields per-instance. - Coupon on completion + notice. A new
grant_couponform action attaches a coupon to the submission and surfaces a pre-submit notice on the form.forms.schema(slug).actionsnow includes acouponblock ({ code, notice, finePrint }) forgrant_couponactions;<BiabForm/>renders it above the submit button (biab-coupon-notice), with{code}substituted in the notice. The coupon id / field mapping stay server-side. - Exit confirmation. When the org enables it,
<BiabForm/>attaches abeforeunloadguard once the visitor has edited a field and hasn't submitted — warning before they abandon the form. Readforms.schema(slug).settings.exitConfirm({ enabled, message }); the browser shows a generic native prompt, andmessageis provided so you can also show it in your own modal. - Phone fields auto-format + cap length.
<BiabForm/>'s phone input now formats as you type — national →(555) 123-4567(10-digit cap), international → loose 3-digit grouping preserving a leading+(15-digit E.164 cap). The submitted value carries the formatting;validateFormSubmissionand the server strip non-digits before the digit-count check, so it stays valid. Purely a UX improvement — no schema or contract change. - Calculation / Math fields (
type: "calculation"). A field whose value is computed from other fields rather than typed — either an inline formula or a reference to one of the org's reusable "business functions".FormFieldDefgains acalculationConfigdescribing the source. Non-sensitive calcs compute live in the browser as the visitor edits inputs, so the result updates in place; sensitive ones (the org marks them) compute authoritatively server-side on submit and never expose their formula. To make live compute possible without the org re-shipping logic,forms.schema(slug)now carries abusinessFunctionsarray (the non-sensitive function defs only) so the SDK can evaluate them on the client.<BiabForm/>renders calculation fields read-only and keeps them in sync; the forms-core entry (@biab-dev/sdk/forms) exports the evaluation primitives so consumers building their own UI can compute the same values:computeCalculation,formatCalcValue,validateFormula,runFormula,runBusinessFunction,functionsToEvalOptions, plus the formula / calculation types. - Deferred file uploads + form-wide cap. File fields no longer upload on
select —
<BiabForm/>buffers the chosenFilein the field value and the upload happens duringsubmit(), so an abandoned form never orphans storage. Until submit, a file field holds aPendingFileValue(useisPendingFileValueto detect it). A form-wide cap ofFORM_WIDE_MAX_FILES(5) applies across all file fields combined, not per field. iPhone HEIC photos and HEVC /.movvideo are accepted automatically whenever a field allows images / video. forms-core (@biab-dev/sdk/forms) exportsbuildAcceptAttr(theacceptstring for an<input type="file">),validateFile,isFileTypeAllowed,FORM_WIDE_MAX_FILES, andPendingFileValue/isPendingFileValueso a custom file UI matches the binding's rules exactly. - Fix-it wizard. When
submit()fails validation,<BiabForm/>routes to the first errored field's step (consecutive multi-step forms) and scrolls it into view, so the visitor lands on what to fix instead of a top-of-form error list. The controller returns structuredissuesfor every error — required fields, the form-wide file cap, and upload failures — so a headless consumer can drive the same focus / scroll behaviour. - Additive only; safe to upgrade with
pnpm add @biab-dev/sdk@^0.9.
0.9.8
- Choice Cards field (
type: "choice_cards") — selectable cards instead of a plain radio/checkbox list.forms.schema(slug)returns achoiceConfig:mode: "single"→ radio semantics; the submitted value is the chosen option'slabel(a string).mode: "multi"→ checkbox semantics; the value is astring[]of chosen labels.- Each option is
{ label, icon?, imageUrl?, price?, description?, productId?, href? }.labelis the only thing submitted;icon(a Lucide name),imageUrl,price,description,href, andproductIdare display + metadata — they let a card showcase a product/service and map a selection back to your catalog, but they never change validation. validateFormSubmissionenforces the same rule as the server: single must be one of the option labels, multi must be a subset.
- Headless render hooks.
<BiabForm/>renders choice cards with stable BEM classes (biab-field__choice-cards,biab-field__choice-card,…__choice-card-image/-label/-price/-description/-link) plusdata-active,data-product-id, anddata-icon(the Lucide name) attributes — style them or swap in your own icon set without the SDK shipping an icon dependency. - Additive only; safe to upgrade with
pnpm add @biab-dev/sdk@^0.9.
0.9.7
- Form schema carries render settings.
client.forms.schema(slug)now returnssettings(animation, multi-step, progress style) + anorgIconURL, so a consumer can render the form exactly as the org configured it. - New + richer form field types (all flow through
forms.schema/submitandvalidateFormSubmission):- Boolean fields expose
display: "toggle" | "checkbox"(same value, two UIs) andmustBeTrue— when set, the value must betrueto submit (consent, e.g. "I agree"); otherwise an optional yes/no accepts true OR false. - Tags (
type: "tags") — a free-text tag input whose value is astring[]. - Custom values —
allowCustomonselect/radio/checkboxlets a submission carry a value outsideoptions(the "Other…" choice); the options whitelist isn't enforced for that field.
- Boolean fields expose
- Submission provenance.
forms.submit(slug, data, { source, referrer, metadata })stamps where/how a form was filled (e.g."facebook","google-ads:summer"); it flows into CRM lead-source. No channel is invented — a missing source stays missing, and a per-form Require Source setting can reject sourceless submissions. Chatbot fills self-label as"chatbot". - Stricter client validation.
validateFormSubmissionnow matches the server: email requires a real TLD, phone enforces digit count. A required yes/no toggle is now satisfied by either answer — "no" is valid, not just "yes". - Reserved keys.
sourceandreferrerare reserved for submission provenance and can't be used as a field's output key. - Customer portal — billing actions & referrals.
customerPortal().payInvoice(id, { successUrl?, cancelUrl? })→ a Stripe-hosted Checkout URL to redirect to (card entry stays on Stripe; your app never touches card data).customerPortal().signContract(id)→ a ready, absolute hosted signing URL for the org's branded e-sign page.customerPortal().viewQuote(id)(alias ofgetQuote) alongsideacceptQuote/rejectQuote.customerPortal().myReferralStats()/myReferralHistory({ limit? })/myReferralPayouts()— the customer's own referral activity as an affiliate.customerPortal().listOrders({ limit? })/getOrder(id)/trackShipment(id)— orders + a shipping-status tracker (carrier, tracking link, status timeline, ETA) with a best-effort live carrier poll. Requires the org'secommerce.shippingentitlement.
- Storefront branding.
client.site(siteId).branding()returns the org's logo + favicon (site media) and org name + icon — render the masthead/favicon without the org re-supplying assets. - Shipping settings.
client.shipping.settings()returns the org's storefront shipping config (enabled, coarse ships-from, destination countries, duty model, carriers) — sanitized (no keys, no precise origin). - Form actions (preview).
forms.schema(slug)now includes a sanitizedactionsarray ({ type, condition }) describing what a submission triggers (e.g. "add to followers") so you can preview it. Execution is server-side on submit; action wiring (URLs, secrets, mapping) never leaves the server. - Additive only; safe to upgrade with
pnpm add @biab-dev/sdk@^0.9.
0.9.6
- Form output keys + validated submissions. Each field in a form
schema (
client.forms.schema(slug)) now publishes a stable, readablekey(camelCase or snake_case, configurable per org / form / field). Build yourforms.submitpayload keyed by thatkey— the legacy fieldidis still accepted, so existing integrations keep working. - Strict server-side validation. Because form schemas are externally
addressable, every submission is now validated against the live form
definition before it's stored; payloads that don't match the schema are
rejected and nothing is written.
validateFormSubmission(schema, data)mirrors the server contract (and now accepts eitherkeyorid) so you can check client-side first. forms.submitreturns a result, not a throw. It now resolves to aFormSubmitResult—{ ok: true, submissionId }or{ ok: false, reason, message, issues? }wherereasondistinguishes API-key problems, plan/billing gates, rate limits, schema-validation failures, not- found, and network errors. Branch onresult.okinstead of catching.- Dry-run testing.
forms.submit(slug, data, { dryRun: true })validates a payload against the live schema and returns the same result shape, but stores nothing and fires no CRM / staff-notify side effects (dryRun: true, nosubmissionId). Same authenticated endpoint, so Postman/curl works too (send{ "test": true, ... }). Authenticated submits are rate-limited per key+form. - Additive only; safe to upgrade with
pnpm add @biab-dev/sdk@^0.9.
0.9.5
- News / Updates feed on the bundle.
bundle.updatescarries Google-Business-style posts (offers / events / plain updates) with images, link, and posted date — render an "updates" surface alongside the blog without a second fetch. Untyped passthrough today (a localBundleUpdateItemtype matches the shape; see the starter templates). - Commerce SDK docs filled out (cart / checkout / coupons / subscriptions / customer portal) and migrations moved onto the journaled drizzle-kit flow.
- Additive only; safe to upgrade with
pnpm add @biab-dev/sdk@^0.9.
0.9.4
- Reviews wall — lazy-loaded.
client.reviews.list({ limit, offset })paginates the public review wall, and the marketing bundle now carries a review aggregate (average, count, star histogram) so the first paint shows the summary + first page with no extra round-trip. Bundles served by pre-0.9.4 hosts simply omit the aggregate and the SDK tolerates it.
0.9.3
- Deploy/consumption hardening. Smooths the "install from npm in a real build pipeline" path (the starter templates and reference consumer now build against the published package rather than a workspace link) plus assorted fixes. No API changes.
0.9.2
- Programmatic Local SEO.
defineParallelPage({ routePattern: "/services/[service]/[area]", ... })declares one URL pattern; BIAB materialises the cartesian cross-product (e.g. 5 services × 30 areas = 150 indexed pages), each with its own meta, canonical, and sitemap entry.client.parallelPages.{list,listVariants,render,sitemapUrl, robotsUrl}. Gated onmarketing.programmatic_pages. - Billing-lifecycle degradation. Three states — grace (everything
works), lapsed (static reads keep serving, interactive surfaces return
a typed
billing_lapsed), suspended (robots →Disallow: /, sitemap empty, pages 503). Surface it withBiabPaymentLapsedError/BiabServiceSuspendedError. The customer's URLs survive the org's billing problem. - Privacy-conscious analytics.
<BIABAnalytics />(@biab-dev/sdk/react-analytics) orinitBiabAnalytics(...)(@biab-dev/sdk/analytics-core, framework-agnostic). No cookies, no localStorage, no third-party requests; daily-rotating anonymous-ID hash; DNT + GPC honoured. Wired into every starter template. - Three new starter templates (Angular, HTMX, Remix) bring the starter count to 11.
- Full notes: see the 0.9.2 release notes in the docs.
0.9.1
- Revalidation webhook architecture. BIAB publish events fan out to
the consumer via an HMAC-signed webhook. Drop-in for Next
(
export { POST } from "@biab-dev/sdk/next/revalidate") callsrevalidateTag; the framework-agnosticcreateGenericRevalidateHandler(@biab-dev/sdk/adapters/revalidate) lets any runtime bust its own cache. Replay-window + signature verification shared viaverifyAndExtractTags. - Scheduling + forms surface.
client.scheduling.*(event types + slots + bookings, Calendly-style) andclient.forms.*(schema-driven form definitions + submit + file upload).
0.9.0
- Full programmatic consumer surface — no iframes. Orgs render
native UI on top of every BIAB feature:
client.blog.*,client.storefront.*,client.cart.*(visitor-token or session-bound),client.checkout.*(Stripe handoff- status, Stripe Link opted in on every session),
client.coupons.validate(five kinds: amount / percent / free-shipping / BXGY / bundle-gift),client.address.*(Google Places autocomplete + verify),client.shipping.*(Shippo multi-carrier rates + tracking),client.notifications.*,client.subscriptions.*.
- status, Stripe Link opted in on every session),
- Customer portal + tenant auth (first npm release carrying the
0.8.6–0.8.9 interim work):
createAuthHandlermounts the WorkOS callback on the tenant's own domain;getTenantSession(...)validates thebiab_sessioncookie server-side;<SignIn/>,<SignUp/>,<SignOut/>, anduseUsership from@biab-dev/sdk/react. The portal (customerPortal(orgId).withSession(token)) exposes the signed-in customer's work bundle, jobs, quotes (accept/reject), per-job comments, and review submission. - Marketing bundle gains
banner(bundle.banner— scheduled, dismissible news messages) plus the brand / company / gallery / reviews sections, and a "contact the company" + staff-ETA surface on portal jobs. - Plan-gating contract. Gated reads return HTTP 200 with a
discriminated-union body (
{ available: false, reason, requiredPlan, upgradeUrl }); gated mutations return 403 with the same shape — branch onavailablewithout try/catch. - No breaking changes from 0.8.7. Full notes: see the 0.9.0 release notes in the docs.
0.8.x — Schema-driven marketing content
0.8.7
- Customer-portal review submission + per-job comments (migration 0139). Signed-in customers leave a review (optionally linked to a job) and comment on their own jobs from the portal.
0.8.6
- Customer-portal work bundle. Per-item detail, accept/reject on quotes, and the auth shortcuts the SDK's React components build on.
- Bundle schema gains
brand/company/gallery/reviewssections (the 0.8.4 interim work, first published here).
0.8.3
- CLI: better failure diagnostics for
sync-content. When the host responds toPOST /sites/:siteId/marketing/sectionswith an HTML page instead of JSON (e.g. Next's 404 shell because the route hasn't been deployed yet, a CDN error page, or an auth-portal redirect), the CLI now reports a clear "server returned an HTML page, not JSON — the bulk-upsert route may not exist on this host yet" message instead of the crypticUnexpected token '<'fromJSON.parse. A similar guard now wraps successful 2xx responses too, in case an edge proxy ever wraps the JSON body. - No API changes; safe to upgrade with
pnpm add @biab-dev/sdk@^0.8.
0.8.2
- New:
biab-dev sync-contentCLI command. Walks a local<rootDir>/<locale>/<page>/<section>.jsontree and bulk-uploads section values to BIAB via a new package-API endpoint (POST /sites/:siteId/marketing/sections). Per-row failures don't abort the batch — the CLI prints a✓ N succeeded ✕ M failedsummary with per-failure detail so first-time imports of legacy content surface every validation gap in one pass. Pass--laxto skip schema validation on the import (useful for staging content that still needs cleanup); pass--note "first import"to label every revision row created in this run. - New:
contentSyncfield ondefineSiteMarketingSchema(). Declares the local content layout to the CLI:{ rootDir, sectionAliases?, pageAliases?, locales?, skipSections? }.sectionAliaseslets filename → schema-key mismatches (e.g.marquee.json→trustMarquee) stay in one place;skipSectionsexcludes managed-data sections (gallery, blogFeed, pricing) whose values come from other dashboards. - New:
marketing:write_contentscope. Distinct frommarketing:write_schema— a content-sync key cannot reshape the schema, and a schema-only key cannot rewrite values. - Dashboard: V7 Pages list now shows real per-locale coverage. A
new
siteMarketing.content.listPagesOverviewtRPC procedure unions legacy single-doc pages with section-only pages and computes per-locale coverage against the EN section universe, so the<CoverageBar>atom renders accurate fill in one round-trip.
0.8.1
- Fix: Add explicit
.jsextensions on every relative import in the SDK source so Node's ESM loader resolves them at runtime. Without this,import "@biab-dev/sdk"from a Node-ESM consumer (CLI, Astro, Next runtime) fails withCannot find module './client'because themodule: ES2022 + moduleResolution: Bundlerbuild leaves imports extension-less. Bundler-based consumers (Vite, Webpack, Turbopack) weren't affected — they resolve without extensions — but Node and the SDK's ownbiab-devCLI were broken. - No API changes; safe to upgrade with
pnpm add @biab-dev/sdk@^0.8.
0.8.0
- Schema-driven marketing flow. Consumers declare each marketing
section as a typed zod schema in
biab.config.tsviadefineSiteMarketingSchema(). The CLI (pnpm biab-dev sync-schema) canonicalizes the body, computes a SHA-256, and POSTs to a draft slot on the host. The dashboard's "Promote" button copies draft → published — only path that writes the live slot, gated onwebsite.manage. A leaked SDK API key can stage a bad draft but cannot affect visitors. client.site(id).marketing.getPageBundle({ pageKey, locale }). Live-site read path. Returns sections tagged{ ok: true, data, source } | { ok: false, error }so one malformed row never crashes the page. Same call returns SEO + JSON-LD nodes + hreflang + availableLocales.@biab-dev/sdk/seo— typed JSON-LD builders (LocalBusiness, Organization, Website, Service, FAQ, Breadcrumb, Article, Review, ProductOffer) +renderJsonLdNodes/renderJsonLdToHtmlfor<head>injection. AIEO-ready out of the box.- React hooks:
useMarketingPageBundle,useMarketingSection,useMarketingPageSeo,useMarketingLocales,usePublishedMarketingSchema,useMarketingBundlePreloader. - Versioning + restore on every content-bearing surface. Section values, SEO rows, brand tokens, and the schema itself. Restores are themselves revisions (append-only timeline, never destructive).
- Brand-token expansion server-side — SEO crawlers see resolved copy and rebranding is a one-row edit at the site level.
marketingPages.get(pageKey)still works. A server-side legacy adapter keeps the existingcustom-demo-style flow unchanged.
0.7.x — Early surfaces
0.7.62
- Chatbot tool-loop parity (recommend_form + uiActions, inline form rendering, CRM mirrors), live front-desk availability + wait-time hints, and BYOK provider keys (OpenAI / Anthropic / Grok).
0.7.51
- CORS preflight on chatbot routes for browser-side widgets hosted on cross-origin domains.
0.7.4
- Tenant auth via WorkOS Organizations — sign-in / sign-up /
sign-out for org end-users,
useUser()hook, SSRgetTenantSession().