Starter templates

Every starter renders the same generic business site: marketing sections, a storefront, cart and Stripe checkout, a blog, a reviews wall, programmatic SEO pages, and a custom-database demo. What changes is the platform's constraints.

Pick your framework with the picker in the navbar — every code example across these docs follows your choice.

git clone --depth 1 https://github.com/businessdash/SDK-Starter-Templates.git my-app
cd my-app/React-Bun
cp .env.example .env
pnpm install

The three shapes

A starter is one of three things, and which one it is decides how it holds a key, what it can call, and whether it needs a server at all.

Web — the SDK does the work

Angular · Astro · HTMX · Next.js · Nuxt · Qwik · React · Remix · Solid · SvelteKit · T3 · TanStack Start · Vanilla JS

These import @businessdash/sdk directly. Server-rendered pages hold the secret key server-side; anything the browser touches goes through a same-origin proxy route or a publishable token.

Server — a hand-written client, full API

Laravel (PHP) · Phoenix (Elixir) · Vapor (Swift)

The SDK is TypeScript, so these talk to the Package API directly — a job that turns out to be small, because the surface is plain REST with a bearer key. Each is roughly 600–900 lines for everything a starter needs.

They hold a secret key: it never leaves the process, so the whole API is available.

The parts that would be genuinely hard to port aren't ported. The <bd-form> web component and the analytics beacon load from a CDN as ES modules, and the schema CLI runs as a dev-only dependency — so the canonical checksum stays in one place rather than being reimplemented byte-for-byte in three languages.

Mobile — publishable token only

React Native (Expo) · Swift (iOS/macOS) · Flutter · Kotlin (Android)

An app ships its credential inside the artifact. strings on an .ipa or .apk finds anything you bundle, so every mobile client here refuses a secret key at construction.

That is far less limiting than it sounds, and it is the single most useful thing to know about building a BusinessDash app:

The publishable scope set covers the entire customer-facing surface — storefront, cart, checkout, blog, marketing content, chat, forms, the whole customer portal, sign-in, and public custom-object reads. A BusinessDash mobile app needs no backend-for-frontend. Only operator and admin writes require a secret key, and a customer app never makes those.

Four rules that bite everyone

These are platform-independent, and every starter encodes them.

1. The access gate answers reads with HTTP 200

A read against a lapsed plan returns 200 with available: false in the body, not a 4xx. Reads use a body-only signal so a cached CDN response can't hard-fail a page — which also means a client that only inspects the status code decodes an empty screen and never notices.

Check the body before the status.

2. Every list response is keyed items

Not products, posts, reviews, subscriptions, comments, or categories. Reading the wrong key yields nothing rather than an error, so a page renders "no products" against a full catalog and nothing looks broken.

A single blog post also arrives wrapped — { post, access }, where access is granted or paywall. A paywalled post comes back truncated rather than absent, so ignoring the flag renders a teaser as the whole article.

3. Money has two shapes

Product, subscription, invoice and checkout totals are integer cents — the platform-wide convention for stored money, so no total picks up a floating-point rounding error on the way to Stripe.

Cart unitPrice and subtotal arrive decimal.

Every starter ships two separate formatting functions rather than one overloaded helper, because mixing them is a 100× error in either direction.

4. The cart visitor token is generated locally

There is no endpoint that mints one — the platform keys the cart on whatever arrives in X-BD-Cart-Visitor. (cart/session exists, but it mints a tokenized iframe-embed URL: a different feature.)

It is an opaque id, not a secret. Ordinary storage is correct for it; the session token is the one that belongs in a Keychain or SecureStore.

Checkout returns the Stripe URL as stripeUrl, not url.

The CLI is the same everywhere

Schema push, data-model push, and seeding go through bd — a Node CLI that talks to the same REST API. It is identical for every starter, including the ones with no JavaScript in them at all.

