Scaffolding a DuckViz app — from `npx` to a working dashboard in 60 seconds
`npx duckviz create-app my-app` drops you into a Next.js starter with Explorer, Dashboard, Report, and Deck wired to a demo dataset and the SDK proxy. Here's why we built it, what it deliberately does not do, and the bug week that made it actually work.
A friend pinged me a few weeks back with a screenshot of <Dashboard /> rendering in his side project. The next message said: "okay how do I plug this into our actual app at work."
My answer was a Notion doc. Two thousand words. A providers file, a Zustand store, an SDK proxy route, a customFetch rewrite, four routes, one demo endpoint, and a list of imports the size of a CVS receipt. He read it. He understood it. He still didn't ship anything.
That was the wrong answer. So I wrote a different one:
npx duckviz create-app my-appSixty seconds later you have a Next.js 15 app with Explorer, Dashboard, Report, and Deck rendering against a thousand fake users. No Notion doc.
The shape of the scaffold
DuckViz has four flagship surfaces — <Explorer />, <Dashboard />, <ReportBuilder />, <DeckBuilder />. Most demo apps I see online ship one of those, render it against hardcoded JSON, and call it done. That's a fine pitch but a useless starting point: the moment you try to add the second surface, you discover the wiring you skipped.
So create-app --template=nextjs ships all four. They share one route per surface, one demo data endpoint at /api/users, one ingestion hook (useUsersDataset() in lib/use-users.ts), and one dashboard config (lib/demo-dashboard.ts — KPI total users, KPI total MRR, bar by country, line of signups over time). That's it. Four routes, one dataset, one config. The point is for the connective tissue between them to be visible — because the moment you swap the demo for your real data, what you'll edit is precisely the connective tissue.
The other thing the scaffold ships is the boring stuff that always trips people up:
app/api/duckviz/[...route]/route.ts— the SDK proxy. ReadsDUCKVIZ_TOKENfrom.env.localserver-side and forwards every browser AI call upstream with the bearer attached. Without this you'd either ship the token to the browser (no) or the AI features wouldn't work at all (also no).lib/custom-fetch.ts— a five-line helper that rewrites/api/widget-flow/*to/api/duckviz/widget-flow/*and gets passed into Explorer / Report / Deck via theircustomFetchprop. Without this the components hit your own origin and 404 on every AI call. (We learned this the hard way — see below.)app/providers.tsx—<DuckvizThemeProvider>and<DuckvizDBProvider persistence batchSize={5000}>wrapping the tree. DuckDB-WASM in a Web Worker, IndexedDB persistence, the works.
That's the whole thing. Read top-to-bottom in about ten minutes. Replace the demo data and you have a real app.
The bug week that made it actually ship
create-app shipped in 0.3.0 and the experience went something like:
npx duckviz create-app my-app
cd my-app
npm run dev
# 🟢 explorer renders
# 🟢 dashboard renders
# 🔴 every AI call returns 404
That was bug one. The SDK proxy was wired at /api/duckviz/[...route], but Explorer / Report / Deck were calling /api/widget-flow/* against my own origin, where nothing existed. The fix in 0.3.2 was the lib/custom-fetch.ts shim plus threading customFetch={customFetch} into all three components. Five-line file, half a day to actually find. If you scaffolded between 0.3.0 and 0.3.1, your AI features were quietly broken — re-scaffold or backport the change.
Bug two showed up earlier in 0.3.1. The demo dashboard widget config used type: "kpi" for the two big-number cards. The chart registry doesn't have a kpi alias — the actual id is "big-number". Both KPI cards rendered "Unknown chart type: kpi" on /dashboard and looked exactly like a configuration mistake the user had made themselves. They hadn't. I had. Renamed.
Bug three was a next build failure. app/explorer/page.tsx couldn't compile because @duckviz/explorer only declared "import" and "types" in its exports field — and the strict TypeScript checker in Next 15 wanted a default fallback. Same shape on @duckviz/sdk/next, which typed params as Promise<{...}> | {...} — a union the strict checker refused. Both fixed upstream in @duckviz/explorer@0.17.1 and @duckviz/sdk@0.2.1.
Three bugs, all in the gap between "the scaffold copies files" and "the scaffold produces a working app." Unit tests covered the copy. Nothing covered the working-app part.
Lesson, written down where I'll see it: a starter's job is not to copy correct files. Its job is to produce an app that runs. The CI guard now is scripts/sync-template-deps.ts — for every @duckviz/* range in the template's package.json.template, it asserts the range is satisfied by the current monorepo version. So when I bump @duckviz/explorer to fix something the template depends on, the CI fails until I bump the template's range too. Boring guard, very effective.
What it deliberately does not do
A starter is a tradeoff between batteries-included and not-my-batteries. I've used scaffolders that asked me eight questions before they'd start (router? styling? auth? state? testing? linting? formatter? theme?) and by question three I'd given up and run create-next-app instead.
create-app asks zero questions. The opinion is the whole product:
- Next.js, App Router. Not Remix, not Vite. One template. The flag exists (
--template=nextjs) so a future second template doesn't break the API, but I'm not adding one until someone shows up wanting it bad enough to maintain it. - Tailwind v4 + React 19, because that's what
create-next-app@latestinstalls as of writing. Ifcreate-next-appchanges its defaults next month, I'll change with it. - No auth scaffold, because real auth is your job and a fake login screen is worse than no login screen.
- No "pick a theme preset" prompt, because
<DuckvizThemeProvider>already takes apresetprop and the starter sets a sensible one. Change one line. - No choose-your-own-package-manager, because
npm_config_user_agentalready tells me which one you're using. Override with--pmif I got it wrong.
What you get is one path through one stack with one set of conventions, working end-to-end. If you want a different stack, fork it. The whole template is a directory of plain files; nothing magical.
Try it
npx duckviz create-app my-appcd my-appcp .env.local.example .env.local # paste your DUCKVIZ_TOKENnpm run dev # then open /explorerMint a token at app.duckviz.com/settings/tokens. Free tier, no card. The full quickstart is at docs.duckviz.com/docs/packages/quickstart — same path, more screenshots.
The Notion doc is retired. If you want to add DuckViz to an existing app, the Next.js integration guide walks the same wiring step by step. If you want to start from scratch, that's what this CLI is for.
— Vikas