├── .DS_Store
├── .github
├── FUNDING.yml
└── workflows
│ └── build-electron.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .tool-versions
├── README.md
├── build
├── entitlements.mac.plist
├── icon.png
└── notarize.js
├── eslint.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── scripts
└── repush-tag.sh
└── src
├── assets
└── icon.png
└── main.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcboles/duckdb-ui/2024ea609271ab6ee0751c1663bf365d1e8de07f/.DS_Store
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [tcboles] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/workflows/build-electron.yml:
--------------------------------------------------------------------------------
1 | name: Build & Release Electron App
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*' # Triggers only on version tags like v1.2.2
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | build:
13 | name: Build on ${{ matrix.os }}
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | matrix:
17 | include:
18 | - name: macOS M1 (arm64)
19 | os: macos-latest
20 | build_script: build:mac:arm64
21 |
22 | - name: macOS Intel (x64)
23 | os: macos-13
24 | build_script: build:mac:x64
25 |
26 | - name: Windows
27 | os: windows-latest
28 | build_script: build:win
29 |
30 | - name: Linux
31 | os: ubuntu-latest
32 | build_script: build:linux
33 |
34 | env:
35 | APPLE_ID: ${{ secrets.APPLE_ID }}
36 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
37 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
38 | ELECTRON_NOTARIZE_USE_NOTARYTOOL: "true"
39 | npm_config_build_from_source: "true"
40 |
41 | steps:
42 | - name: Checkout code
43 | uses: actions/checkout@v4
44 |
45 | - name: Set up Node.js
46 | uses: actions/setup-node@v4
47 | with:
48 | node-version: 20
49 |
50 | - name: Install dependencies
51 | run: npm install
52 |
53 | - name: Import macOS signing certificate
54 | if: contains(matrix.os, 'macos')
55 | run: |
56 | echo "${{ secrets.CERTIFICATE_P12 }}" | base64 --decode > certificate.p12
57 | security create-keychain -p "" build.keychain
58 | security default-keychain -s build.keychain
59 | security unlock-keychain -p "" build.keychain
60 | security import certificate.p12 -k build.keychain -P "${{ secrets.CERTIFICATE_PASSWORD }}" -T /usr/bin/codesign
61 | security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
62 |
63 | - name: Build Electron App
64 | run: npm run ${{ matrix.build_script }}
65 | shell: bash
66 |
67 | - name: Upload artifacts
68 | uses: actions/upload-artifact@v4
69 | with:
70 | name: ${{ matrix.name }}-build
71 | path: |
72 | dist/*.dmg
73 | dist/*.exe
74 | dist/*.AppImage
75 | dist/*.deb
76 | dist/*.zip
77 | dist/*.tar.gz
78 | if-no-files-found: ignore
79 |
80 | release:
81 | name: Create GitHub Release
82 | needs: build
83 | runs-on: ubuntu-latest
84 |
85 | steps:
86 | - name: Download all artifacts
87 | uses: actions/download-artifact@v4
88 | with:
89 | path: ./artifacts
90 |
91 | - name: Create GitHub Release
92 | uses: softprops/action-gh-release@v2
93 | with:
94 | tag_name: ${{ github.ref_name }}
95 | files: ./artifacts/**/*.*
96 | env:
97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | .envrc
4 | dist
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | eslint.config.js
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "printWidth": 100,
4 | "trailingComma": "none"
5 | }
6 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 22.13.1
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DuckDB UI (Unofficial Desktop Wrapper)
2 |
3 | > A standalone cross-platform wrapper for the DuckDB UI using Electron.
4 |
5 | ⚠️ **Disclaimer:** I am not affiliated with the DuckDB project. This is an unofficial desktop client built around the [DuckDB web UI](https://duckdb.org/docs/stable/extensions/ui). All credits for DuckDB itself go to the official DuckDB team.
6 |
7 | ---
8 |
9 | ## 🚀 Latest Downloads
10 |
11 | ### 🔖 Version v1.2.2
12 | - [MacOS (Apple Silicon)](https://github.com/tcboles/duckdb-ui/releases/download/v1.2.2-0/DuckDB-1.2.2-0-mac-arm64.dmg)
13 | - [MacOS (intel)](https://github.com/tcboles/duckdb-ui/releases/download/v1.2.2-0/DuckDB-1.2.2-0-mac-x64.dmg)
14 | - [Windows](https://github.com/tcboles/duckdb-ui/releases/download/v1.2.2-0/DuckDB.Setup-1.2.2-0-win-x64.exe)
15 | - [Linux](https://github.com/tcboles/duckdb-ui/releases/download/v1.2.2-0/DuckDB-1.2.2-0-linux-x86_64.AppImage)
16 |
17 | ### 🔖 Version v1.2.1
18 | - [MacOS (Apple Silicon)](https://github.com/tcboles/duckdb-ui/releases/download/v1.2.1-4/DuckDB-1.2.1-4-mac-arm64.dmg)
19 | - [MacOS (intel)](https://github.com/tcboles/duckdb-ui/releases/download/v1.2.1-4/DuckDB-1.2.1-4-mac-x64.dmg)
20 | - [Windows](https://github.com/tcboles/duckdb-ui/releases/download/v1.2.1-4/DuckDB.Setup-1.2.1-4-win-x64.exe)
21 | - [Linux](https://github.com/tcboles/duckdb-ui/releases/download/v1.2.1-4/DuckDB-1.2.1-4-linux-x86_64.AppImage)
22 |
23 | ---
24 |
25 | ## 🛠 Features
26 | - Full-featured Electron wrapper around the DuckDB UI
27 | - Cross-platform: macOS, Windows, Linux
28 | - Easy to launch DuckDB locally with no setup
29 |
30 | ---
31 |
32 | ## 🧪 Why?
33 | DuckDB ships an amazing browser-based UI, but this wrapper lets you run it natively like a desktop app.
34 |
35 | ---
36 |
37 | ## 📬 Feedback
38 | Feel free to open issues or feature requests. PRs welcome!
--------------------------------------------------------------------------------
/build/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | com.apple.security.cs.allow-jit
7 |
8 | com.apple.security.cs.disable-library-validation
9 |
10 | com.apple.security.app-sandbox
11 |
12 |
13 |
--------------------------------------------------------------------------------
/build/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcboles/duckdb-ui/2024ea609271ab6ee0751c1663bf365d1e8de07f/build/icon.png
--------------------------------------------------------------------------------
/build/notarize.js:
--------------------------------------------------------------------------------
1 | // build/notarize.js
2 | import { notarize } from 'electron-notarize';
3 |
4 | export default async function notarizing(context) {
5 | const { electronPlatformName, appOutDir } = context;
6 |
7 | process.env.ELECTRON_NOTARIZE_USE_NOTARYTOOL = 'true';
8 |
9 | if (electronPlatformName !== 'darwin') {
10 | console.log('Not a macOS build. Skipping notarization.');
11 | return;
12 | }
13 |
14 | const appName = context.packager.appInfo.productFilename;
15 | const appPath = `${appOutDir}/${appName}.app`;
16 |
17 | const { APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID } = process.env;
18 |
19 | if (!APPLE_ID || !APPLE_APP_SPECIFIC_PASSWORD || !APPLE_TEAM_ID) {
20 | console.error(
21 | '❌ Missing Apple credentials. Make sure APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID are set.'
22 | );
23 | process.exit(1);
24 | }
25 |
26 | console.log(`🔐 Notarizing ${appPath}...`);
27 |
28 | try {
29 | await notarize({
30 | appBundleId: 'com.tcboles.duckdb',
31 | appPath,
32 | appleId: APPLE_ID,
33 | appleIdPassword: APPLE_APP_SPECIFIC_PASSWORD,
34 | teamId: APPLE_TEAM_ID,
35 | tool: 'notarytool'
36 | });
37 |
38 | console.log('✅ Notarization complete!');
39 | } catch (error) {
40 | console.error('❌ Notarization failed:');
41 | console.error(error);
42 | process.exit(1);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | const eslintConfigPrettier = require('eslint-config-prettier');
2 | const unusedImports = require('eslint-plugin-unused-imports');
3 |
4 | module.exports = [
5 | eslintConfigPrettier,
6 | {
7 | plugins: {
8 | 'unused-imports': unusedImports
9 | },
10 | rules: {
11 | 'require-atomic-updates': 0,
12 | 'no-async-promise-executor': 0,
13 | 'comma-dangle': ['error', 'never'],
14 | 'no-unused-vars': 'off', // or '@typescript-eslint/no-unused-vars': 'off',
15 | 'unused-imports/no-unused-imports': 'error',
16 | 'unused-imports/no-unused-vars': [
17 | 'error',
18 | {
19 | 'vars': 'all',
20 | 'varsIgnorePattern': '^_',
21 | 'args': 'after-used',
22 | 'argsIgnorePattern': '^_'
23 | }
24 | ]
25 | }
26 | }
27 | ];
28 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "baseUrl": ".",
6 | "paths": {
7 | "~/*": ["./src/*"]
8 | }
9 | },
10 | "include": ["./src/**/*", "jsconfig.json"]
11 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "DuckDB",
3 | "productName": "DuckDB",
4 | "description": "DuckDB",
5 | "version": "1.2.2-0",
6 | "type": "module",
7 | "main": "src/main.js",
8 | "build": {
9 | "appId": "com.tcboles.duckdb",
10 | "icon": "src/assets/icon.png",
11 | "mac": {
12 | "target": {
13 | "target": "dmg"
14 | },
15 | "identity": "Thomas Boles",
16 | "hardenedRuntime": true,
17 | "gatekeeperAssess": false,
18 | "entitlements": "build/entitlements.mac.plist",
19 | "entitlementsInherit": "build/entitlements.mac.plist",
20 | "artifactName": "${productName}-${version}-mac-${arch}.${ext}"
21 | },
22 | "afterSign": "build/notarize.js",
23 | "win": {
24 | "target": "nsis",
25 | "icon": "src/assets/icon.ico",
26 | "artifactName": "${productName}.Setup-${version}-win-${arch}.${ext}"
27 | },
28 | "linux": {
29 | "target": "AppImage",
30 | "category": "Utility",
31 | "artifactName": "${productName}-${version}-linux-${arch}.${ext}"
32 | },
33 | "files": [
34 | "src/**",
35 | "build/**"
36 | ]
37 | },
38 | "scripts": {
39 | "dev": "nodemon --watch . --ext js,jsx,ts,tsx --exec \"electron .\"",
40 | "start": "electron .",
41 | "build:mac": "electron-builder --mac --publish never",
42 | "build:mac:arm64": "electron-builder --mac --arm64 --publish never",
43 | "build:mac:x64": "electron-builder --mac --x64 --publish never",
44 | "build:win": "electron-builder --win --publish never",
45 | "build:linux": "electron-builder --linux --publish never",
46 | "lint": "eslint --fix ./src",
47 | "repush-tag": "bash ./scripts/repush-tag.sh"
48 | },
49 | "author": "Thomas Boles (https://github.com/tcboles)",
50 | "license": "MIT",
51 | "devDependencies": {
52 | "electron": "^35.1.1",
53 | "electron-builder": "^26.0.12",
54 | "electron-notarize": "^1.2.2",
55 | "eslint": "^9.9.0",
56 | "eslint-config-prettier": "^10.1.1",
57 | "eslint-plugin-prettier": "^5.2.1",
58 | "eslint-plugin-unused-imports": "^4.1.3",
59 | "nodemon": "^3.1.9",
60 | "prettier": "^3.3.3"
61 | },
62 | "dependencies": {
63 | "@duckdb/node-api": "^1.2.2-alpha.18"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/scripts/repush-tag.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | TAG=$(git describe --tags --abbrev=0)
6 |
7 | echo "Repushing tag: $TAG"
8 |
9 | # Delete the local and remote tag
10 | git tag -d "$TAG"
11 | git push origin ":refs/tags/$TAG"
12 |
13 | # Recreate and push the tag
14 | git tag "$TAG"
15 | git push origin "$TAG"
16 |
17 | echo "✅ Tag $TAG re-pushed successfully"
--------------------------------------------------------------------------------
/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcboles/duckdb-ui/2024ea609271ab6ee0751c1663bf365d1e8de07f/src/assets/icon.png
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { DuckDBInstance } from '@duckdb/node-api';
2 | import { app, BrowserWindow } from 'electron';
3 | import path from 'path';
4 | import { fileURLToPath } from 'url';
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = path.dirname(__filename);
8 |
9 | function createWindow() {
10 | const win = new BrowserWindow({
11 | width: 1200,
12 | height: 1000,
13 | title: `DuckDB (Unofficial)`,
14 | icon: path.join(__dirname, 'assets/icon.png'),
15 | webPreferences: {
16 | nodeIntegration: false,
17 | contextIsolation: true,
18 | partition: 'persist:duckdb'
19 | }
20 | });
21 |
22 | win.loadURL('http://localhost:4213');
23 | }
24 |
25 | if (process.platform === 'darwin') {
26 | app.dock.setIcon(path.join(__dirname, 'assets/icon.png'));
27 | }
28 |
29 | let db;
30 |
31 | async function startServers() {
32 | const instance = await DuckDBInstance.create();
33 | db = await instance.connect();
34 | await db.run('CALL start_ui_server();');
35 |
36 | // Optionally, wait a few seconds to ensure the servers are up before creating the window.
37 | createWindow();
38 | }
39 |
40 | app.whenReady().then(async () => {
41 | // Set the application name (macOS uses this for the app menu)
42 | app.setName('DuckDB (Unofficial)');
43 |
44 | await startServers();
45 |
46 | app.on('activate', () => {
47 | if (BrowserWindow.getAllWindows().length === 0) createWindow();
48 | });
49 | });
50 |
51 | app.on('window-all-closed', () => {
52 | if (process.platform !== 'darwin') app.quit();
53 | });
54 |
55 | app.on('quit', async () => {
56 | db?.close?.();
57 | });
58 |
--------------------------------------------------------------------------------