Get started with Dexie Cloud
This tutorial goes through the basics on how to create a dexie cloud app. If you prefer looking at a working example:
1. Bootstrapping
No matter if you create a brand new app or adjust an existing one, this tutorial will guide you through the steps.
You can use whatever framework you prefer but in this tutorial weāll be showing some sample components in React, so if you start on an empty paper, Iād recommend using vite to bootstrap a react app:
npm create vite@latest my-app -- --template react-ts
Make sure to have dexie-related dependencies installed:
npm install dexie
npm install dexie-cloud-addon
npm install dexie-react-hooks # If using react
2. Declare a db
(still local only)
Unless you already use Dexie (in which case you could just adjust it), create a new module db.ts
where you declare the database.
If migrating from vanilla Dexie.js to Dexie Cloud, make sure to remove any auto-incrementing keys (such as ++id
- replace with @id
or just id
) as primary keys has to be globally unique strings in Dexie Cloud.
// db.ts
import { Dexie } from 'dexie';
import dexieCloud from 'dexie-cloud-addon';
export const db = new Dexie('mydb', { addons: [dexieCloud] });
db.version(1).stores({
items: 'itemId',
animals: `
@animalId,
name,
age,
[name+age]`
});
In this example we declare 2 tables: "items"
and "animals"
."itemId"
is the primary key for "items"
and "animalId"
for "animals"
.
Notice the @
in @animalId
. This makes it auto-generated and is totally optional but can be handy since it makes it easier to add new objects to the table.
Note that animals
also declares some secondary indices name
, age
and a [name+age]
(a compound index). These indices are here only to examplify. For this tutorial, we only need the ānameā index. A rule of thumb here is to only declare secondary index if needed in a where- or orderBy expression. And donāt worry - you can add or remove indices later
3. Add Types (optional)
// Item.ts
export interface Item {
itemId: string;
name: string;
description: string;
}
// Animal.ts
export interface Animal {
animalId: string;
name: string;
age: number;
}
Then adjust the db.ts
module weāve already created so that it looks something like this:
// db.ts
import dexieCloud, { type DexieCloudTable } from 'dexie-cloud-addon';
import type { Item } from './Item.ts';
import type { Animal } from './Animal.ts';
export const db = new Dexie('mydb', { addons: [dexieCloud] }) as Dexie & {
items: DexieCloudTable<Item, 'itemId'>;
animals: DexieCloudTable<Animal, 'animalId'>;
};
db.version(1).stores({
items: 'itemId',
animals: `
@animalId,
name,
age,
[name+age]`
});
Weāre actually just casting our Dexie to force the typings to reflect the items
and animals
tables that we are declaring in db.version(1).stores(ā¦).
_Thereās also the option to declare the entities as classes instead of interfaces. See TodoList.ts, TodoDB.ts and db.ts in the dexie-cloud-todo-list example. If you find that way more appealing, thatās also ok.
4. Play Around
Create some components that renders and manipulates the database. In this example, we use React + Typescript that demonstrate basic CRUD with a Dexie Cloud animals
table.
// components/App.tsx
import React from 'react';
import CreateAnimal from './CreateAnimal';
import AnimalList from './AnimalList';
export default function App() {
return (
<>
<style>
div.animal { display: 'flex', align-items: 'center', gap: 8 }
div.create-form { display: 'flex', gap: 8, margin-bottom: 12 }
</style>
<div>
<h1>Animals</h1>
<CreateAnimal />
<AnimalList />
</div>
</>
);
}
App: top-level component that renders CreateAnimal
and AnimalList
.
// components/AnimalList.tsx
import React from 'react';
import { useLiveQuery } from 'dexie-react-hooks';
import { db } from '../db';
import AnimalView from './AnimalView';
import type { Animal } from '../Animal';
export default function AnimalList() {
const animals = useLiveQuery(() => db.animals.orderBy('name').toArray(), []);
if (!animals) return <div>Loadingā¦</div>;
return (
<ul>
{animals.map((a: Animal) => (
<li key={a.animalId}>
<AnimalView animal={a} />
</li>
))}
</ul>
);
}
AnimalList: lists animals using useLiveQuery
(live updates) and renders AnimalView
for each.
// components/AnimalView.tsx
import React from 'react';
import { db } from '../db';
import type { Animal } from '../Animal';
export default function AnimalView({ animal }: { animal: Animal }) {
const onDelete = async () => {
await db.animals.delete(animal.animalId);
};
return (
<div className="animal">
<div>
<strong>{animal.name}</strong> ā {animal.age} yrs
</div>
<button aria-label="Delete" onClick={onDelete} title="Delete">
šļø
</button>
</div>
);
}
AnimalView: shows name
and age
and a delete button that removes the item from the table.
// components/CreateAnimal.tsx
import React, { useState } from 'react';
import { db } from '../db';
export default function CreateAnimal() {
const [name, setName] = useState('');
const [age, setAge] = useState<number | ''>('');
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name || age === '') return;
await db.animals.add({ name, age: Number(age) });
setName('');
setAge('');
};
return (
<form onSubmit={onSubmit} className="create-form">
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(e.target.value ? Number(e.target.value) : '')}
placeholder="Age"
/>
<button type="submit">Add</button>
</form>
);
}
CreateAnimal: small form that adds a new animal to db.animals
(the table uses an auto-generated @animalId
).
Start the app and browse to it. Add and delete animals - see the app work with a local database only.
5. Make it Sync
Still, we havenāt connected Dexie Cloud in the picture. Everything is happening locally so far. Yes, weāve prepared the code but we havenāt yet connected it to a cloud database.
-
Create a database in the cloud
npx dexie-cloud create
This will produde two local files:
dexie-cloud.json
anddexie-cloud.key
. Make sure to .gitignore them:echo "dexie-cloud.json" >> .gitignore echo "dexie-cloud.key" >> .gitignore
-
White-list application URL (such as http://localhost:3000)
npx dexie-cloud whitelist http://localhost:3000 # assuming port 3000 # ...Dont forget (at a later stage) to also white-list public URLs: npx dexie-cloud whitelist https://mygreatapp02240s.azurewebsites.net
-
Pick the
dbUrl
from your localdexie-cloud.json
file and configure the database indb.ts
// db.ts ... db.cloud.configure({ databaseUrl: "<dbUrl>", })
-
Add a Login button to your App.tsx:
<button onClick={() => db.cloud.login()}>Login</button>
-
Now, launch the app and navigate a browser to it
6. Learn about Access Control and Sharing (optional)
By default, all data being created will remain private to the end user, even though kept in sync with the cloud. Learn more how you can create realms, roles, members and permissions to invite a group of users to a commonly shared realm of data.
See Access Control in Dexie Cloud
To share data is also a 100% local-first action. Even creating invitations to new users can be done while being offline or while being in a shaky network.
7. Use Dexie Cloud Manager (optional)
Login to Dexie Cloud Manager to manage:
- end-users seats
- end-user evaluation policy
- SMTP settings
- subscription upgrades
8. Use dexie-cloud
CLI
The CLI can be used to switch between databases, export, import, authorize colleguaes. See all commands in the CLI docs.
8. Customize Authentication (optional)
Choose between:
- Keep the default authentication but customize the GUI
- Replace authentication in its whole with a custom solution
9. Customize Email Templates
Email templates for outgoing emails can be customized using the npx dexie-cloud templates command.
10. FAQ
What happens when clicking login button?
The default authentication dialog (which is customizable) will ask for an email address for one-time password (OTP) authentication and prompt for the OTP. If this was the first time of login, your user will be registered in the database - otherwise it acts as a normal login. Once logged in / registered - the local database will be in sync with your account on your dexie-cloud database.
- You get prompted for email
- You get prompted for OTP
- You enter OTP
- You get logged in
- All local data is uploaded to cloud and cloud data is downloaded
- Now the local and remote databases are connected in real time.
The login flow typically happens once per end user and device. Itās a part of the setup process for your application. Users can logout but if not, their device will be persistently logged in for as long as the local database lives.
Can I force a login + initial sync before any data is accessed?
Yes, a requireAuth property can be passed to db.cloud.configure(). This will block an query until a user is logged in and has completed an initial sync flow. Itās also possible to force a login as a specified email or userId and even to provide an OTP token this way (for example read from the query if the a magic link was sent).
Is it possible to Logout?
Yes, but local first apps are normally intended to have long or even eternal login sessions. A logout from a local first app is similar to erasing the local database.
A logout button can be added that calls db.cloud.logout()
when clicked.
What is dexie-cloud.key
good for?
This file is needed when you use the CLI (npx dexie-cloud
) to whitelist, export, import etc. Itās not needed for web applications as it is authorized using the npx dexie-cloud whitelist
command instead. The clientId and clientSecret is also needed when using the the REST API.
Why should dexie-cloud.json
and dexie-cloud.key
be .gitignored?
Keys shall never be committed to git (dexie-cloud.key
). dexie-cloud.json
does not contain any sensitive data but is still not tied to your code base - some other person might want to run the app on another database.
How can I make my webapp an installable app (PWA) on desktop and mobile?
To make your webapp a Progressive Web App (PWA), you only need a few small pieces:
- Create a web app manifest (
manifest.webmanifest
) with name, icons, start_url and display (standalone). - Add a service worker that caches your app shell and (optionally) API responses. Keep the service worker small and focused on offline/app-shell behavior.
- Register the service worker from your client code (e.g.
navigator.serviceWorker.register('/sw.js')
). - Ensure your site is served over HTTPS (localhost is allowed for development).
If you use Vite, there are community plugins that automate manifest generation and service-worker integration (for example vite-plugin-pwa
). These plugins can inject the manifest, generate precache lists and wire service-worker registration for you.
Quick checklist:
- Add
manifest.webmanifest
to your public folder and link it from<head>
. - Add a minimal
sw.js
(or use a plugin to generate Workbox-powered service worker). - Register the service worker in your app entry file.
- Test using Chrome/Edge Lighthouse or
web.dev/measure
and verify installability on mobile.
How can I bundle my app as a native app for iOS and Google Play?
To package your webapp as native apps you have a few solid options. The most common are:
- Capacitor (Ionic) ā modern, actively maintained native runtime that wraps your web app and provides native plugins. Good for both iOS and Android.
- Electron ā popular for packaging web apps as native desktop apps (macOS, Windows, Linux). Works well with PWAs and frameworks like Vite; pair with builders like
electron-builder
orelectron-forge
for installers. - PWABuilder / TWA (Trusted Web Activity) ā generate Android APKs/AABs from a PWA; TWA is ideal if your app is already a high-quality PWA.
- Cordova / PhoneGap ā older tooling still in use for legacy projects but generally superseded by Capacitor.
Recommended quick actions:
- Make sure your app is a solid PWA first (see previous section). PWAs are the best starting point for native packaging.
-
Choose a tool:
-
Capacitor: follow its setup docs to add platforms, copy the web build into the native projects and run builds with Xcode (iOS) and Android Studio (Android).
-
Electron: follow its docs to wrap your web build in a desktop runtime ā build the web UI, create a small main process that loads the built files, and use builders like
electron-builder
orelectron-forge
to produce installers; remember to configure signing/notarization and auto-updates. -
PWABuilder / TWA: use PWABuilder to generate an Android TWA wrapper or follow the TWA docs to create an AAB that links to your hosted PWA.
-
- Configure platform-specific settings: app id/package name, icons and splash screens, permissions, and any native plugins you need.
- Test on real devices and use platform tooling (Xcode for iOS, Android Studio / bundletool for Android) to create release builds and sign them.
- Follow the store submission guides to publish on App Store and Google Play (youāll need developer accounts, app listing assets, privacy policy, etc.).
Useful documentation:
- Capacitor: https://capacitorjs.com/docs
- Electron: https://www.electronjs.org/docs/latest
- PWABuilder: https://www.pwabuilder.com/
- Trusted Web Activity (Android / Google): https://developer.chrome.com/docs/android/trusted-web-activity/
- Apple App Store publishing: https://developer.apple.com/app-store/
- Google Play publishing: https://developer.android.com/distribute
How do I whitelist my app when bundled as native app with Capacitor?
npx dexie-cloud whitelist capacitor://localhost
npx dexie-cloud whitelist http://localhost
How do I whitelist my app when bundled with Electron?
Electron apps does not require whitelisting.
How can I get help
Let the community help out on:
- Stackoverflow
- Github Issues
- Discord
- Request private support from the dexie team: privsupport@dexie.org
We prefer getting questions on stackoverflow and Github because it they will be publicly searchable for other users and creates a helps learning AI engines.
Table of Contents
- API Reference
- Access Control in Dexie Cloud
- Add demo users
- Add public data
- Authentication in Dexie Cloud
- Best Practices
- Building Addons
- Collection
- Collection.and()
- Collection.clone()
- Collection.count()
- Collection.delete()
- Collection.desc()
- Collection.distinct()
- Collection.each()
- Collection.eachKey()
- Collection.eachPrimaryKey()
- Collection.eachUniqueKey()
- Collection.filter()
- Collection.first()
- Collection.keys()
- Collection.last()
- Collection.limit()
- Collection.modify()
- Collection.offset()
- Collection.or()
- Collection.primaryKeys()
- Collection.raw()
- Collection.reverse()
- Collection.sortBy()
- Collection.toArray()
- Collection.uniqueKeys()
- Collection.until()
- Compound Index
- Consistency in Dexie Cloud
- Consistent add() operator
- Consistent remove() operator
- Consistent replacePrefix() operator
- Consuming Dexie as a module
- Custom Emails in Dexie Cloud
- DBCore
- DBCoreAddRequest
- DBCoreCountRequest
- DBCoreCursor
- DBCoreDeleteRangeRequest
- DBCoreDeleteRequest
- DBCoreGetManyRequest
- DBCoreGetRequest
- DBCoreIndex
- DBCoreKeyRange
- DBCoreMutateRequest
- DBCoreMutateResponse
- DBCoreOpenCursorRequest
- DBCorePutRequest
- DBCoreQuery
- DBCoreQueryRequest
- DBCoreQueryResponse
- DBCoreRangeType
- DBCoreSchema
- DBCoreTable
- DBCoreTableSchema
- DBCoreTransaction
- DBCoreTransactionMode
- DBPermissionSet
- Deprecations
- Derived Work
- Design
- Dexie Cloud API
- Dexie Cloud API Limits
- Dexie Cloud Best Practices
- Dexie Cloud CLI
- Dexie Cloud Docs
- Dexie Cloud REST API
- Dexie Cloud Web Hooks
- Dexie Constructor
- Dexie.AbortError
- Dexie.BulkError
- Dexie.ConstraintError
- Dexie.DataCloneError
- Dexie.DataError
- Dexie.DatabaseClosedError
- Dexie.IncompatiblePromiseError
- Dexie.InternalError
- Dexie.InvalidAccessError
- Dexie.InvalidArgumentError
- Dexie.InvalidStateError
- Dexie.InvalidTableError
- Dexie.MissingAPIError
- Dexie.ModifyError
- Dexie.NoSuchDatabaseErrorError
- Dexie.NotFoundError
- Dexie.Observable
- Dexie.Observable.DatabaseChange
- Dexie.OpenFailedError
- Dexie.PrematureCommitError
- Dexie.QuotaExceededError
- Dexie.ReadOnlyError
- Dexie.SchemaError
- Dexie.SubTransactionError
- Dexie.Syncable
- Dexie.Syncable.IDatabaseChange
- Dexie.Syncable.IPersistentContext
- Dexie.Syncable.ISyncProtocol
- Dexie.Syncable.StatusTexts
- Dexie.Syncable.Statuses
- Dexie.Syncable.registerSyncProtocol()
- Dexie.TimeoutError
- Dexie.TransactionInactiveError
- Dexie.UnknownError
- Dexie.UnsupportedError
- Dexie.UpgradeError()
- Dexie.VersionChangeError
- Dexie.VersionError
- Dexie.[table]
- Dexie.addons
- Dexie.async()
- Dexie.backendDB()
- Dexie.close()
- Dexie.currentTransaction
- Dexie.debug
- Dexie.deepClone()
- Dexie.defineClass()
- Dexie.delByKeyPath()
- Dexie.delete()
- Dexie.derive()
- Dexie.events()
- Dexie.exists()
- Dexie.extend()
- Dexie.fakeAutoComplete()
- Dexie.getByKeyPath()
- Dexie.getDatabaseNames()
- Dexie.hasFailed()
- Dexie.ignoreTransaction()
- Dexie.isOpen()
- Dexie.js
- Dexie.name
- Dexie.on()
- Dexie.on.blocked
- Dexie.on.close
- Dexie.on.error
- Dexie.on.populate
- Dexie.on.populate-(old-version)
- Dexie.on.ready
- Dexie.on.storagemutated
- Dexie.on.versionchange
- Dexie.open()
- Dexie.override()
- Dexie.semVer
- Dexie.setByKeyPath()
- Dexie.shallowClone()
- Dexie.spawn()
- Dexie.table()
- Dexie.tables
- Dexie.transaction()
- Dexie.transaction()-(old-version)
- Dexie.use()
- Dexie.verno
- Dexie.version
- Dexie.version()
- Dexie.vip()
- Dexie.waitFor()
- DexieCloudOptions
- DexieError
- Docs Home
- Download
- EntityTable
- Export and Import Database
- Get started with Dexie Cloud
- Get started with Dexie in Angular
- Get started with Dexie in React
- Get started with Dexie in Svelte
- Get started with Dexie in Vue
- Hello World
- How To Use the StorageManager API
- Inbound
- IndexSpec
- Indexable Type
- IndexedDB on Safari
- Invite
- Member
- Migrating existing DB to Dexie
- MultiEntry Index
- PersistedSyncState
- Privacy Policy
- Promise
- Promise.PSD
- Promise.catch()
- Promise.finally()
- Promise.on.error
- Promise.onuncatched
- Questions and Answers
- Realm
- Releasing Dexie
- Road Map
- Road Map: Dexie 5.0
- Road Map: Dexie Cloud
- Role
- Run Dexie Cloud on Own Servers
- Sharding and Scalability
- Simplify with yield
- Support Ukraine
- SyncState
- Table
- Table Schema
- Table.add()
- Table.bulkAdd()
- Table.bulkDelete()
- Table.bulkGet()
- Table.bulkPut()
- Table.bulkUpdate()
- Table.clear()
- Table.count()
- Table.defineClass()
- Table.delete()
- Table.each()
- Table.filter()
- Table.get()
- Table.hook('creating')
- Table.hook('deleting')
- Table.hook('reading')
- Table.hook('updating')
- Table.limit()
- Table.mapToClass()
- Table.name
- Table.offset()
- Table.orderBy()
- Table.put()
- Table.reverse()
- Table.schema
- Table.toArray()
- Table.toCollection()
- Table.update()
- Table.upsert()
- Table.where()
- The main limitations of IndexedDB
- Transaction
- Transaction.abort()
- Transaction.on.abort
- Transaction.on.complete
- Transaction.on.error
- Transaction.table()
- Tutorial
- Typescript
- Typescript (old)
- Understanding the basics
- UserLogin
- Version
- Version.stores()
- Version.upgrade()
- WhereClause
- WhereClause.above()
- WhereClause.aboveOrEqual()
- WhereClause.anyOf()
- WhereClause.anyOfIgnoreCase()
- WhereClause.below()
- WhereClause.belowOrEqual()
- WhereClause.between()
- WhereClause.equals()
- WhereClause.equalsIgnoreCase()
- WhereClause.inAnyRange()
- WhereClause.noneOf()
- WhereClause.notEqual()
- WhereClause.startsWith()
- WhereClause.startsWithAnyOf()
- WhereClause.startsWithAnyOfIgnoreCase()
- WhereClause.startsWithIgnoreCase()
- Y.js
- db.cloud.configure()
- db.cloud.currentUser
- db.cloud.currentUserId
- db.cloud.events.syncComplete
- db.cloud.invites
- db.cloud.login()
- db.cloud.logout()
- db.cloud.options
- db.cloud.permissions()
- db.cloud.persistedSyncState
- db.cloud.roles
- db.cloud.schema
- db.cloud.sync()
- db.cloud.syncState
- db.cloud.userInteraction
- db.cloud.usingServiceWorker
- db.cloud.version
- db.cloud.webSocketStatus
- db.members
- db.realms
- db.roles
- db.syncable.connect()
- db.syncable.delete()
- db.syncable.disconnect()
- db.syncable.getOptions()
- db.syncable.getStatus()
- db.syncable.list()
- db.syncable.on('statusChanged')
- db.syncable.setFilter()
- dexie-cloud-addon
- dexie-react-hooks
- liveQuery()
- unhandledrejection-event
- useDocument()
- useLiveQuery()
- useObservable()
- usePermissions()
- y-dexie