Skip to content

How to Use SQLite in Vue 3: Complete Guide to Offline-First Web Apps

Published: at 

Want to build web apps that work offline? While browsers have built-in storage options like IndexedDB, they’re not great for complex data operations. This guide shows you how to use SQLite with Vue 3 to build powerful offline-first apps.

📚 What We’ll Build

Screenshot Sqlite Playground Try it out: Write and run SQL queries right in your browser

🚀 Want the code? Get the complete example at github.com/alexanderop/sqlite-vue-example

🗃️ Why SQLite?

Browser storage like IndexedDB is okay, but SQLite is better because:

🛠️ How It Works

We’ll use three main technologies:

  1. SQLite Wasm: SQLite converted to run in browsers
  2. Web Workers: Runs database code without freezing your app
  3. Origin Private File System: A secure place to store your database

Here’s how they work together:

How SQLite runs in your browser

📝 Implementation Guide

Let’s build this step by step, starting with the core SQLite functionality and then creating a playground to test it.

Step 1: Install Dependencies

First, install the required SQLite WASM package:

npm install @sqlite.org/sqlite-wasm

Step 2: Configure Vite

Create or update your vite.config.ts file to support WebAssembly and cross-origin isolation:


import { function defineConfig(config: UserConfig): UserConfig (+3 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
} from 'vite'
export default function defineConfig(config: UserConfigFnObject): UserConfigFnObject (+3 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
(() => ({
UserConfig.server?: ServerOptions | undefined
Server specific options, e.g. host, port, https...
server
: {
CommonServerOptions.headers?: OutgoingHttpHeaders | undefined
Specify server response headers.
headers
: {
'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', }, }, UserConfig.optimizeDeps?: DepOptimizationOptions | undefined
Dep optimization options
optimizeDeps
: {
DepOptimizationConfig.exclude?: string[] | undefined
Do not optimize these dependencies (must be resolvable import paths, cannot be globs).
exclude
: ['@sqlite.org/sqlite-wasm'],
}, }))

This configuration is crucial for SQLite WASM to work properly:

Step 3: Add TypeScript Types

Since @sqlite.org/sqlite-wasm doesn’t include TypeScript types for Sqlite3Worker1PromiserConfig, we need to create our own. Create a new file types/sqlite-wasm.d.ts:

Define this as a d.ts file so that TypeScript knows about it.

import type { class Worker
The `Worker` class represents an independent JavaScript execution thread. Most Node.js APIs are available inside of it. Notable differences inside a Worker environment are: * The `process.stdin`, `process.stdout`, and `process.stderr` streams may be redirected by the parent thread. * The `require('node:worker_threads').isMainThread` property is set to `false`. * The `require('node:worker_threads').parentPort` message port is available. * `process.exit()` does not stop the whole program, just the single thread, and `process.abort()` is not available. * `process.chdir()` and `process` methods that set group or user ids are not available. * `process.env` is a copy of the parent thread's environment variables, unless otherwise specified. Changes to one copy are not visible in other threads, and are not visible to native add-ons (unless `worker.SHARE_ENV` is passed as the `env` option to the `Worker` constructor). On Windows, unlike the main thread, a copy of the environment variables operates in a case-sensitive manner. * `process.title` cannot be modified. * Signals are not delivered through `process.on('...')`. * Execution may stop at any point as a result of `worker.terminate()` being invoked. * IPC channels from parent processes are not accessible. * The `trace_events` module is not supported. * Native add-ons can only be loaded from multiple threads if they fulfill `certain conditions`. Creating `Worker` instances inside of other `Worker`s is possible. Like [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) and the `node:cluster module`, two-way communication can be achieved through inter-thread message passing. Internally, a `Worker` has a built-in pair of `MessagePort` s that are already associated with each other when the `Worker` is created. While the `MessagePort` object on the parent side is not directly exposed, its functionalities are exposed through `worker.postMessage()` and the `worker.on('message')` event on the `Worker` object for the parent thread. To create custom messaging channels (which is encouraged over using the default global channel because it facilitates separation of concerns), users can create a `MessageChannel` object on either thread and pass one of the`MessagePort`s on that `MessageChannel` to the other thread through a pre-existing channel, such as the global one. See `port.postMessage()` for more information on how messages are passed, and what kind of JavaScript values can be successfully transported through the thread barrier. ```js const assert = require('node:assert'); const { Worker, MessageChannel, MessagePort, isMainThread, parentPort, } = require('node:worker_threads'); if (isMainThread) { const worker = new Worker(__filename); const subChannel = new MessageChannel(); worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]); subChannel.port2.on('message', (value) => { console.log('received:', value); }); } else { parentPort.once('message', (value) => { assert(value.hereIsYourPort instanceof MessagePort); value.hereIsYourPort.postMessage('the worker is sending this'); value.hereIsYourPort.close(); }); } ```
@sincev10.5.0
Worker
} from 'node:worker_threads'
declare module '@sqlite.org/sqlite-wasm' { type type OnreadyFunction = () => voidOnreadyFunction = () => void type
type Sqlite3Worker1PromiserConfig = {
    onready?: OnreadyFunction;
    worker?: Worker | (() => Worker);
    generateMessageId?: (messageObject: unknown) => string;
    debug?: (...args: any[]) => void;
    onunhandled?: (event: MessageEvent) => void;
}
Sqlite3Worker1PromiserConfig
= {
onready?: OnreadyFunction | undefinedonready?: type OnreadyFunction = () => voidOnreadyFunction worker?: Worker | (() => Worker) | undefinedworker?: class Worker
The `Worker` class represents an independent JavaScript execution thread. Most Node.js APIs are available inside of it. Notable differences inside a Worker environment are: * The `process.stdin`, `process.stdout`, and `process.stderr` streams may be redirected by the parent thread. * The `require('node:worker_threads').isMainThread` property is set to `false`. * The `require('node:worker_threads').parentPort` message port is available. * `process.exit()` does not stop the whole program, just the single thread, and `process.abort()` is not available. * `process.chdir()` and `process` methods that set group or user ids are not available. * `process.env` is a copy of the parent thread's environment variables, unless otherwise specified. Changes to one copy are not visible in other threads, and are not visible to native add-ons (unless `worker.SHARE_ENV` is passed as the `env` option to the `Worker` constructor). On Windows, unlike the main thread, a copy of the environment variables operates in a case-sensitive manner. * `process.title` cannot be modified. * Signals are not delivered through `process.on('...')`. * Execution may stop at any point as a result of `worker.terminate()` being invoked. * IPC channels from parent processes are not accessible. * The `trace_events` module is not supported. * Native add-ons can only be loaded from multiple threads if they fulfill `certain conditions`. Creating `Worker` instances inside of other `Worker`s is possible. Like [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) and the `node:cluster module`, two-way communication can be achieved through inter-thread message passing. Internally, a `Worker` has a built-in pair of `MessagePort` s that are already associated with each other when the `Worker` is created. While the `MessagePort` object on the parent side is not directly exposed, its functionalities are exposed through `worker.postMessage()` and the `worker.on('message')` event on the `Worker` object for the parent thread. To create custom messaging channels (which is encouraged over using the default global channel because it facilitates separation of concerns), users can create a `MessageChannel` object on either thread and pass one of the`MessagePort`s on that `MessageChannel` to the other thread through a pre-existing channel, such as the global one. See `port.postMessage()` for more information on how messages are passed, and what kind of JavaScript values can be successfully transported through the thread barrier. ```js const assert = require('node:assert'); const { Worker, MessageChannel, MessagePort, isMainThread, parentPort, } = require('node:worker_threads'); if (isMainThread) { const worker = new Worker(__filename); const subChannel = new MessageChannel(); worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]); subChannel.port2.on('message', (value) => { console.log('received:', value); }); } else { parentPort.once('message', (value) => { assert(value.hereIsYourPort instanceof MessagePort); value.hereIsYourPort.postMessage('the worker is sending this'); value.hereIsYourPort.close(); }); } ```
@sincev10.5.0
Worker
| (() => class Worker
The `Worker` class represents an independent JavaScript execution thread. Most Node.js APIs are available inside of it. Notable differences inside a Worker environment are: * The `process.stdin`, `process.stdout`, and `process.stderr` streams may be redirected by the parent thread. * The `require('node:worker_threads').isMainThread` property is set to `false`. * The `require('node:worker_threads').parentPort` message port is available. * `process.exit()` does not stop the whole program, just the single thread, and `process.abort()` is not available. * `process.chdir()` and `process` methods that set group or user ids are not available. * `process.env` is a copy of the parent thread's environment variables, unless otherwise specified. Changes to one copy are not visible in other threads, and are not visible to native add-ons (unless `worker.SHARE_ENV` is passed as the `env` option to the `Worker` constructor). On Windows, unlike the main thread, a copy of the environment variables operates in a case-sensitive manner. * `process.title` cannot be modified. * Signals are not delivered through `process.on('...')`. * Execution may stop at any point as a result of `worker.terminate()` being invoked. * IPC channels from parent processes are not accessible. * The `trace_events` module is not supported. * Native add-ons can only be loaded from multiple threads if they fulfill `certain conditions`. Creating `Worker` instances inside of other `Worker`s is possible. Like [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) and the `node:cluster module`, two-way communication can be achieved through inter-thread message passing. Internally, a `Worker` has a built-in pair of `MessagePort` s that are already associated with each other when the `Worker` is created. While the `MessagePort` object on the parent side is not directly exposed, its functionalities are exposed through `worker.postMessage()` and the `worker.on('message')` event on the `Worker` object for the parent thread. To create custom messaging channels (which is encouraged over using the default global channel because it facilitates separation of concerns), users can create a `MessageChannel` object on either thread and pass one of the`MessagePort`s on that `MessageChannel` to the other thread through a pre-existing channel, such as the global one. See `port.postMessage()` for more information on how messages are passed, and what kind of JavaScript values can be successfully transported through the thread barrier. ```js const assert = require('node:assert'); const { Worker, MessageChannel, MessagePort, isMainThread, parentPort, } = require('node:worker_threads'); if (isMainThread) { const worker = new Worker(__filename); const subChannel = new MessageChannel(); worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]); subChannel.port2.on('message', (value) => { console.log('received:', value); }); } else { parentPort.once('message', (value) => { assert(value.hereIsYourPort instanceof MessagePort); value.hereIsYourPort.postMessage('the worker is sending this'); value.hereIsYourPort.close(); }); } ```
@sincev10.5.0
Worker
)
generateMessageId?: ((messageObject: unknown) => string) | undefinedgenerateMessageId?: (messageObject: unknownmessageObject: unknown) => string debug?: ((...args: any[]) => void) | undefineddebug?: (...args: any[]args: any[]) => void onunhandled?: ((event: MessageEvent) => void) | undefinedonunhandled?: (event: MessageEvent<any>event: interface MessageEvent<T = any>
A message received by a target object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent)
@sincev15.0.0
MessageEvent
) => void
} type type DbId = string | undefinedDbId = string | undefined type
type PromiserMethods = {
    'config-get': {
        args: Record<string, never>;
        result: {
            dbID: DbId;
            version: {
                libVersion: string;
                sourceId: string;
                libVersionNumber: number;
                downloadVersion: number;
            };
            bigIntEnabled: boolean;
            opfsEnabled: boolean;
            vfsList: string[];
        };
    };
    open: {
        args: Partial<{
            filename?: string;
            vfs?: string;
        }>;
        result: {
            dbId: DbId;
            filename: string;
            persistent: boolean;
            vfs: string;
        };
    };
    exec: {
        args: {
            sql: string;
            dbId?: DbId;
            bind?: unknown[];
            returnValue?: string;
        };
        result: {
            dbId: DbId;
            sql: string;
            bind: unknown[];
            returnValue: string;
            resultRows?: unknown[][];
        };
    };
}
PromiserMethods
= {
'config-get': { args: Record<string, never>args: type Record<K extends keyof any, T> = { [P in K]: T; }
Construct a type with a set of properties K of type T
Record
<string, never>
result: {
    dbID: DbId;
    version: {
        libVersion: string;
        sourceId: string;
        libVersionNumber: number;
        downloadVersion: number;
    };
    bigIntEnabled: boolean;
    opfsEnabled: boolean;
    vfsList: string[];
}
result
: {
dbID: DbIddbID: type DbId = string | undefinedDbId
version: {
    libVersion: string;
    sourceId: string;
    libVersionNumber: number;
    downloadVersion: number;
}
version
: {
libVersion: stringlibVersion: string sourceId: stringsourceId: string libVersionNumber: numberlibVersionNumber: number downloadVersion: numberdownloadVersion: number } bigIntEnabled: booleanbigIntEnabled: boolean opfsEnabled: booleanopfsEnabled: boolean vfsList: string[]vfsList: string[] } } 'open': {
args: Partial<{
    filename?: string;
    vfs?: string;
}>
args
: type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
Make all properties in T optional
Partial
<{
filename?: string | undefinedfilename?: string vfs?: string | undefinedvfs?: string }>
result: {
    dbId: DbId;
    filename: string;
    persistent: boolean;
    vfs: string;
}
result
: {
dbId: DbIddbId: type DbId = string | undefinedDbId filename: stringfilename: string persistent: booleanpersistent: boolean vfs: stringvfs: string } } 'exec': {
args: {
    sql: string;
    dbId?: DbId;
    bind?: unknown[];
    returnValue?: string;
}
args
: {
sql: stringsql: string dbId?: DbIddbId?: type DbId = string | undefinedDbId bind?: unknown[] | undefinedbind?: unknown[] returnValue?: string | undefinedreturnValue?: string }
result: {
    dbId: DbId;
    sql: string;
    bind: unknown[];
    returnValue: string;
    resultRows?: unknown[][];
}
result
: {
dbId: DbIddbId: type DbId = string | undefinedDbId sql: stringsql: string bind: unknown[]bind: unknown[] returnValue: stringreturnValue: string resultRows?: unknown[][] | undefinedresultRows?: unknown[][] } } } type
type PromiserResponseSuccess<T extends keyof PromiserMethods> = {
    type: T;
    result: PromiserMethods[T]["result"];
    messageId: string;
    dbId: DbId;
    workerReceivedTime: number;
    workerRespondTime: number;
    departureTime: number;
}
PromiserResponseSuccess
<function (type parameter) T in type PromiserResponseSuccess<T extends keyof PromiserMethods>T extends keyof
type PromiserMethods = {
    'config-get': {
        args: Record<string, never>;
        result: {
            dbID: DbId;
            version: {
                libVersion: string;
                sourceId: string;
                libVersionNumber: number;
                downloadVersion: number;
            };
            bigIntEnabled: boolean;
            opfsEnabled: boolean;
            vfsList: string[];
        };
    };
    open: {
        args: Partial<{
            filename?: string;
            vfs?: string;
        }>;
        result: {
            dbId: DbId;
            filename: string;
            persistent: boolean;
            vfs: string;
        };
    };
    exec: {
        args: {
            sql: string;
            dbId?: DbId;
            bind?: unknown[];
            returnValue?: string;
        };
        result: {
            dbId: DbId;
            sql: string;
            bind: unknown[];
            returnValue: string;
            resultRows?: unknown[][];
        };
    };
}
PromiserMethods
> = {
type: T extends keyof PromiserMethodstype: function (type parameter) T in type PromiserResponseSuccess<T extends keyof PromiserMethods>T result: PromiserMethods[T]["result"]result:
type PromiserMethods = {
    'config-get': {
        args: Record<string, never>;
        result: {
            dbID: DbId;
            version: {
                libVersion: string;
                sourceId: string;
                libVersionNumber: number;
                downloadVersion: number;
            };
            bigIntEnabled: boolean;
            opfsEnabled: boolean;
            vfsList: string[];
        };
    };
    open: {
        args: Partial<{
            filename?: string;
            vfs?: string;
        }>;
        result: {
            dbId: DbId;
            filename: string;
            persistent: boolean;
            vfs: string;
        };
    };
    exec: {
        args: {
            sql: string;
            dbId?: DbId;
            bind?: unknown[];
            returnValue?: string;
        };
        result: {
            dbId: DbId;
            sql: string;
            bind: unknown[];
            returnValue: string;
            resultRows?: unknown[][];
        };
    };
}
PromiserMethods
[function (type parameter) T in type PromiserResponseSuccess<T extends keyof PromiserMethods>T]['result']
messageId: stringmessageId: string dbId: DbIddbId: type DbId = string | undefinedDbId workerReceivedTime: numberworkerReceivedTime: number workerRespondTime: numberworkerRespondTime: number departureTime: numberdepartureTime: number } type
type PromiserResponseError = {
    type: "error";
    result: {
        operation: string;
        message: string;
        errorClass: string;
        input: object;
        stack: unknown[];
    };
    messageId: string;
    dbId: DbId;
}
PromiserResponseError
= {
type: "error"type: 'error'
result: {
    operation: string;
    message: string;
    errorClass: string;
    input: object;
    stack: unknown[];
}
result
: {
operation: stringoperation: string message: stringmessage: string errorClass: stringerrorClass: string input: objectinput: object stack: unknown[]stack: unknown[] } messageId: stringmessageId: string dbId: DbIddbId: type DbId = string | undefinedDbId } type type PromiserResponse<T extends keyof PromiserMethods> = PromiserResponseError | PromiserResponseSuccess<T>PromiserResponse<function (type parameter) T in type PromiserResponse<T extends keyof PromiserMethods>T extends keyof
type PromiserMethods = {
    'config-get': {
        args: Record<string, never>;
        result: {
            dbID: DbId;
            version: {
                libVersion: string;
                sourceId: string;
                libVersionNumber: number;
                downloadVersion: number;
            };
            bigIntEnabled: boolean;
            opfsEnabled: boolean;
            vfsList: string[];
        };
    };
    open: {
        args: Partial<{
            filename?: string;
            vfs?: string;
        }>;
        result: {
            dbId: DbId;
            filename: string;
            persistent: boolean;
            vfs: string;
        };
    };
    exec: {
        args: {
            sql: string;
            dbId?: DbId;
            bind?: unknown[];
            returnValue?: string;
        };
        result: {
            dbId: DbId;
            sql: string;
            bind: unknown[];
            returnValue: string;
            resultRows?: unknown[][];
        };
    };
}
PromiserMethods
> =
|
type PromiserResponseSuccess<T extends keyof PromiserMethods> = {
    type: T;
    result: PromiserMethods[T]["result"];
    messageId: string;
    dbId: DbId;
    workerReceivedTime: number;
    workerRespondTime: number;
    departureTime: number;
}
PromiserResponseSuccess
<function (type parameter) T in type PromiserResponse<T extends keyof PromiserMethods>T>
|
type PromiserResponseError = {
    type: "error";
    result: {
        operation: string;
        message: string;
        errorClass: string;
        input: object;
        stack: unknown[];
    };
    messageId: string;
    dbId: DbId;
}
PromiserResponseError
type type Promiser = <T extends keyof PromiserMethods>(messageType: T, messageArguments: PromiserMethods[T]["args"]) => Promise<PromiserResponse<T>>Promiser = <function (type parameter) T in <T extends keyof PromiserMethods>(messageType: T, messageArguments: PromiserMethods[T]["args"]): Promise<PromiserResponse<T>>T extends keyof
type PromiserMethods = {
    'config-get': {
        args: Record<string, never>;
        result: {
            dbID: DbId;
            version: {
                libVersion: string;
                sourceId: string;
                libVersionNumber: number;
                downloadVersion: number;
            };
            bigIntEnabled: boolean;
            opfsEnabled: boolean;
            vfsList: string[];
        };
    };
    open: {
        args: Partial<{
            filename?: string;
            vfs?: string;
        }>;
        result: {
            dbId: DbId;
            filename: string;
            persistent: boolean;
            vfs: string;
        };
    };
    exec: {
        args: {
            sql: string;
            dbId?: DbId;
            bind?: unknown[];
            returnValue?: string;
        };
        result: {
            dbId: DbId;
            sql: string;
            bind: unknown[];
            returnValue: string;
            resultRows?: unknown[][];
        };
    };
}
PromiserMethods
>(
messageType: T extends keyof PromiserMethodsmessageType: function (type parameter) T in <T extends keyof PromiserMethods>(messageType: T, messageArguments: PromiserMethods[T]["args"]): Promise<PromiserResponse<T>>T, messageArguments: PromiserMethods[T]["args"]messageArguments:
type PromiserMethods = {
    'config-get': {
        args: Record<string, never>;
        result: {
            dbID: DbId;
            version: {
                libVersion: string;
                sourceId: string;
                libVersionNumber: number;
                downloadVersion: number;
            };
            bigIntEnabled: boolean;
            opfsEnabled: boolean;
            vfsList: string[];
        };
    };
    open: {
        args: Partial<{
            filename?: string;
            vfs?: string;
        }>;
        result: {
            dbId: DbId;
            filename: string;
            persistent: boolean;
            vfs: string;
        };
    };
    exec: {
        args: {
            sql: string;
            dbId?: DbId;
            bind?: unknown[];
            returnValue?: string;
        };
        result: {
            dbId: DbId;
            sql: string;
            bind: unknown[];
            returnValue: string;
            resultRows?: unknown[][];
        };
    };
}
PromiserMethods
[function (type parameter) T in <T extends keyof PromiserMethods>(messageType: T, messageArguments: PromiserMethods[T]["args"]): Promise<PromiserResponse<T>>T]['args'],
) => interface Promise<T>
Represents the completion of an asynchronous operation
Promise
<type PromiserResponse<T extends keyof PromiserMethods> = PromiserResponseError | PromiserResponseSuccess<T>PromiserResponse<function (type parameter) T in <T extends keyof PromiserMethods>(messageType: T, messageArguments: PromiserMethods[T]["args"]): Promise<PromiserResponse<T>>T>>
export function function sqlite3Worker1Promiser(config?: Sqlite3Worker1PromiserConfig | OnreadyFunction): Promisersqlite3Worker1Promiser( config: OnreadyFunction | Sqlite3Worker1PromiserConfig | undefinedconfig?:
type Sqlite3Worker1PromiserConfig = {
    onready?: OnreadyFunction;
    worker?: Worker | (() => Worker);
    generateMessageId?: (messageObject: unknown) => string;
    debug?: (...args: any[]) => void;
    onunhandled?: (event: MessageEvent) => void;
}
Sqlite3Worker1PromiserConfig
| type OnreadyFunction = () => voidOnreadyFunction,
): type Promiser = <T extends keyof PromiserMethods>(messageType: T, messageArguments: PromiserMethods[T]["args"]) => Promise<PromiserResponse<T>>Promiser }

Step 4: Create the SQLite Composable

The core of our implementation is the useSQLite composable. This will handle all database operations:

import type { import DbIdDbId } from '@sqlite.org/sqlite-wasm'
import { import sqlite3Worker1Promisersqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm'
import { function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
} from 'vue'
const
const databaseConfig: {
    readonly filename: "file:mydb.sqlite3?vfs=opfs";
    readonly tables: {
        readonly test: {
            readonly name: "test_table";
            readonly schema: "\n        CREATE TABLE IF NOT EXISTS test_table (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL,\n          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n        );\n      ";
        };
    };
}
databaseConfig
= {
filename: "file:mydb.sqlite3?vfs=opfs"filename: 'file:mydb.sqlite3?vfs=opfs',
tables: {
    readonly test: {
        readonly name: "test_table";
        readonly schema: "\n        CREATE TABLE IF NOT EXISTS test_table (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL,\n          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n        );\n      ";
    };
}
tables
: {
test: {
    readonly name: "test_table";
    readonly schema: "\n        CREATE TABLE IF NOT EXISTS test_table (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL,\n          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n        );\n      ";
}
test
: {
name: "test_table"name: 'test_table', schema: "\n CREATE TABLE IF NOT EXISTS test_table (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n );\n "schema: ` CREATE TABLE IF NOT EXISTS test_table ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); `, }, }, } as
type const = {
    readonly filename: "file:mydb.sqlite3?vfs=opfs";
    readonly tables: {
        readonly test: {
            readonly name: "test_table";
            readonly schema: "\n        CREATE TABLE IF NOT EXISTS test_table (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL,\n          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n        );\n      ";
        };
    };
}
const
export function
function useSQLite(): {
    isLoading: Ref<boolean, boolean>;
    error: Ref<Error | null, Error | null>;
    isInitialized: Ref<boolean, boolean>;
    executeQuery: (sql: string, params?: unknown[]) => Promise<...>;
}
useSQLite
() {
const const isLoading: Ref<boolean, boolean>isLoading = ref<boolean>(value: boolean): Ref<boolean, boolean> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
(false)
const const error: Ref<Error | null, Error | null>error = ref<Error | null>(value: Error | null): Ref<Error | null, Error | null> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
<Error | null>(null)
const const isInitialized: Ref<boolean, boolean>isInitialized = ref<boolean>(value: boolean): Ref<boolean, boolean> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
(false)
let let promiser: anypromiser: type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain the return type of a function type
ReturnType
<typeof import sqlite3Worker1Promisersqlite3Worker1Promiser> | null = null
let let dbId: string | nulldbId: string | null = null async function function (local function) initialize(): Promise<boolean>initialize() { if (const isInitialized: Ref<boolean, boolean>isInitialized.Ref<boolean, boolean>.value: booleanvalue) return true const isLoading: Ref<boolean, boolean>isLoading.Ref<boolean, boolean>.value: booleanvalue = true const error: Ref<Error | null, Error | null>error.Ref<Error | null, Error | null>.value: Error | nullvalue = null try { // Initialize the SQLite worker let promiser: anypromiser = await new
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
Creates a new Promise.
@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((resolve: (value: unknown) => voidresolve) => {
const const _promiser: any_promiser = import sqlite3Worker1Promisersqlite3Worker1Promiser({ onready: () => voidonready: () => resolve: (value: unknown) => voidresolve(const _promiser: any_promiser), }) }) if (!let promiser: anypromiser) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Failed to initialize promiser')
// Get configuration and open database await let promiser: anypromiser('config-get', {}) const const openResponse: anyopenResponse = await let promiser: anypromiser('open', { filename: "file:mydb.sqlite3?vfs=opfs"filename:
const databaseConfig: {
    readonly filename: "file:mydb.sqlite3?vfs=opfs";
    readonly tables: {
        readonly test: {
            readonly name: "test_table";
            readonly schema: "\n        CREATE TABLE IF NOT EXISTS test_table (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL,\n          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n        );\n      ";
        };
    };
}
databaseConfig
.filename: "file:mydb.sqlite3?vfs=opfs"filename,
}) if (const openResponse: anyopenResponse.type === 'error') { throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(const openResponse: anyopenResponse.result.message)
} let dbId: string | nulldbId = const openResponse: anyopenResponse.result.dbId as string // Create initial tables await let promiser: anypromiser('exec', { dbId: stringdbId, sql: "\n CREATE TABLE IF NOT EXISTS test_table (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n );\n "sql:
const databaseConfig: {
    readonly filename: "file:mydb.sqlite3?vfs=opfs";
    readonly tables: {
        readonly test: {
            readonly name: "test_table";
            readonly schema: "\n        CREATE TABLE IF NOT EXISTS test_table (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL,\n          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n        );\n      ";
        };
    };
}
databaseConfig
.
tables: {
    readonly test: {
        readonly name: "test_table";
        readonly schema: "\n        CREATE TABLE IF NOT EXISTS test_table (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL,\n          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n        );\n      ";
    };
}
tables
.
test: {
    readonly name: "test_table";
    readonly schema: "\n        CREATE TABLE IF NOT EXISTS test_table (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL,\n          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n        );\n      ";
}
test
.schema: "\n CREATE TABLE IF NOT EXISTS test_table (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n );\n "schema,
}) const isInitialized: Ref<boolean, boolean>isInitialized.Ref<boolean, boolean>.value: booleanvalue = true return true } catch (function (local var) err: unknownerr) { const error: Ref<Error | null, Error | null>error.Ref<Error | null, Error | null>.value: Error | nullvalue = function (local var) err: unknownerr instanceof var Error: ErrorConstructorError ? function (local var) err: Errorerr : new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Unknown error')
throw const error: Ref<Error | null, Error | null>error.Ref<Error | null, Error | null>.value: Errorvalue } finally { const isLoading: Ref<boolean, boolean>isLoading.Ref<boolean, boolean>.value: booleanvalue = false } } async function function (local function) executeQuery(sql: string, params?: unknown[]): Promise<any>executeQuery(sql: stringsql: string, params: unknown[]params: unknown[] = []) { if (!let dbId: string | nulldbId || !let promiser: anypromiser) { await function (local function) initialize(): Promise<boolean>initialize() } const isLoading: Ref<boolean, boolean>isLoading.Ref<boolean, boolean>.value: booleanvalue = true const error: Ref<Error | null, Error | null>error.Ref<Error | null, Error | null>.value: Error | nullvalue = null try { const const result: anyresult = await let promiser: anypromiser!('exec', { dbId: DbIddbId: let dbId: string | nulldbId as import DbIdDbId, sql: stringsql, bind: unknown[]bind: params: unknown[]params, returnValue: stringreturnValue: 'resultRows', }) if (const result: anyresult.type === 'error') { throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(const result: anyresult.result.message)
} return const result: anyresult } catch (function (local var) err: unknownerr) { const error: Ref<Error | null, Error | null>error.Ref<Error | null, Error | null>.value: Error | nullvalue = function (local var) err: unknownerr instanceof var Error: ErrorConstructorError ? function (local var) err: Errorerr : new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Query execution failed')
throw const error: Ref<Error | null, Error | null>error.Ref<Error | null, Error | null>.value: Errorvalue } finally { const isLoading: Ref<boolean, boolean>isLoading.Ref<boolean, boolean>.value: booleanvalue = false } } return { isLoading: Ref<boolean, boolean>isLoading, error: Ref<Error | null, Error | null>error, isInitialized: Ref<boolean, boolean>isInitialized, executeQuery: (sql: string, params?: unknown[]) => Promise<any>executeQuery, } }

