Repository navigation
Architecture Decision Records
Significant technical decisions are recorded here so contributors can see not only what was chosen, but why, and what was given up. A new ADR is added whenever a decision is hard to reverse or shapes the rest of the codebase.
| ADR | Decision | Status |
|---|---|---|
| ADR-001 | Client-only architecture, no backend | Accepted |
| ADR-002 | React + Vite instead of Next.js | Accepted |
| ADR-003 | DuckDB-wasm's worker-based API; heavy work off the main thread | Accepted |
| ADR-004 | Keep query results as Arrow, outside React state | Accepted |
| ADR-005 | CodeMirror 6 instead of Monaco | Accepted |
| ADR-006 | Canvas charts with SQL-side aggregation | Accepted |
| ADR-007 | Start single-threaded; adopt multithreading only if benchmarks justify it | Accepted |
| ADR-008 | Self-host the DuckDB-wasm assets | Accepted |
| ADR-009 | Compose shadcn/ui components on Base UI; keep every visual value in one token file | Accepted |
| ADR-010 | Biome for linting and formatting instead of ESLint + Prettier | Accepted |
| ADR-011 | Keep imported files in OPFS by default | Accepted |
Context. Blinkql's core promises are privacy and zero setup. DuckDB-wasm can run the full analytical workload in the browser.
Decision. Blinkql has no application backend. It is deployed as static files.
Consequences.
- ➕ User data cannot reach a server, because there is none.
- ➕ Hosting is close to free and scales to any number of users; there is nothing to operate.
- ➖ Capacity is bounded by the user's device memory and CPU.
- ➖ No server-side collaboration. Sharing is limited to links that carry queries, not data.
- ➖ No direct database connections.
Context. Blinkql is a single-page app with no server rendering or SEO needs beyond a landing view. It relies on browser-only APIs: Web Workers, WebAssembly, OPFS.
Decision. A React single-page app built with Vite.
Alternatives considered.
- Next.js: server rendering and server components add complexity with no benefit here, and browser-only APIs would need client-only boundaries throughout.
- Svelte 5 / SolidJS: fine-grained reactivity suits data-heavy UIs and has a smaller runtime. Not chosen because React has the broadest ecosystem and contributor familiarity, and because Blinkql keeps large data outside framework state (ADR-004), which removes most of the reactivity advantage on the hot paths.
Consequences. Unnecessary re-renders must be actively prevented with profiling, selector-based subscriptions, and memoization where measurements show it helps.
Context. Queries over large files can take seconds. Any of that work on the main thread freezes scrolling, typing and clicks.
Decision. Use DuckDB-wasm's asynchronous, Web Worker–based API for all query execution. Any other CPU-heavy work (large exports, file conversion, expensive formatting) moves to a dedicated worker, exposed through a typed Comlink API, once profiling shows it's needed.
Consequences.
- ➕ The UI stays responsive during long queries.
- ➖ All engine access is asynchronous, and results cross a thread boundary, which ADR-004 addresses.
Context. Converting a million-row result into JavaScript row objects and putting it into React state costs time, memory and large re-renders.
Decision. Results are stored as Apache Arrow tables in a result store outside React. Components subscribe to small derived values (row count, column metadata, the visible window), and cell values are read lazily from Arrow vectors at render time.
Consequences.
- ➕ Memory and render cost scale with what's on screen, not with result size.
- ➖ The result store needs its own subscription mechanism and careful lifecycle management, such as releasing old results.
Context. The editor needs SQL highlighting, autocomplete and good keyboard support, while cold-start time is a key performance budget.
Decision. CodeMirror 6 with @codemirror/lang-sql.
Consequences.
- ➕ Much smaller footprint than Monaco; modular, so only what's used ships.
- ➖ Fewer IDE-grade features out of the box. Schema-aware completion is built in-house.
Context. SVG charting libraries create a DOM node per mark and slow down sharply with large series.
Decision. Use canvas renderers (Apache ECharts' canvas mode, uPlot for dense time series), and aggregate or bucket data in SQL before charting.
Consequences.
- ➕ Charts stay smooth regardless of source data size.
- ➖ Canvas charts need extra work for accessibility: data-table fallbacks and descriptive labels.
Context. DuckDB-wasm offers bundles with different capabilities; multithreading requires cross-origin isolation and SharedArrayBuffer.
Decision. Ship with the bundle DuckDB-wasm selects by feature detection. Evaluate the multithreaded bundle as experiment P-08 and adopt it only if it shows a meaningful gain without stability or compatibility cost.
Consequences. Decisions about threading are driven by data. Cross-origin isolation headers are configured from the start so the experiment is possible.
Context. DuckDB-wasm can load its WebAssembly and worker files from a public CDN. It also loads some features on first use as extensions, by default from extensions.duckdb.org. The browser build doesn't include Parquet or JSON support, so reading either format triggers one of these downloads.
Decision. Serve these files from Blinkql's own origin, version-pinned and cached with long-term immutable headers. This covers the engine bundles and every extension Blinkql uses (currently parquet and json):
- The engine bundles come from the exact
@duckdb/duckdb-wasmversion pinned inpackage.json. Vite emits them under/assets/with content-hashed names. - The extensions are pinned to one DuckDB version, with a SHA-256 checksum per file, in
src/engine/extensions.ts. The build downloads them, rejects any file whose checksum doesn't match, and serves them under/duckdb-extensions/<version>/. The engine points DuckDB'scustom_extension_repositorysetting at that path.
Consequences.
- ➕ Works cleanly under cross-origin isolation (COEP) and a strict Content Security Policy.
- ➕ No runtime dependency on a third-party CDN or extension repository; full control over caching and versions.
- ➖ The engine files count toward Blinkql's own hosting bandwidth.
- ➖ Builds need network access to
extensions.duckdb.org(files are cached innode_modules/.cacheafterwards). - ➖ Extensions only load into the DuckDB version they were built for. Upgrading
@duckdb/duckdb-wasmtherefore also means updating the version and checksums inextensions.ts. Until then the engine refuses to start, so the mismatch fails CI instead of breaking Parquet quietly.
Context. Blinkql needs an interface that is consistent, accessible and distinctive (see Design Language), built by a small core team and outside contributors, within a tight bundle budget.
Decision. Build the interface only from shadcn/ui components (Mira style, on Base UI primitives), composed rather than custom-built. Keep every colour, radius, font and duration as a CSS variable in src/index.css (Design Tokens). The SQL editor, results grid and charts are the only surfaces not built from shadcn/ui, because the architecture requires them, and they read the same tokens.
Alternatives considered.
- A custom component library: full control, but far more work and accessibility risk before the product exists.
- A packaged component library styled from outside: faster to start, but harder to make look like Blinkql, and the components can't be trimmed to what's used.
- shadcn/ui on Radix primitives: still fully supported, but Base UI has been shadcn/ui's default since July 2026 and is its recommendation for new projects. Some components, such as the new Toast, ship only for Base UI.
- shadcn/ui on React Aria: the most thorough accessibility and internationalisation, but its size against the 250 KB initial-JavaScript budget is unmeasured, and the default path is better trodden.
Consequences.
- ➕ Accessible primitives (keyboard, focus, ARIA) from day one, and a workflow most React contributors already know.
- ➕ The whole look changes in one file, with contrast checked in CI.
- ➖ Component source is copied into the repository, so upstream fixes have to be pulled in deliberately.
- ➖ Many older shadcn/ui examples and blog posts assume Radix. Use the Base UI tab in shadcn/ui's docs, and don't mix in Radix packages.
Context. The Tech Stack originally listed ESLint (with typescript-eslint and eslint-plugin-react-hooks) for linting and Prettier for formatting. That is two tools, two config files and several plugins that have to agree with each other, and both run on every save and in CI.
Decision. Use Biome for linting, formatting and import sorting, configured in biome.json. Rules start from Biome's recommended preset and are made moderately strict: unused imports, variables and parameters, any, missing import type and React hooks violations are errors; non-null assertions and stray console.log calls are warnings. Code is formatted with 2 spaces, double quotes, semicolons and a 100-column line width. Generated shadcn/ui files in src/components/ui/ are formatted but not linted, so upstream code can be pulled in unchanged.
Alternatives considered.
- ESLint + Prettier: the largest rule ecosystem, but two tools to configure and keep in step, and noticeably slower on every save and in CI.
- Oxlint (Vite's template default) + Prettier: very fast linting, but still needs a separate formatter.
Consequences.
- ➕ One fast tool and one config file for linting, formatting and import order; one VS Code extension.
- ➕ Includes the React hooks rules (
useExhaustiveDependencies,useHookAtTopLevel) and Tailwind CSS directive parsing. - ➖ Fewer plugins than ESLint. Rules that only exist as ESLint plugins (for example
eslint-plugin-jsx-a11y's full set) aren't available, though Biome covers most accessibility rules. - ➖ Contributors used to Prettier may see small formatting differences.
Context. Imported files lived only as long as the page, so every reload meant importing them again. Browsers offer the Origin Private File System (OPFS): per-origin storage on the device that suits large files, can be written as a stream, and is never uploaded.
Decision. Keep a copy of every imported local file in OPFS by default (datasets/<table name>, plus a manifest.json with each table's name, original file name, format, size and save time). The copy is written in the background after the table appears. On start-up the stored files are imported again under their old table names. Each table has a Forget action that drops it and deletes its copy. The first save asks for persistent storage (navigator.storage.persist()). Remote files are not copied. See #37.
Alternatives considered.
- Opt-in per table ("Keep"): more cautious with on-device copies, but one more click for every file, and forgotten clicks look like lost data after a reload.
- IndexedDB blobs: works everywhere, but large files are slower to write and can't be streamed to disk as simply.
Consequences.
- ➕ Tables are back immediately after a reload, with no re-import.
- ➖ Copies of user data stay on the device until forgotten or until site data is cleared. They are never synced or sent anywhere, but anyone using the same browser profile can see them.
- ➖ Copies count against the storage quota, and without persistence the browser may evict them under pressure. A failed save never blocks the import; the table works for the visit and a notice says so, and evicted files are dropped from the manifest on start-up.
- ➖ Private windows and some browsers (including Playwright's WebKit) can't keep files, so there tables last only for the visit.
Open a Discussion describing the problem. Once there's agreement, add an ADR to this page in a pull request using this template:
## ADR-XXX: <decision title>
**Context.** What problem or force led to this decision?
**Decision.** What we're doing.
**Alternatives considered.** What else we looked at, and why not.
**Consequences.** ➕ What gets better. ➖ What gets worse or harder.⚡ Blinkql: queries in the blink of an eye · MIT licensed · Live app · Issues · Discussions
Project
Engineering
- Tech Stack
- Architecture
- Performance & Benchmarks
- Decision Records (ADRs)
- Privacy & Security
- Browser Support & Limitations
Design
Community