├── .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 | --------------------------------------------------------------------------------