Step 5: Create a SQLite Playground Component

Now let’s create a component to test our SQLite implementation:


<script setup lang="ts">
import { import useSQLiteuseSQLite } from '@/composables/useSQLite'
import { function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
} from 'vue'
const { const isLoading: anyisLoading, const error: anyerror, const executeQuery: anyexecuteQuery } = import useSQLiteuseSQLite() const const sqlQuery: Ref<string, string>sqlQuery = ref<string>(value: string): Ref<string, string> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
('SELECT * FROM test_table')
const const queryResult: Ref<any[], any[]>queryResult = ref<any[]>(value: any[]): Ref<any[], any[]> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
<any[]>([])
const const queryError: Ref<string | null, string | null>queryError = ref<string | null>(value: string | null): Ref<string | null, string | null> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
<string | null>(null)
// Predefined example queries for testing const
const exampleQueries: {
    title: string;
    query: string;
}[]
exampleQueries
= [
{ title: stringtitle: 'Select all', query: stringquery: 'SELECT * FROM test_table' }, { title: stringtitle: 'Insert', query: stringquery: "INSERT INTO test_table (name) VALUES ('New Test Item')" }, { title: stringtitle: 'Update', query: stringquery: "UPDATE test_table SET name = 'Updated Item' WHERE name LIKE 'New%'" }, { title: stringtitle: 'Delete', query: stringquery: "DELETE FROM test_table WHERE name = 'Updated Item'" }, ] async function function runQuery(): Promise<void>runQuery() { const queryError: Ref<string | null, string | null>queryError.Ref<string | null, string | null>.value: string | nullvalue = null const queryResult: Ref<any[], any[]>queryResult.Ref<any[], any[]>.value: any[]value = [] try { const const result: anyresult = await const executeQuery: anyexecuteQuery(const sqlQuery: Ref<string, string>sqlQuery.Ref<string, string>.value: stringvalue) const const isSelect: booleanisSelect = const sqlQuery: Ref<string, string>sqlQuery.Ref<string, string>.value: stringvalue.String.trim(): string
Removes the leading and trailing white space and line terminator characters from a string.
trim
().String.toLowerCase(): string
Converts all the alphabetic characters in a string to lowercase.
toLowerCase
().String.startsWith(searchString: string, position?: number): boolean
Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at position. Otherwise returns false.
startsWith
('select')
if (const isSelect: booleanisSelect) { const queryResult: Ref<any[], any[]>queryResult.Ref<any[], any[]>.value: any[]value = const result: anyresult?.result.resultRows || [] } else { // After mutation, fetch updated data const queryResult: Ref<any[], any[]>queryResult.Ref<any[], any[]>.value: any[]value = (await const executeQuery: anyexecuteQuery('SELECT * FROM test_table'))?.result.resultRows || [] } } catch (function (local var) err: unknownerr) { const queryError: Ref<string | null, string | null>queryError.Ref<string | null, string | null>.value: string | nullvalue = function (local var) err: unknownerr instanceof var Error: ErrorConstructorError ? function (local var) err: Errorerr.Error.message: stringmessage : 'An error occurred' } } </script> <template> <div: HTMLAttributes & ReservedPropsdiv HTMLAttributes.class?: anyclass="max-w-7xl mx-auto px-4 py-6"> <h2: HTMLAttributes & ReservedPropsh2 HTMLAttributes.class?: anyclass="text-2xl font-bold">SQLite Playground</h2: HTMLAttributes & ReservedPropsh2> <!-- Example queries --> <div: HTMLAttributes & ReservedPropsdiv HTMLAttributes.class?: anyclass="mt-4"> <h3: HTMLAttributes & ReservedPropsh3 HTMLAttributes.class?: anyclass="text-sm font-medium">Example Queries:</h3: HTMLAttributes & ReservedPropsh3> <div: HTMLAttributes & ReservedPropsdiv HTMLAttributes.class?: anyclass="flex gap-2 mt-2"> <button: ButtonHTMLAttributes & ReservedPropsbutton v-for="
const example: {
    title: string;
    query: string;
}
example
in
const exampleQueries: {
    title: string;
    query: string;
}[]
exampleQueries
"
key?: PropertyKey | undefined:key?: PropertyKey | undefinedkey="
const example: {
    title: string;
    query: string;
}
example
.title: stringtitle"
HTMLAttributes.class?: anyclass="px-3 py-1 text-sm rounded-full bg-gray-100 hover:bg-gray-200" @onClick?: ((payload: MouseEvent) => void) | undefinedclick="const sqlQuery: Ref<string, string>sqlQuery =
const example: {
    title: string;
    query: string;
}
example
.query: stringquery"
> {{
const example: {
    title: string;
    query: string;
}
example
.title: stringtitle }}
</button: ButtonHTMLAttributes & ReservedPropsbutton> </div: HTMLAttributes & ReservedPropsdiv> </div: HTMLAttributes & ReservedPropsdiv> <!-- Query input --> <div: HTMLAttributes & ReservedPropsdiv HTMLAttributes.class?: anyclass="mt-6"> <textarea: TextareaHTMLAttributes & ReservedPropstextarea TextareaHTMLAttributes.value?: string | number | readonly string[] | null | undefinedv-model="const sqlQuery: Ref<string, string>sqlQuery" TextareaHTMLAttributes.rows?: Numberish | undefinedrows="4" HTMLAttributes.class?: anyclass="w-full px-4 py-3 rounded-lg font-mono text-sm" TextareaHTMLAttributes.disabled?: Booleanish | undefined:TextareaHTMLAttributes.disabled?: Booleanish | undefineddisabled="const isLoading: anyisLoading" /> <button: ButtonHTMLAttributes & ReservedPropsbutton ButtonHTMLAttributes.disabled?: Booleanish | undefined:ButtonHTMLAttributes.disabled?: Booleanish | undefineddisabled="const isLoading: anyisLoading" HTMLAttributes.class?: anyclass="mt-2 px-4 py-2 rounded-lg bg-blue-600 text-white" @onClick?: ((payload: MouseEvent) => void) | undefinedclick="function runQuery(): Promise<void>runQuery" > {{ const isLoading: anyisLoading ? 'Running...' : 'Run Query' }} </button: ButtonHTMLAttributes & ReservedPropsbutton> </div: HTMLAttributes & ReservedPropsdiv> <!-- Error display --> <div: HTMLAttributes & ReservedPropsdiv v-if="const error: anyerror || const queryError: Ref<string | null, string | null>queryError" HTMLAttributes.class?: anyclass="mt-4 p-4 rounded-lg bg-red-50 text-red-600" > {{ const error: anyerror?.message || const queryError: Ref<string | null, string | null>queryError }} </div: HTMLAttributes & ReservedPropsdiv> <!-- Results table --> <div: HTMLAttributes & ReservedPropsdiv v-if="const queryResult: Ref<any[], any[]>queryResult.Array<any>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.
length
" HTMLAttributes.class?: anyclass="mt-4">
<h3: HTMLAttributes & ReservedPropsh3 HTMLAttributes.class?: anyclass="text-lg font-semibold">Results:</h3: HTMLAttributes & ReservedPropsh3> <div: HTMLAttributes & ReservedPropsdiv HTMLAttributes.class?: anyclass="mt-2 overflow-x-auto"> <table: TableHTMLAttributes & ReservedPropstable HTMLAttributes.class?: anyclass="w-full"> <thead: HTMLAttributes & ReservedPropsthead> <tr: HTMLAttributes & ReservedPropstr> <th: ThHTMLAttributes & ReservedPropsth v-for="const column: stringcolumn in var Object: ObjectConstructor
Provides functionality common to all JavaScript objects.
Object
.ObjectConstructor.keys(o: {}): string[] (+1 overload)
Returns the names of the enumerable string properties and methods of an object.
@paramo Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
keys
(const queryResult: Ref<any[], any[]>queryResult[0])"
key?: PropertyKey | undefined:key?: PropertyKey | undefinedkey="const column: stringcolumn" HTMLAttributes.class?: anyclass="px-4 py-2 text-left" > {{ const column: stringcolumn }} </th: ThHTMLAttributes & ReservedPropsth> </tr: HTMLAttributes & ReservedPropstr> </thead: HTMLAttributes & ReservedPropsthead> <tbody: HTMLAttributes & ReservedPropstbody> <tr: HTMLAttributes & ReservedPropstr v-for="(const row: anyrow, const index: numberindex) in const queryResult: Ref<any[], any[]>queryResult" key?: PropertyKey | undefined:key?: PropertyKey | undefinedkey="const index: numberindex" > <td: TdHTMLAttributes & ReservedPropstd v-for="const column: stringcolumn in var Object: ObjectConstructor
Provides functionality common to all JavaScript objects.
Object
.ObjectConstructor.keys(o: {}): string[] (+1 overload)
Returns the names of the enumerable string properties and methods of an object.
@paramo Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
keys
(const row: anyrow)"
key?: PropertyKey | undefined:key?: PropertyKey | undefinedkey="const column: stringcolumn" HTMLAttributes.class?: anyclass="px-4 py-2" > {{ const row: anyrow[const column: stringcolumn] }} </td: TdHTMLAttributes & ReservedPropstd> </tr: HTMLAttributes & ReservedPropstr> </tbody: HTMLAttributes & ReservedPropstbody> </table: TableHTMLAttributes & ReservedPropstable> </div: HTMLAttributes & ReservedPropsdiv> </div: HTMLAttributes & ReservedPropsdiv> </div: HTMLAttributes & ReservedPropsdiv> </template>

🎯 Real-World Example: Notion’s SQLite Implementation

Notion recently shared how they implemented SQLite in their web application, providing some valuable insights:

Performance Improvements

Multi-Tab Architecture

Notion solved the challenge of handling multiple browser tabs with an innovative approach:

  1. Each tab has its own Web Worker for SQLite operations
  2. A SharedWorker manages which tab is “active”
  3. Only one tab can write to SQLite at a time
  4. Queries from all tabs are routed through the active tab’s Worker

Key Learnings from Notion

  1. Async Loading: They load the WASM SQLite library asynchronously to avoid blocking initial page load
  2. Race Conditions: They implemented a “racing” system between SQLite and API requests to handle slower devices
  3. OPFS Handling: They discovered that Origin Private File System (OPFS) doesn’t handle concurrency well out of the box
  4. Cross-Origin Isolation: They opted for OPFS SyncAccessHandle Pool VFS to avoid cross-origin isolation requirements

This real-world implementation demonstrates both the potential and challenges of using SQLite in production web applications. Notion’s success shows that with careful architecture choices, SQLite can significantly improve web application performance.

🎯 Conclusion

You now have a solid foundation for building offline-capable Vue applications using SQLite. This approach offers significant advantages over traditional browser storage solutions, especially for complex data requirements.

Related Posts