A browser-based SQL Terminal for DuckDB powered by Ghostty terminal emulator.
The latest version is always deployed to https://duckdb-terminal.com.
The TypeScript API Docs can be found at https://tobilg.github.io/duckdb-terminal.
.help, .tables, .schema┌─────────────────────────────────────────────────────────────────────────────┐
│ Browser │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ DuckDB Terminal │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Terminal │ │ │
│ │ │ │ │ │
│ │ │ - REPL (input, output, history) │ │ │
│ │ │ - Command parsing (SQL, dot commands, multi-line) │ │ │
│ │ │ - Result formatting (table, CSV, TSV, JSON) │ │ │
│ │ │ - Syntax highlighting │ │ │
│ │ └───────┬─────────────────────┬─────────────────────────┬─────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ │ │ │
│ │ ┌───────────────┐ ┌─────────────────────┐ │ │ │
│ │ │ Terminal │ │ Database │ │ │ │
│ │ │ Adapter │ │ │ │ │ │
│ │ │ │ │ - DuckDB WASM │ │ │ │
│ │ │ - Ghostty │ │ wrapper │ │ │ │
│ │ │ - Themes │ │ - Query execution │ │ │ │
│ │ │ - Keyboard │ │ - Auto-complete │ │ │ │
│ │ │ - Mobile │ │ - File loading │ │ │ │
│ │ └───────┬───────┘ └─────────┬───────────┘ │ │ │
│ │ │ │ │ │ │
│ └──────────┼─────────────────────┼─────────────────────────┼────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Ghostty Web │ │ DuckDB WASM │ │ IndexedDB │ │
│ │ (npm package) │ │ (Web Worker) │ │ (Command History) │ │
│ │ │ │ │ │ │ │
│ │ - Canvas rendering │ │ - SQL engine │ └─────────────────────┘ │
│ │ - VT100 emulation │ │ - Query processing │ │
│ └────────────────────┘ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ OPFS │ │
│ │ (Database Storage) │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
npm install duckdb-terminal
import { createTerminal } from 'duckdb-terminal';
const Terminal = await createTerminal({
container: '#terminal',
theme: 'dark',
});
git clone https://github.com/tobilg/duckdb-terminal.git
cd duckdb-terminal
npm install
npm run dev
Open http://localhost:5173 in your browser.
interface TerminalConfig {
// Container element or CSS selector
container: HTMLElement | string;
// Font family (default: 'Fira Code', 'Cascadia Code', etc.)
fontFamily?: string;
// Font size in pixels (default: 14)
fontSize?: number;
// Theme: 'dark' | 'light' | Theme (default: 'dark')
theme?: 'dark' | 'light' | Theme;
// Storage: 'memory' | 'opfs' (default: 'memory')
storage?: 'memory' | 'opfs';
// Database path for OPFS storage
databasePath?: string;
// Maximum DuckDB execution threads (default: 1)
maximumThreads?: number;
// DuckDB-Wasm bundle manifest. A `coi` entry is required for >1 thread.
duckdbBundles?: DuckDBBundles;
// Show welcome message (default: true)
// When enabled, displays loading progress during DuckDB initialization
welcomeMessage?: boolean;
// Primary prompt string (default: '🦆 ')
prompt?: string;
// Continuation prompt for multi-line SQL (default: ' > ')
continuationPrompt?: string;
// Enable clickable URL detection (default: true)
linkDetection?: boolean;
// Scrollback buffer size in bytes (default: 10485760 = 10MB)
scrollback?: number;
// Enable interactive charts feature (default: false)
enableCharts?: boolean;
// Maximum rows shown per result page (default: 1000)
maxDisplayRows?: number;
// Eagerly count all result rows before page 1 (default: false)
exactRowCount?: boolean;
}
The terminal deliberately uses DuckDB-Wasm's smaller, single-threaded bundle by default. Setting threads from SQL cannot add thread support to that build. To enable multiple threads, provide the experimental COI bundle at initialization and set maximumThreads:
import { createTerminal } from 'duckdb-terminal';
import { getJsDelivrBundles, getPlatformFeatures } from '@duckdb/duckdb-wasm';
const features = await getPlatformFeatures();
const maximumThreads = Math.min(navigator.hardwareConcurrency || 2, 4);
const canUseThreads =
maximumThreads > 1 &&
features.crossOriginIsolated &&
features.wasmExceptions &&
features.wasmSIMD &&
features.wasmThreads;
await createTerminal({
container: '#terminal',
...(canUseThreads
? {
maximumThreads,
duckdbBundles: {
...getJsDelivrBundles(),
coi: {
mainModule: '/duckdb/duckdb-coi.wasm',
mainWorker: '/duckdb/duckdb-browser-coi.worker.js',
pthreadWorker: '/duckdb/duckdb-browser-coi.pthread.worker.js',
},
},
}
: {}),
});
Serve all three COI assets from the page's own origin. The page must also send these headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Only the bundle selected for the current browser is downloaded. If maximumThreads is greater than 1 but DuckDB-Wasm cannot select the COI bundle, initialization fails immediately with an actionable error instead of failing later on SET threads.
DuckDB-Wasm's published wasm_threads extensions currently cannot be loaded by the COI bundle because their memory is not linked as shared. This affects Parquet, JSON, spatial, and other dynamically loaded extensions. See duckdb-wasm#2041 and extension-ci-tools#391. Applications opting into multithreading must either avoid those extensions or provide compatible custom builds.
This repository's hosted demo intentionally uses the default single-threaded bundle so file import and extension autoloading remain available. It does not serve COI assets or enable cross-origin isolation.
| Command | Description |
|---|---|
.ai <question> |
Generate SQL from natural language (shorthand) |
.ai query <question> |
Generate SQL from natural language |
.ai provider list |
List available AI providers |
.ai provider set <name> |
Set the AI provider |
.ai endpoint get |
Show current proxy endpoint |
.ai endpoint set <url> |
Set proxy endpoint URL |
.chart [options] |
Show interactive chart of last query result |
.clear |
Clear the terminal |
.clearhistory |
Clear command history |
.copy |
Copy last query results to clipboard |
.download [filename] |
Download last result as file (format based on mode) |
.examples |
Show example queries |
.files [list|add|remove] |
Manage loaded files (list, add, or remove) |
.help |
Show available commands |
.highlight on|off |
Toggle syntax highlighting |
.links on|off |
Toggle clickable URL detection |
.mode table|csv|tsv|json |
Set output format |
.open |
Open file picker to load data files |
.pagesize <n> |
Set result page size (0 resets to the safe maximum) |
.prompt [primary [cont]] |
Get or set the command prompt |
.reset |
Reset database and all settings to defaults |
.schema <table> |
Show table schema |
.share |
Open sharing modal to share queries via URL |
.tables |
List all tables |
.theme dark|light |
Switch color theme (clears screen) |
.timer on|off |
Toggle query timing |
The terminal includes an interactive charting feature powered by uPlot. Charts are displayed as an overlay on top of the terminal and support hover tooltips, legends, and PNG export.
Note: Charts must be enabled in the configuration with
enableCharts: true. The uPlot library (~50KB) is loaded from CDN on first use.
-- Run a query, then visualize it
SELECT date, revenue, cost FROM sales;
.chart
The chart type is automatically detected based on your data:
| Data Pattern | Chart Type | Example |
|---|---|---|
| Temporal + Numeric columns | Line | DATE + revenue → time series |
| Categorical + Numeric columns | Bar | category + total → bar chart |
| Two Numeric columns only | Scatter | x_value + y_value → scatter plot |
| Single Numeric column | Histogram | value → distribution histogram |
| Multiple Numeric columns | Line | col1, col2, col3 → multi-series line |
| Option | Description | Example |
|---|---|---|
type=TYPE |
Force chart type: line, bar, scatter, histogram |
.chart type=bar |
x=COLUMN |
Specify X-axis column | .chart x=date |
y=COLUMN |
Specify Y-axis column(s), comma-separated | .chart y=revenue,cost |
export |
Export chart as PNG | .chart export |
-- Line chart: Time series with multiple series
SELECT DATE '2024-01-01' + INTERVAL (i) DAY AS date,
100 + random() * 50 AS revenue,
80 + random() * 30 AS cost
FROM generate_series(0, 11) AS t(i);
.chart
-- Bar chart: Categorical data
SELECT category, SUM(amount) as total
FROM (VALUES
('Electronics', 1500),
('Clothing', 800),
('Food', 1200),
('Books', 400),
('Toys', 600)
) AS t(category, amount)
GROUP BY category;
.chart
-- Scatter plot: Two numeric columns
SELECT random() * 100 AS x_value,
random() * 100 AS y_value
FROM generate_series(1, 50);
.chart
-- Histogram: Single numeric column distribution
SELECT (random() * 100)::INTEGER AS value
FROM generate_series(1, 200);
.chart
-- Multi-series bar chart
SELECT region,
SUM(CASE WHEN product = 'A' THEN sales ELSE 0 END) as product_a,
SUM(CASE WHEN product = 'B' THEN sales ELSE 0 END) as product_b
FROM (VALUES
('North', 'A', 120), ('North', 'B', 90),
('South', 'A', 80), ('South', 'B', 150),
('East', 'A', 200), ('East', 'B', 110),
('West', 'A', 95), ('West', 'B', 130)
) AS t(region, product, sales)
GROUP BY region;
.chart
-- Line chart with numeric X axis (sine/cosine waves)
SELECT i AS x,
sin(i * 0.5) * 50 + 50 AS sine_wave,
cos(i * 0.5) * 50 + 50 AS cosine_wave
FROM generate_series(0, 20) AS t(i);
.chart
-- Force specific chart type and axes
.chart type=line x=date y=revenue,cost
| Key | Action |
|---|---|
| Hover | Show tooltip with values at cursor position |
ESC |
Close the chart |
Ctrl+S / Cmd+S |
Export chart as PNG |
Share SQL queries via URL to collaborate with others. When someone opens a shared link, the queries execute automatically.
Ctrl+Shift+S (or Cmd+Shift+S on Mac)Space to select/deselect queriesShared URLs use a hash-based format:
https://duckdb-terminal.com/#$queries=v1,ENCODED_QUERY_1,ENCODED_QUERY_2,...
| Key | Action |
|---|---|
Space |
Select/deselect query |
↑ / ↓ |
Navigate query list |
Enter |
Copy shareable link |
Escape |
Close modal |
Generate SQL queries from natural language using a text-to-sql proxy. This feature requires running the Text-to-SQL Proxy locally or on a server.
Download precompiled binaries from the releases page for your platform (Linux, macOS, Windows).
Quick start:
# Download and extract the binary for your platform, then run:
./text-to-sql-proxy
# Or with a specific provider API key:
ANTHROPIC_API_KEY=your-key ./text-to-sql-proxy
OPENAI_API_KEY=your-key ./text-to-sql-proxy
The proxy starts on http://localhost:4000 by default. See the proxy documentation for more configuration options.
http://localhost:4000 by default-- Create some tables first
CREATE TABLE users (id INTEGER, name VARCHAR, email VARCHAR);
CREATE TABLE orders (id INTEGER, user_id INTEGER, total DECIMAL, created_at DATE);
-- Generate SQL from natural language
.ai show all users
-- Or use the explicit query subcommand
.ai query find orders over 100 dollars from last month
The generated SQL is displayed with syntax highlighting and then executed automatically.
| Command | Description |
|---|---|
.ai <question> |
Generate SQL from natural language (shorthand) |
.ai query <question> |
Generate SQL from natural language |
.ai provider list |
List available AI providers from the proxy |
.ai provider set <name> |
Set the AI provider (e.g., claude, openai) |
.ai endpoint get |
Show current proxy endpoint URL |
.ai endpoint set <url> |
Change the proxy endpoint URL |
Settings are persisted in localStorage:
http://localhost:4000claude-- Check current endpoint
.ai endpoint get
-- Change to a different proxy
.ai endpoint set http://my-proxy:8000
-- List available providers
.ai provider list
-- Switch provider
.ai provider set openai
"Please create tables or views to be able to use the Text-to-SQL features""Please install and run the Text-to-SQL Proxy"| Key | Action |
|---|---|
Enter |
Execute command/SQL |
Tab |
Auto-complete |
Backspace |
Delete character before cursor |
Delete |
Delete character at cursor |
↑ / ↓ |
Navigate history |
← / → |
Move cursor |
Home |
Move to start of line |
End |
Move to end of line |
Ctrl+A |
Move to start of line |
Ctrl+E |
Move to end of line |
Ctrl+K |
Clear from cursor to end of line |
Ctrl+U |
Clear entire line |
Ctrl+V |
Paste from clipboard |
Ctrl+C |
Cancel current input or running query |
When viewing paginated results, the following keys are available:
| Key | Action |
|---|---|
n / ↓ / Enter |
Next page |
p / ↑ |
Previous page |
q / Escape / Ctrl+C |
Quit pagination |
-- Create a table
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name VARCHAR,
email VARCHAR
);
-- Insert data
INSERT INTO users VALUES
(1, 'Alice', 'alice@example.com'),
(2, 'Bob', 'bob@example.com');
-- Query data
SELECT * FROM users WHERE name LIKE 'A%';
-- Use built-in functions
SELECT range(10), current_timestamp;
The terminal supports four output formats and provides an easy way to copy query results to your clipboard.
Use the .mode command to switch between output formats:
-- Table format (default) - human-readable ASCII table
.mode table
SELECT * FROM users;
+----+-------+-------------------+
| id | name | email |
+----+-------+-------------------+
| 1 | Alice | alice@example.com |
| 2 | Bob | bob@example.com |
+----+-------+-------------------+
-- CSV format - comma-separated values
.mode csv
SELECT * FROM users;
id,name,email
1,Alice,alice@example.com
2,Bob,bob@example.com
-- TSV format - tab-separated values
.mode tsv
SELECT * FROM users;
id name email
1 Alice alice@example.com
2 Bob bob@example.com
-- JSON format - array of objects
.mode json
SELECT * FROM users;
[
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"}
]
After running a query, use .copy to copy the results to your clipboard in the current output format:
-- Run your query
SELECT * FROM users WHERE active = true;
-- Copy results to clipboard (uses current .mode format)
.copy
The copied content respects your current output mode, so you can:
.mode csv or .mode tsv then .copy to paste into a spreadsheet.mode json then .copy to paste into a JSON file or API tool.mode table then .copy to paste formatted output into documentationLarge result sets are paginated automatically, with 1,000 rows per page by default. Each page fetches one extra row to determine whether a next page exists, avoiding a full count before the first page. Use .pagesize to select a smaller page:
-- Use 50 rows per page
.pagesize 50
-- Run a query with many results
SELECT * FROM large_table;
-- Navigate pages:
-- n or Down Arrow - next page
-- p or Up Arrow - previous page
-- 1-9 - jump to page number
-- q - quit pagination
Set .pagesize 0 to restore the configured safe maximum (maxDisplayRows, 1,000 by default). Unlimited terminal rendering is intentionally not available because it can exhaust browser memory or overwhelm the terminal renderer.
Queries that already contain LIMIT or OFFSET are wrapped safely, so their result can still be paginated without changing their meaning. Statements that cannot be wrapped are streamed only until the safe maximum and are marked as truncated when more rows exist.
The terminal reports the current page and whether previous or next pages exist. Exact totals become available automatically when the last page is reached. Set exactRowCount: true if your application requires exact totals before page 1; this runs a separate COUNT(*) query and can substantially increase work for expensive queries.
Programmatic executeSQL() calls, queryEnd events, .copy, .download, and charts use the current page. The returned QueryResult.pagination object always reports page navigation state and includes totalRows and totalPages once known.
You can create custom themes by providing a Theme object instead of 'dark' or 'light':
import { createTerminal, type Theme, type ThemeColors } from 'duckdb-terminal';
// Define custom colors
const myColors: ThemeColors = {
background: '#1a1b26', // Terminal background
foreground: '#a9b1d6', // Default text color
cursor: '#c0caf5', // Cursor color
selection: '#33467c', // Selection highlight
// Standard ANSI colors
black: '#15161e',
red: '#f7768e',
green: '#9ece6a',
yellow: '#e0af68',
blue: '#7aa2f7',
magenta: '#bb9af7',
cyan: '#7dcfff',
white: '#a9b1d6',
// Bright variants
brightBlack: '#414868',
brightRed: '#f7768e',
brightGreen: '#9ece6a',
brightYellow: '#e0af68',
brightBlue: '#7aa2f7',
brightMagenta: '#bb9af7',
brightCyan: '#7dcfff',
brightWhite: '#c0caf5',
};
// Create theme object
const tokyoNight: Theme = {
name: 'tokyo-night',
colors: myColors,
};
// Use custom theme
const Terminal = await createTerminal({
container: '#terminal',
theme: tokyoNight,
});
// You can also change theme at runtime
Terminal.setTheme(tokyoNight);
| Property | Description | ANSI Code |
|---|---|---|
background |
Terminal background color | - |
foreground |
Default text color | - |
cursor |
Cursor color | - |
selection |
Text selection highlight | - |
black |
ANSI black | \x1b[30m |
red |
ANSI red | \x1b[31m |
green |
ANSI green | \x1b[32m |
yellow |
ANSI yellow | \x1b[33m |
blue |
ANSI blue | \x1b[34m |
magenta |
ANSI magenta | \x1b[35m |
cyan |
ANSI cyan | \x1b[36m |
white |
ANSI white | \x1b[37m |
brightBlack |
Bright black (gray) | \x1b[90m |
brightRed |
Bright red | \x1b[91m |
brightGreen |
Bright green | \x1b[92m |
brightYellow |
Bright yellow | \x1b[93m |
brightBlue |
Bright blue | \x1b[94m |
brightMagenta |
Bright magenta | \x1b[95m |
brightCyan |
Bright cyan | \x1b[96m |
brightWhite |
Bright white | \x1b[97m |
The library exports two built-in themes that you can use directly or extend:
import { darkTheme, lightTheme } from 'duckdb-terminal';
// Use directly
const Terminal = await createTerminal({
container: '#terminal',
theme: darkTheme,
});
// Or extend
const myTheme: Theme = {
name: 'my-dark',
colors: {
...darkTheme.colors,
background: '#000000', // Override specific colors
cursor: '#ff0000',
},
};
Creates and starts a DuckDB Terminal instance.
import { createTerminal } from 'duckdb-terminal';
const Terminal = await createTerminal({
container: document.getElementById('terminal'),
theme: 'dark',
});
// Write to terminal
Terminal.write('Hello, World!');
Terminal.writeln('With newline');
// Execute SQL programmatically
const result = await Terminal.executeSQL('SELECT 1+1 as answer');
console.log(result); // { columns: ['answer'], rows: [[2]], rowCount: 1, duration: 5 }
// Change theme
Terminal.setTheme('light');
// Clear terminal
Terminal.clear();
// Clean up when done (removes event listeners, closes database)
Terminal.destroy();
The Terminal emits events that you can subscribe to for monitoring and integrating with your application:
import { createTerminal, type TerminalEvents } from 'duckdb-terminal';
const Terminal = await createTerminal({
container: '#terminal',
theme: 'dark',
});
// Subscribe to events
Terminal.on('ready', () => {
console.log('Terminal is ready!');
});
Terminal.on('queryStart', ({ sql }) => {
console.log('Executing:', sql);
});
Terminal.on('queryEnd', ({ sql, result, error, duration }) => {
if (error) {
console.error('Query failed:', error);
} else {
console.log(`Query completed in ${duration}ms, ${result?.rowCount} rows`);
}
});
Terminal.on('stateChange', ({ state, previous }) => {
console.log(`State changed: ${previous} -> ${state}`);
});
Terminal.on('themeChange', ({ theme, previous }) => {
console.log(`Theme changed to: ${theme.name}`);
});
Terminal.on('fileLoaded', ({ filename, size, type }) => {
console.log(`Loaded file: ${filename} (${size} bytes)`);
});
Terminal.on('commandExecute', ({ command, args }) => {
console.log(`Command: ${command}`, args);
});
Terminal.on('error', ({ message, source }) => {
console.error(`Error from ${source}: ${message}`);
});
// Unsubscribe using the returned function
const unsubscribe = Terminal.on('queryEnd', handler);
unsubscribe(); // Stop listening
// Or use off() directly
Terminal.off('queryEnd', handler);
| Event | Payload | Description |
|---|---|---|
ready |
{} |
Terminal is fully initialized |
queryStart |
{ sql } |
SQL query execution started |
queryEnd |
{ sql, result, error?, duration } |
SQL query completed or failed |
stateChange |
{ state, previous } |
Terminal state changed (idle, collecting, executing, paginating) |
themeChange |
{ theme, previous } |
Theme was changed |
fileLoaded |
{ filename, size, type } |
File was loaded via drag-drop or picker |
commandExecute |
{ command, args } |
Dot command was executed |
error |
{ message, source } |
An error occurred |
import {
Terminal,
Database,
TerminalAdapter,
formatTable,
formatCSV,
formatJSON
} from 'duckdb-terminal';
// Use Database directly
const db = new Database({ storage: 'memory' });
await db.init();
const result = await db.executeQuery('SELECT 42');
// Use formatters
console.log(formatTable(result.columns, result.rows));
console.log(formatCSV(result.columns, result.rows));
console.log(formatJSON(result.columns, result.rows));
// Get completions
const suggestions = await db.getCompletions('SEL', 3);
SharedArrayBuffer or cross-origin isolationSharedArrayBuffer, WebAssembly threads, a COI bundle, and COOP/COEP headersEmbedding applications that enable multithreading must configure their development and production servers with the isolation headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
# Development
npm run dev
# Production build
npm run build
# Run tests
npm test
# Type checking
npm run typecheck
| File | Format | Usage |
|---|---|---|
dist/duckdb-terminal.js |
ESM | Modern bundlers |
dist/duckdb-terminal.umd.cjs |
UMD | Script tags, legacy |
dist/*.d.ts |
TypeScript | Type definitions |
MIT