npm run sync-schema        # push bd.config.ts to the draft slot
npm run sync-data-model    # push bd.data-model.config.ts (+ generated forms)
npm run view-data-model    # what's LIVE right now — the dashboard can change it too
npm run seed               # upsert rows from ./bd-records

Nothing changes on the platform until you promote the draft in the dashboard. sync-* prints the migration plan first.

Why the config files stay TypeScript

The schema artifact carries a canonical SHA-256 that the server recomputes and compares. Reimplementing the canonicalisation in PHP, Elixir, Swift, Dart, or Kotlin would mean matching it byte for byte — and a silent mismatch rejects every push.

So the config stays TypeScript and the real CLI computes the checksum. Every non-JS starter carries a small package.json whose only job is that: Node is a build-time tool, never a runtime dependency. A Laravel app in production has no Node anywhere near it.

The scripts run the CLI through tsx, so the TypeScript config loads on any supported Node version. Node 22.6+ can strip types natively, but relying on that would break on older runtimes — and the failure message ("Could not load TypeScript config") doesn't obviously point at your Node version.

Seeding is plain JSON

sync-records reads ./bd-records/*.json, so seed data is completely language-neutral — byte-identical across every starter:

{
  "object": "todos",
  "universalIdentifier": "todo-seed-welcome",
  "values": { "title": "Welcome", "done": false },
  "relations": { "todo": "<target universalIdentifier>" }
}

Two things worth understanding:

universalIdentifier is identity, not a name. It is what makes re-running seed converge rather than duplicate — rows are upserted against it.

relations link by the target row's universalIdentifier, not by a database id you don't have yet. That's what lets one push reference rows it is creating in the same run.

Seeds land in objects that must already exist, so the order is: sync-data-model → promote in the dashboard → seed.

Platform notes

Remix is React Router 7

The Remix starter runs on React Router 7 framework mode (as of 0.9.60) — imports come from react-router, routes are declared through @react-router/fs-routes with the same flat-file convention, and the dev/build commands are react-router dev / react-router build. If you cloned it earlier as a Remix v2 app, the React Router upgrade guide covers the same migration the starter went through.

React Native imports /native

Expo is the only mobile starter that reuses the SDK — React Native is JavaScript, after all. It must import @businessdash/sdk/native, not the root: the root re-exports the auth handler, which uses Node's Buffer. The call is inside a function, so the root import appears to work and then throws in production.

Metro also needs unstable_enablePackageExports before any subpath export resolves.

Chat is polling everywhere

There is no SSE or WebSocket anywhere in the Package API. Front Desk chat is chatbot/messages every 3–5 seconds while the widget is open.

The Phoenix starter is the only one that improves on this: one GenServer polls per conversation and fans results out over PubSub, so N viewers collapse to one upstream request. Every other starter polls per client.

Forms are the one thing an app reimplements

<bd-form> is a DOM custom element. Web and server starters mount it from a CDN and get conditional blocks, availability pickers and file uploads for free. Mobile starters read the schema and render it with native components — the schema is typed, but the renderer is yours.

What each starter assumes

StarterKeyClientNotes
Web (13 frameworks)secret + publishable@businessdash/sdkProxy route for browser calls
Laravelsecrethand-written PHPCDN islands, npx for the CLI
Phoenixsecrethand-written Elixir⚠️ raw body required for the webhook HMAC
Vaporsecrethand-written Swift⚠️ folder must not be named Vapor
React Native (Expo)publishable@businessdash/sdk/nativeEXPO_PUBLIC_ prefix
SwiftpublishableBdKit (SPM)Session token → Keychain
Flutterpublishablepackages/bd (pure Dart)Config via --dart-define
Kotlinpublishable:bd (pure JVM)Config via local.properties

Each starter's README carries the platform-specific traps in full — the Phoenix raw-body reader, the Vapor directory-name collision, the Metro resolver flag. Read it before the first commit, not after the first bug.

Where they live

All starters live in one repository: bd-sdk-starters. Clone the whole thing and use the directory you need — each one is self-contained.