---
description: Learn how to use custom SQLite extensions in Capacitor on Android and iOS. Build and load FTS5 tokenizers and custom functions with the SQLite plugin.
title: How to Use Custom SQLite Extensions in Capacitor - Capawesome
image: https://capawesome.io/docs/assets/images/social/blog/how-to-use-custom-sqlite-extensions-with-capacitor.png
---

<!doctype html> 

[Skip to content ](#how-to-use-custom-sqlite-extensions-in-capacitor) 

[🔐 Introducing the **Capacitor Vault** plugin — store secrets behind biometrics or a device passcode. ](/blog/announcing-the-capacitor-vault-plugin/) 

* [ SDKs ](/docs/sdks/)
* [ Formbricks ](/docs/sdks/capacitor/formbricks/)
* [ Geocoder ](/docs/sdks/capacitor/geocoder/)
* [ Google Sign-In ](/docs/sdks/capacitor/google-sign-in/)
* [ Grafana Faro ](/docs/sdks/capacitor/grafana-faro/)
* [ libSQL ](/docs/sdks/capacitor/libsql/)
* [ Live Update ](/docs/sdks/capacitor/live-update/)
* [ Managed Configurations ](/docs/sdks/capacitor/managed-configurations/)
* [ Media Session ](/docs/sdks/capacitor/media-session/)
* [ ML Kit ](/docs/sdks/capacitor/mlkit/)
* [ Navigation Bar ](/docs/sdks/capacitor/navigation-bar/)
* [ NFC ](/docs/sdks/capacitor/nfc/)
* [ OAuth ](/docs/sdks/capacitor/oauth/)
* [ Pedometer ](/docs/sdks/capacitor/pedometer/)
* [ Photo Editor ](/docs/sdks/capacitor/photo-editor/)
* [ PostHog ](/docs/sdks/capacitor/posthog/)
* [ Printer ](/docs/sdks/capacitor/printer/)
* [ Purchases ](/docs/sdks/capacitor/purchases/)
* [ RealtimeKit ](/docs/sdks/capacitor/realtimekit/)
* [ Screen Orientation ](/docs/sdks/capacitor/screen-orientation/)
* [ Screenshot ](/docs/sdks/capacitor/screenshot/)
* [ Secure Preferences ](/docs/sdks/capacitor/secure-preferences/)
* [ Speech Recognition ](/docs/sdks/capacitor/speech-recognition/)
* [ Speech Synthesis ](/docs/sdks/capacitor/speech-synthesis/)
* [ Share Target ](/docs/sdks/capacitor/share-target/)
* [ Square Mobile Payments ](/docs/sdks/capacitor/square-mobile-payments/)
* [ SQLite ](/docs/sdks/capacitor/sqlite/)
* [ Superwall ](/docs/sdks/capacitor/superwall/)
* [ Torch ](/docs/sdks/capacitor/torch/)
* [ Vault ](/docs/sdks/capacitor/vault/)
* [ Wifi ](/docs/sdks/capacitor/wifi/)
* [ Zip ](/docs/sdks/capacitor/zip/)
* [ Cordova ](/docs/sdks/cordova/)
* [ Cloud ](/docs/cloud/)
* [ Integrations ](/docs/cloud/live-updates/integrations/)
* Concepts
* Reference
* [ Troubleshooting ](/docs/cloud/live-updates/troubleshooting/)
* [ FAQ ](/docs/cloud/live-updates/faq/)
* [ Native Builds ](/docs/cloud/native-builds/)
* [ Set Up Environments ](/docs/cloud/native-builds/environments/)
* [ Overwrite Native Configurations ](/docs/cloud/native-builds/native-configurations/)
* [ Auto-Increment Build Numbers ](/docs/cloud/native-builds/auto-incrementing-build-numbers/)
* [ Configure the Web Build Script ](/docs/cloud/native-builds/web-build-script/)
* [ Build from a Monorepo ](/docs/cloud/native-builds/monorepo/)
* [ Use pnpm, Yarn, or bun ](/docs/cloud/native-builds/package-managers/)
* [ Install Private npm Packages ](/docs/cloud/native-builds/npm-private-registry/)
* [ Override the Java Version ](/docs/cloud/native-builds/override-java-version/)
* [ Custom iOS Provisioning Profiles ](/docs/cloud/native-builds/custom-ios-provisioning-profiles/)
* [ Build without Git ](/docs/cloud/native-builds/build-without-git/)
* [ Access Git Behind a Firewall ](/docs/cloud/native-builds/firewall-access/)
* [ Integrations ](/docs/cloud/native-builds/integrations/)
* Reference
* [ Troubleshooting ](/docs/cloud/native-builds/troubleshooting/)
* [ FAQ ](/docs/cloud/native-builds/faq/)
* [ App Store Publishing ](/docs/cloud/app-store-publishing/)
* [ Submit a Build ](/docs/cloud/app-store-publishing/submit-a-build/)
* [ Submit Automatically After a Build ](/docs/cloud/app-store-publishing/submit-automatically/)
* [ Troubleshooting ](/docs/cloud/app-store-publishing/troubleshooting/)
* [ FAQ ](/docs/cloud/app-store-publishing/faq/)
* [ Automations ](/docs/cloud/automations/)
* [ Reference ](/docs/cloud/automations/reference/)
* [ Troubleshooting ](/docs/cloud/automations/troubleshooting/)
* [ FAQ ](/docs/cloud/automations/faq/)
* [ Assist ](/docs/cloud/assist/)
* [ CLI ](/docs/cloud/cli/)
* APIs and SDKs
* [ Webhooks ](/docs/cloud/webhooks/)
* [ Integrations ](/docs/cloud/integrations/)
* Account
* [ Organization ](/docs/cloud/organizations/)
* [ Two-Factor Enforcement ](/docs/cloud/organizations/two-factor-authentication/)
* [ Audit Logs ](/docs/cloud/organizations/audit-logs/)
* [ Billing ](/docs/cloud/organizations/billing/)
* [ License Keys ](/docs/cloud/license-keys/)
* [ AI ](/docs/ai/)
* [ Insiders ](/docs/insiders/)
* [ Billing & Plans ](/docs/insiders/billing-and-plans/)
* [ FAQ ](/docs/insiders/faq/)
* [ License ](https://capawesome.io/legal/eula/)
* [ Support ](/docs/support/)
* [ Contributing ](/docs/contributing/)
* Contributing code
* [ Code of Conduct ](/docs/contributing/code-of-conduct/)
* [ Questions ](https://docs.github.com/en/discussions/collaborating-with-your-community-using-discussions/participating-in-a-discussion#creating-a-discussion)
* [ Blog ](/blog/)
* Categories

* [ Loading the extension on iOS ](#loading-the-extension-on-ios)
* [ Using a custom tokenizer in full-text search ](#using-a-custom-tokenizer-in-full-text-search)
* [ Potential issues ](#potential-issues)
* [ Stay Updated ](#stay-updated)
* [ Conclusion ](#conclusion)

* Related links

# How to Use Custom SQLite Extensions in Capacitor[¶](#how-to-use-custom-sqlite-extensions-in-capacitor "Permanent link")

SQLite ships with a lot out of the box, but sometimes you need behavior it doesn't provide: a custom FTS5 tokenizer for a language it doesn't handle well, a domain-specific SQL function, or a custom collation. Loadable extensions let you add exactly that. As of version 0.3.9, the [Capacitor SQLite plugin](/docs/sdks/capacitor/sqlite/) supports custom SQLite extensions on both Android and iOS. The catch is that each platform uses a different mechanism, so this guide walks through both — using a custom FTS5 tokenizer as the running example.

[ ![Build and deploy your Capacitor app with Capawesome Cloud](https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1) ](https://capawesome.io/) 

## Key Takeaways[¶](#key-takeaways "Permanent link")

* Custom SQLite extensions are supported on **Android and iOS** (not Web), starting with version **0.3.9** of the Capacitor SQLite plugin.
* **Android** loads extensions at runtime through the `androidExtensions` option on the `open(...)` method, and requires the bundled requery SQLite backend.
* **iOS** requires statically linking the extension and registering it at app startup with `sqlite3_auto_extension`; once registered, it's available in every database.
* You write the extension **once in C** and then build or integrate it per platform.

## What are custom SQLite extensions?[¶](#what-are-custom-sqlite-extensions "Permanent link")

A SQLite extension is a piece of native code that registers new functionality with SQLite: scalar and aggregate functions, collating sequences, virtual tables, and FTS5 tokenizers. Instead of patching SQLite or waiting for a feature to land upstream, you compile your code and hook it into SQLite. The [official SQLite documentation on run-time loadable extensions](https://www.sqlite.org/loadext.html) covers the C API in detail.

Two common reasons to reach for one in a mobile app:

* **Custom FTS5 tokenizers** — the built-in tokenizers don't fit every language or matching strategy. A custom tokenizer lets you control exactly how text is split and normalized for full-text search.
* **Custom SQL functions** — push logic that's awkward in SQL (specialized string processing, scoring, geospatial math) down into the database, where it runs close to the data.

## Why does each platform work differently?[¶](#why-does-each-platform-work-differently "Permanent link")

The platforms differ because of how each one builds and restricts SQLite. On Android, the system SQLite is compiled without loadable-extension support, so the plugin loads extensions into the bundled requery backend at runtime. On iOS, App Store apps can't load dynamic libraries at runtime and the system SQLite is also built without that support, so the extension has to be statically linked into the binary and registered at startup. Web isn't supported on either count.

The upshot: the extension's C source is the same on both platforms. What changes is how you build and wire it up. Android needs a shared library per CPU architecture loaded through a plugin option; iOS needs the source compiled into the app and registered before any database opens.

## Writing a loadable extension[¶](#writing-a-loadable-extension "Permanent link")

Every extension follows the same skeleton. The source includes `sqlite3ext.h`, declares the extension API with the `SQLITE_EXTENSION_INIT1` macro, and exposes a single init function that registers your functionality:

`[](#%5F%5Fcodelineno-0-1)#include <sqlite3ext.h>
[](#%5F%5Fcodelineno-0-2)SQLITE_EXTENSION_INIT1
[](#%5F%5Fcodelineno-0-3)
[](#%5F%5Fcodelineno-0-4)int sqlite3_sqlitetokenizerar_init(
[](#%5F%5Fcodelineno-0-5)  sqlite3 *db,
[](#%5F%5Fcodelineno-0-6)  char **pzErrMsg,
[](#%5F%5Fcodelineno-0-7)  const sqlite3_api_routines *pApi
[](#%5F%5Fcodelineno-0-8)) {
[](#%5F%5Fcodelineno-0-9)  SQLITE_EXTENSION_INIT2(pApi);
[](#%5F%5Fcodelineno-0-10)  /* Register your custom FTS5 tokenizer with the fts5_api here. */
[](#%5F%5Fcodelineno-0-11)  return SQLITE_OK;
[](#%5F%5Fcodelineno-0-12)}
`

The body of the init function is where you register your custom tokenizer. You fetch the `fts5_api` pointer and call `xCreateTokenizer` with your tokenizer's callbacks. Implementing the tokenizer itself is beyond the scope of this post, but the [FTS5 custom tokenizer documentation](https://www.sqlite.org/fts5.html#custom%5Ftokenizers) walks through the required interface.

The init function name follows the SQLite convention `sqlite3_<name>_init`. The `INIT1` and `INIT2` macros let the same source compile two ways: as a runtime-loadable extension on Android, or — when compiled with `-DSQLITE_CORE` — as a statically linked extension on iOS. We'll use that flag in the iOS section.

## Loading the extension on Android[¶](#loading-the-extension-on-android "Permanent link")

On Android, you compile the extension into a native library for each CPU architecture, bundle it with your app, and load it through the `androidExtensions` option.

### Enable the bundled SQLite backend[¶](#enable-the-bundled-sqlite-backend "Permanent link")

Extension loading requires the requery backend, because the system SQLite can't load extensions. Enable it by setting `capawesomeCapacitorSqliteIncludeRequery` to `true` in your app's `variables.gradle` file:

`[](#%5F%5Fcodelineno-1-1)ext {
[](#%5F%5Fcodelineno-1-2)+  capawesomeCapacitorSqliteIncludeRequery = true // Default: false
[](#%5F%5Fcodelineno-1-3)}
`

The requery library is published on JitPack, so add the JitPack repository to your app's `build.gradle` file:

`[](#%5F%5Fcodelineno-2-1)repositories {
[](#%5F%5Fcodelineno-2-2)    google()
[](#%5F%5Fcodelineno-2-3)    mavenCentral()
[](#%5F%5Fcodelineno-2-4)+   maven { url 'https://jitpack.io' }
[](#%5F%5Fcodelineno-2-5)}
`

One constraint to keep in mind: this option can't be combined with `capawesomeCapacitorSqliteIncludeSqlcipher`. SQLCipher bundles its own SQLite version, so the two backends are mutually exclusive.

### Compile for each Android ABI[¶](#compile-for-each-android-abi "Permanent link")

A native library has to be compiled separately for each CPU architecture (ABI) your app supports. Android currently defines four ABIs: `arm64-v8a`, `armeabi-v7a`, `x86`, and `x86_64`. The [Android NDK](https://developer.android.com/ndk) includes a Clang toolchain that can target each one. You can install it through Android Studio's SDK Manager.

Point a variable at your installed NDK and the prebuilt toolchain for your host platform (the example below uses `darwin-x86_64` for macOS — use `linux-x86_64` on Linux or `windows-x86_64` on Windows):

`[](#%5F%5Fcodelineno-3-1)export NDK=$HOME/Library/Android/sdk/ndk/<version>
[](#%5F%5Fcodelineno-3-2)export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
[](#%5F%5Fcodelineno-3-3)export API=24
`

Set `API` to match your app's `minSdkVersion`. With that in place, compile the extension once per ABI as a shared library:

`[](#%5F%5Fcodelineno-4-1)for TARGET in \
[](#%5F%5Fcodelineno-4-2)  aarch64-linux-android:arm64-v8a \
[](#%5F%5Fcodelineno-4-3)  armv7a-linux-androideabi:armeabi-v7a \
[](#%5F%5Fcodelineno-4-4)  i686-linux-android:x86 \
[](#%5F%5Fcodelineno-4-5)  x86_64-linux-android:x86_64
[](#%5F%5Fcodelineno-4-6)do
[](#%5F%5Fcodelineno-4-7)  TRIPLE=${TARGET%%:*}
[](#%5F%5Fcodelineno-4-8)  ABI=${TARGET##*:}
[](#%5F%5Fcodelineno-4-9)  mkdir -p jniLibs/$ABI
[](#%5F%5Fcodelineno-4-10)  $TOOLCHAIN/bin/clang \
[](#%5F%5Fcodelineno-4-11)    --target=$TRIPLE$API \
[](#%5F%5Fcodelineno-4-12)    -shared -fPIC \
[](#%5F%5Fcodelineno-4-13)    -o jniLibs/$ABI/libsqlite_tokenizer_ar.so \
[](#%5F%5Fcodelineno-4-14)    sqlite_tokenizer_ar.c
[](#%5F%5Fcodelineno-4-15)done
`

A runtime-loadable extension doesn't link against SQLite — the `SQLITE_EXTENSION_INIT1` and `SQLITE_EXTENSION_INIT2` macros route every SQLite call through the API pointer passed in at load time. You only need `sqlite3ext.h` (and the `sqlite3.h` it includes) on the include path. The easiest way is to drop both headers from the [SQLite amalgamation](https://www.sqlite.org/amalgamation.html) next to your source file, or add an `-I` flag pointing at them.

### Bundle the native libraries[¶](#bundle-the-native-libraries "Permanent link")

Android automatically packages native libraries placed under `jniLibs`. Move the compiled `.so` files into `android/app/src/main/jniLibs`, with one subfolder per ABI:

`[](#%5F%5Fcodelineno-5-1)android/app/src/main/jniLibs/
[](#%5F%5Fcodelineno-5-2)├── arm64-v8a/
[](#%5F%5Fcodelineno-5-3)│   └── libsqlite_tokenizer_ar.so
[](#%5F%5Fcodelineno-5-4)├── armeabi-v7a/
[](#%5F%5Fcodelineno-5-5)│   └── libsqlite_tokenizer_ar.so
[](#%5F%5Fcodelineno-5-6)├── x86/
[](#%5F%5Fcodelineno-5-7)│   └── libsqlite_tokenizer_ar.so
[](#%5F%5Fcodelineno-5-8)└── x86_64/
[](#%5F%5Fcodelineno-5-9)    └── libsqlite_tokenizer_ar.so
`

Gradle picks these up with no extra configuration and includes them in your APK or App Bundle. At runtime, the plugin resolves each extension to its bundled library, whether it was extracted to the app's native library directory at install time or stored uncompressed inside the APK. Both packaging modes work.

### Load the extension[¶](#load-the-extension "Permanent link")

With the libraries bundled, load them by passing the `androidExtensions` option to the [open(...)](/docs/sdks/capacitor/sqlite/#open) method. Each entry references a library by `name` — the file name without the `lib` prefix and `.so` suffix:

`[](#%5F%5Fcodelineno-6-1)import { Sqlite } from '@capawesome-team/capacitor-sqlite';
[](#%5F%5Fcodelineno-6-2)
[](#%5F%5Fcodelineno-6-3)const { databaseId } = await Sqlite.open({
[](#%5F%5Fcodelineno-6-4)  path: 'my.db',
[](#%5F%5Fcodelineno-6-5)  androidExtensions: [{ name: 'sqlite_tokenizer_ar' }],
[](#%5F%5Fcodelineno-6-6)});
`

When you don't specify an entry point, SQLite derives one from the library file name: it strips the leading `lib`, drops everything after the first dot, lowercases the rest, and removes any non-alphanumeric characters (including underscores). So `libsqlite_tokenizer_ar.so` resolves to `sqlite3_sqlitetokenizerar_init`, which matches our init function. If your init function uses a different name, set it explicitly:

`[](#%5F%5Fcodelineno-7-1)const { databaseId } = await Sqlite.open({
[](#%5F%5Fcodelineno-7-2)  path: 'my.db',
[](#%5F%5Fcodelineno-7-3)  androidExtensions: [
[](#%5F%5Fcodelineno-7-4)    {
[](#%5F%5Fcodelineno-7-5)      name: 'sqlite_tokenizer_ar',
[](#%5F%5Fcodelineno-7-6)      entryPoint: 'sqlite3_sqlitetokenizerar_init',
[](#%5F%5Fcodelineno-7-7)    },
[](#%5F%5Fcodelineno-7-8)  ],
[](#%5F%5Fcodelineno-7-9)});
`

## Loading the extension on iOS[¶](#loading-the-extension-on-ios "Permanent link")

iOS has no plugin option for extensions. Instead, you compile the C source into your app, declare its entry point, and register it once at startup. After that, the extension is available in every database the plugin opens.

### Add the source and set the compiler flag[¶](#add-the-source-and-set-the-compiler-flag "Permanent link")

Add the extension's C source file to your app target in Xcode. Then, under **Build Phases › Compile Sources**, set the per-file compiler flag `-DSQLITE_CORE`. This makes the extension link against the host SQLite instead of expecting the runtime-loading API:

`[](#%5F%5Fcodelineno-8-1)-DSQLITE_CORE
`

### Declare the entry point[¶](#declare-the-entry-point "Permanent link")

Declare the init function in your bridging header so Swift can reference it:

`[](#%5F%5Fcodelineno-9-1)#include <sqlite3.h>
[](#%5F%5Fcodelineno-9-2)
[](#%5F%5Fcodelineno-9-3)int sqlite3_sqlitetokenizerar_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi);
`

### Register at startup[¶](#register-at-startup "Permanent link")

Finally, register the extension before any database is opened — for example in your `AppDelegate`. The `sqlite3_auto_extension` function ensures the extension is applied to every database connection the plugin creates afterward:

`[](#%5F%5Fcodelineno-10-1)func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
[](#%5F%5Fcodelineno-10-2)  sqlite3_auto_extension(unsafeBitCast(sqlite3_sqlitetokenizerar_init, to: (@convention(c) () -> Void).self))
[](#%5F%5Fcodelineno-10-3)  return true
[](#%5F%5Fcodelineno-10-4)}
`

Because registration is global, there's no per-database option to pass on iOS — the tokenizer is simply available everywhere. See the [statically linking section](https://www.sqlite.org/loadext.html#statically%5Flinking%5Fa%5Frun%5Ftime%5Floadable%5Fextension) of the SQLite documentation for more background.

## Using a custom tokenizer in full-text search[¶](#using-a-custom-tokenizer-in-full-text-search "Permanent link")

Once the extension is loaded — at runtime on Android, at startup on iOS — your custom tokenizer behaves like any built-in one. Reference it by name in the `tokenize` option when you create the virtual table:

`` [](#%5F%5Fcodelineno-11-1)await Sqlite.execute({
[](#%5F%5Fcodelineno-11-2)  databaseId,
[](#%5F%5Fcodelineno-11-3)  statement: `
[](#%5F%5Fcodelineno-11-4)    CREATE VIRTUAL TABLE IF NOT EXISTS documents
[](#%5F%5Fcodelineno-11-5)    USING fts5(title, body, tokenize = 'sqlite_tokenizer_ar');
[](#%5F%5Fcodelineno-11-6)  `,
[](#%5F%5Fcodelineno-11-7)});
 ``

From there, full-text queries run through your tokenizer automatically, with the same code on both platforms:

`` [](#%5F%5Fcodelineno-12-1)const { rows } = await Sqlite.query({
[](#%5F%5Fcodelineno-12-2)  databaseId,
[](#%5F%5Fcodelineno-12-3)  statement: `SELECT title FROM documents WHERE documents MATCH ?;`,
[](#%5F%5Fcodelineno-12-4)  values: ['مرحبا'],
[](#%5F%5Fcodelineno-12-5)});
 ``

## Potential issues[¶](#potential-issues "Permanent link")

A few errors come up often enough to call out.

On **Android**:

* **"Extensions require the requery backend"** — you passed `androidExtensions` without enabling `capawesomeCapacitorSqliteIncludeRequery`.
* **Library not found** — the `.so` file is missing for the device's ABI. Confirm you compiled and bundled the library for every architecture your app ships, including `arm64-v8a`, which most modern devices use.
* **Wrong entry point** — if the init function name doesn't match SQLite's derived name, set the `entryPoint` option explicitly. Underscores and other non-alphanumeric characters are stripped from the derived name.
* **Invalid name** — both `name` and `entryPoint` are restricted to the characters `A-Z`, `a-z`, `0-9`, and `_`, which prevents path traversal. Rename libraries that use hyphens or dots.

On **iOS**:

* **Symbol not found** — the source wasn't compiled with `-DSQLITE_CORE`, so it links against the runtime-loading API that isn't available.
* **Tokenizer not registered** — `sqlite3_auto_extension` was called after a database was already opened. Register it before the first `open(...)` call.

On both platforms, the SQLCipher backend can't be combined with the requery backend, so pick one backend per app.

## Stay Updated[¶](#stay-updated "Permanent link")

Want to stay up to date with the latest features and guides for the Capacitor SQLite plugin? Subscribe to the Capawesome newsletter.

[Subscribe to the Capawesome Newsletter](https://capawesome.io/newsletter)

## Conclusion[¶](#conclusion "Permanent link")

Custom SQLite extensions give you an escape hatch when the built-in feature set isn't enough — a tokenizer tuned for your language, or a SQL function that keeps logic next to the data. With the Capacitor SQLite plugin, you write the extension once and integrate it per platform: bundle a native library and pass `androidExtensions` on Android, or statically link and register with `sqlite3_auto_extension` on iOS. The native build is the main effort; the plugin handles the rest.

If you're new to the plugin's API, the [Exploring the Capacitor SQLite API](/blog/exploring-the-capacitor-sqlite-api/) post is a good next read.

Join the Capawesome [Discord](https://discord.gg/VCXxSVjefW) server for questions and subscribe to the Capawesome [newsletter](https://capawesome.io/newsletter) to stay updated.

June 17, 2026 

Back to top