CLI
Scaffold an embedded DuckViz app, or push local files into your browser session from a terminal.
The duckviz CLI is a small Node binary published on npm. Its primary job is scaffolding an embedded DuckViz app for developers; a secondary subcommand pushes local files into the live app for analysts.
create-app— scaffold a Next.js starter wired to the DuckViz packages (Explorer, Dashboard, Report, Deck) with the SDK proxy already set up. Added in0.3.0. Start here if you're embedding DuckViz.<paths...>— push local files into the browser session you're signed into, without an internet round-trip. Bytes move browser ↔ localhost, never through any server. Useful for analyst workflows on the hosted app.
Current version: 0.3.2. See the npm changelog for the full release history.
Upgrade to 0.3.2 if you scaffolded earlier
Scaffolds from 0.3.0 and 0.3.1 shipped a working layout but the AI calls (/api/widget-flow/*, /api/generate-report-sections, etc.) hit your own origin and 404'd — the SDK proxy was wired but unreachable. 0.3.2 adds lib/custom-fetch.ts and threads customFetch through Explorer / Report / Deck so the proxy is actually called. Re-scaffold or backport the change.
Install
Zero-install via npx:
npx duckviz create-app my-app # scaffold a starter
npx duckviz ./data.csv # ingest a fileOr global:
npm install -g duckviz
duckviz --helpScaffold a starter app
npx duckviz create-app my-app
cd my-app
cp .env.local.example .env.local # paste your DUCKVIZ_TOKEN
npm run devOpen http://localhost:3000/explorer — Explorer renders against a demo dataset, the AI widget panel populates, and you can navigate to /dashboard, /report, or /deck to see the same data on each surface.
What the nextjs template ships
| File / route | What it does |
|---|---|
app/dashboard, app/explorer, app/report, app/deck | One route per surface. All four share the same demo dataset. |
app/api/users/route.ts | Demo endpoint returning fake user rows. Replace with your real data source. |
app/api/duckviz/[...route]/route.ts | SDK proxy. Reads DUCKVIZ_TOKEN from .env.local and forwards Explorer / Report / Deck AI calls upstream with the bearer attached server-side. The token never reaches the browser. |
lib/custom-fetch.ts | Rewrites /api/* → /api/duckviz/* so the components hit the proxy instead of 404'ing on your own origin. Threaded into <Explorer>, <ReportBuilder>, <DeckBuilder> via their customFetch prop. |
lib/use-users.ts + lib/demo-dashboard.ts | One-shot ingest into a t_users DuckDB table + a 4-widget demo dashboard (KPI, KPI, bar, line) shared by every surface. |
Stack: Next.js 15 + App Router + Tailwind v4 + React 19.
Options
npx duckviz create-app my-app --template nextjs # only template today
npx duckviz create-app my-app --pm bun # force pkg manager (npm|pnpm|yarn|bun)
npx duckviz create-app my-app --no-install # skip post-scaffold install
npx duckviz create-app my-app --no-git # skip `git init`
npx duckviz create-app my-app --force # overwrite non-empty dirThe package manager is auto-detected from npm_config_user_agent (whichever pm you ran npx/pnpm dlx/bunx with). --pm overrides the detection.
After scaffolding
DUCKVIZ_TOKEN=dvz_live_your_token_here…in .env.local. Mint a token at app.duckviz.com/settings/tokens. Then run dev as you normally would.
See the Next.js Integration Guide for the same wiring explained step-by-step — useful when you want to bolt DuckViz onto an existing Next.js app instead of scaffolding fresh.
Push files to the live app
duckviz <paths...> [options]
Options:
-r, --recursive Walk directories recursively
--fresh Replace the current session (instead of appending)
--url <url> Custom web app URL (default: https://app.duckviz.com)
-p, --port <port> Pin the local server port (default: random)
--no-open Print the URL; skip opening the default browser
--ignore <pattern> Glob to skip files/folders (repeatable)
-h, --help Show helpAs of 0.2.0, the default target is app.duckviz.com (moved from duckviz.com when the product split into three domains). --port, --no-open, and --ignore were also added in 0.2.0.
File allowlist
The CLI only reads extensions the web app knows how to ingest:
.csv .tsv .json .jsonl .ndjson
.xlsx .xls
.xml
.log .syslog .clf .evtx
.txt (since 0.2.0 — parser catalog matches many .txt formats)
.parquet (since 0.2.0 — zero-copy ingest)Anything else is silently skipped when walking directories.
Examples
# Single CSV
npx duckviz data.csv
# Walk a folder, drop the current session first
npx duckviz ./logs -r --fresh
# Multiple files
npx duckviz sales.csv inventory.xlsx users.json
# Skip node_modules and build output
npx duckviz . -r --ignore "node_modules/**" --ignore "dist/**"
# Pin the port (useful behind a reverse proxy or local firewall rule)
npx duckviz data.csv --port 7777
# Headless: print the URL, don't open a browser
npx duckviz data.csv --no-open
# Self-hosted / local dev
npx duckviz . --url http://localhost:3001How it works
Build a manifest
The CLI walks your input paths, applies the allowlist + --ignore globs, and builds a manifest (path, size, content hash).
Start an ephemeral localhost server
An HTTP server binds to 127.0.0.1 on a random (or --port) port. A one-shot bearer token is generated. The server enforces a DNS-rebinding guard: the Host header must match the bound loopback port — malicious pages can't hit your CLI server even if they guess the port.
Open the browser
The default browser opens app.duckviz.com/cli-receive?endpoint=...&token=.... The receive page immediately scrubs the token from the URL bar, so it never lands in history or a Referer header.
Use --no-open to get the URL and open it manually (helpful for remote SSH / containers).
Transfer
The tab authenticates to your local server with the bearer token, downloads each file from the manifest, and pipes it into the ingestion worker. Path traversal is prevented: only manifest-registered relative paths are served.
Done
The CLI exits once every file is confirmed. If you don't open the browser within 10 minutes, the server shuts itself down.
No internet upload
The transfer is browser ↔ localhost. Nothing leaves your machine. The app.duckviz.com tab is only the UI shell — it pulls files from http://127.0.0.1:<port>.
Security
| Guard | Prevents |
|---|---|
| Bearer-only auth | Token never leaves a header — no ?token= query fallback, no logs/Referer leaks |
| DNS-rebinding guard | Host must be 127.0.0.1:<port> — blocks websites spoofing the CLI port |
| Path traversal guard | Only manifest-registered paths are served — no .. escapes |
Manifest-gated --fresh | A crafted ?mode=fresh link can't wipe your session until the CLI token is accepted |
| Idle timeout | Server auto-shuts after 10 minutes |
Troubleshooting
Browser doesn't open
The CLI prints the URL. Paste it into the browser where you're signed in to DuckViz — the transfer picks up from there.
"Connection refused"
The server binds to 127.0.0.1, not 0.0.0.0. A firewall or VPN intercepting loopback traffic is the usual cause. Also check for another process already holding --port.
Server times out
10-minute idle budget expired — re-run the command.
.parquet files show up as binary
Needs CLI 0.2.0+. Older CLI versions skipped .parquet because the allowlist didn't include it.