# How Nautilus Wallet Implements Multi-Wallet Support in IndexedDB

> Discover how Nautilus Wallet manages multi-wallet support using IndexedDB. Learn about its innovative data isolation strategy with walletId foreign keys.

- Repository: [Nautilus Team/nautilus-wallet](https://github.com/nautls/nautilus-wallet)
- Tags: internals
- Published: 2026-03-07

---

**Nautilus Wallet implements multi-wallet support by storing all wallet data in a single IndexedDB instance and isolating each wallet's records through a `walletId` foreign key that exists on every table except the wallet metadata table itself.**

Nautilus Wallet is an open-source cryptocurrency wallet built for the Ergo blockchain that must handle multiple user wallets simultaneously while maintaining strict data isolation. The application achieves robust multi-wallet support through a relational database pattern implemented in Dexie.js, where every address, asset, UTXO, and DApp connection carries a `walletId` reference back to its parent wallet.

## The Core Architecture: Foreign-Key Isolation

The foundation of Nautilus Wallet's multi-wallet database structure is the **`walletId` foreign-key pattern**. Every table that stores wallet-specific data includes a `walletId` column that references the primary key of the `wallets` table. This design allows the application to filter all queries by the active wallet ID, ensuring complete data isolation without requiring separate database instances.

### Database Tables and Relationships

The schema defines six primary tables, with five containing the `walletId` foreign key:

| Table | Primary Key | Foreign Key | Purpose |
|-------|-------------|-------------|---------|
| `wallets` | `++id` (auto-increment) | – | Stores wallet metadata (name, network, public key, settings) |
| `addresses` | `&script` | `walletId` | All derived addresses for a wallet |
| `assets` | `&[tokenId+address]` | `walletId` | Token balances per address |
| `utxos` | `&id` | `walletId` | UTXO entries belonging to a wallet |
| `connectedDApps` | `&origin` | `walletId` | DApp-wallet linkages per wallet |
| `assetInfo` | `&id` | – | Global token metadata (shared across wallets) |

The `assetInfo` table is the only data store that lacks a `walletId` because it contains global token metadata shared across all wallets.

## Schema Definition and Versioning

The database schema and migration logic reside in **[`src/database/dbContext.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/dbContext.ts)**. The `NautilusDb` class extends Dexie and defines the table structures with explicit indexes on the `walletId` fields to ensure query performance.

```typescript
// src/database/dbContext.ts
class NautilusDb extends Dexie {
  wallets!: Table<IDbWallet, number>;
  addresses!: Table<IDbAddress, string>;
  assets!: Table<IDbAsset, string[]>;
  connectedDApps!: Table<IDbDAppConnection, string>;
  utxos!: Table<IDbUtxo, string>;
  assetInfo!: Table<IAssetInfo, string>;

  constructor() {
    super("nautilusDb");
    this.version(1).stores({
      wallets: "++id, network, &publicKey",
      addresses: "&script, type, walletId",
      assets: "&[tokenId+address], &[address+tokenId], walletId"
    });
    // … additional versions add fields and indexes (e.g. utxos, assetInfo)
  }
}

```

The versioning strategy allows Nautilus to add new columns and indexes while preserving existing wallet data. Each version upgrade can perform data migrations that maintain the `walletId` associations.

## Type Safety with walletId

The TypeScript definitions in **[`src/types/database.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/types/database.ts)** enforce the `walletId` requirement at the type level, ensuring that every entity creation includes the foreign key reference.

```typescript
// src/types/database.ts
export interface IDbAddress {
  type: AddressType;
  state: AddressState;
  script: string;
  index: number;
  walletId: number;           // <-- ties address to a wallet
}

export interface IDbAsset {
  tokenId: string;
  confirmedAmount: string;
  address: string;
  walletId: number;           // <-- ties asset balance to a wallet
}

export interface IDbUtxo {
  id: string;
  confirmed: boolean;
  address?: string;
  walletId: number;           // <-- ties UTXO to a wallet
}

```

These interfaces prevent compile-time errors where a developer might forget to associate a new address or UTXO with its parent wallet.

## Service Layer Data Isolation

All database interactions flow through dedicated service classes that enforce the `walletId` filter on every operation. This pattern ensures that no service can accidentally access another wallet's data.

### Wallets Service and Cascading Deletes

The **[`src/database/walletsDbService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/walletsDbService.ts)** file handles wallet lifecycle management, including a cascading delete that removes all associated records when a wallet is deleted.

```typescript
// src/database/walletsDbService.ts
public async delete(walletId: number): Promise<void> {
  await Promise.all([
    dbContext.addresses.where({ walletId }).delete(),
    dbContext.assets.where({ walletId }).delete(),
    dbContext.connectedDApps.where({ walletId }).delete(),
    dbContext.utxos.where({ walletId }).delete(),
    dbContext.wallets.delete(walletId)            // finally delete the wallet record
  ]);
}

```

This method ensures referential integrity by removing all child records before deleting the parent wallet entry.

### Address and Asset Services

The **[`src/database/addressesDbService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/addressesDbService.ts)** and **[`src/database/assetsDbService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/assetsDbService.ts)** files demonstrate the standard query pattern used across all services.

```typescript
// src/database/addressesDbService.ts
async getByWalletId(walletId: number): Promise<IDbAddress[]> {
  return dbContext.addresses.where({ walletId }).sortBy("index");
}

// src/database/assetsDbService.ts
public async getByWalletId(walletId: number): Promise<IDbAsset[]> {
  return dbContext.assets.where({ walletId }).toArray();
}

```

Both services use Dexie's `where({ walletId })` method to filter results, ensuring that the UI layer receives only the data relevant to the currently active wallet.

## Practical Implementation: Creating and Querying Wallets

The multi-wallet architecture enables straightforward workflows for adding new wallets and retrieving their isolated data.

### Adding a New Wallet

When creating a wallet, the application generates the metadata record first, then associates all derived data with the returned auto-increment ID.

```typescript
import { walletsDbService } from '@/database/walletsDbService';
import { addressesDbService } from '@/database/addressesDbService';
import { assetsDbService } from '@/database/assetsDbService';

// 1️⃣ Create the wallet meta record (Dexie will assign an auto-increment id)
const walletId = await walletsDbService.put({
  name: 'My second wallet',
  network: 'ergo',
  type: 'hd',
  publicKey: '02ab…',
  chainCode: '…',
  settings: { avoidAddressReuse: false, hideUsedAddresses: false, defaultChangeIndex: 0 }
});

// 2️⃣ Generate derived addresses (example for index 0‑4) and store them
const derived = [
  { type: 'receive', state: 'unused', script: '0x01…', index: 0, walletId },
  { type: 'receive', state: 'unused', script: '0x02…', index: 1, walletId },
  // …
];
await addressesDbService.bulkPut(derived);

// 3️⃣ Initialise empty asset entries (e.g., ERG balance)
await assetsDbService.bulkPut([
  { tokenId: 'ERG_TOKEN_ID', confirmedAmount: '0', address: derived[0].script, walletId }
]);

```

All subsequent queries using `walletId` will return only the records associated with this specific wallet.

### Retrieving Wallet-Specific Data

To load a wallet's complete context, services filter by the `walletId` foreign key.

```typescript
import { walletsDbService } from '@/database/walletsDbService';
import { addressesDbService } from '@/database/addressesDbService';
import { assetsDbService } from '@/database/assetsDbService';

async function loadWalletContext(id: number) {
  const wallet = await walletsDbService.getById(id);
  const addresses = await addressesDbService.getByWalletId(id);
  const assets = await assetsDbService.getByWalletId(id);
  return { wallet, addresses, assets };
}

```

Because each service internally uses `where({ walletId })`, the returned collections are strictly isolated to the requested wallet.

## Summary

- **Foreign-key isolation** via the `walletId` column on every table except `wallets` ensures complete data separation between multiple wallets in a single IndexedDB instance.
- **Dexie.js schema** in [`src/database/dbContext.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/dbContext.ts) defines indexed tables with `walletId` fields for performant queries and supports versioned migrations.
- **Type safety** through [`src/types/database.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/types/database.ts) enforces `walletId` requirements at compile time, preventing orphaned records.
- **Service layer** classes in `src/database/*DbService.ts` isolate data by filtering every query with `where({ walletId })`, including cascading deletes that clean up all child records when a wallet is removed.
- **Scalable architecture** allows users to create unlimited wallets while maintaining O(1) lookup performance through IndexedDB indexing.

## Frequently Asked Questions

### How does Nautilus Wallet prevent data leakage between wallets?

Nautilus Wallet prevents data leakage by requiring a `walletId` foreign key on every database table except the global `assetInfo` store. All service-layer queries in files like [`src/database/addressesDbService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/addressesDbService.ts) and [`src/database/assetsDbService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/assetsDbService.ts) use Dexie's `where({ walletId })` method to filter results, ensuring that the UI layer can only access records explicitly linked to the currently active wallet ID.

### What happens to a wallet's data when it is deleted?

When a wallet is deleted through `walletsDbService.delete()` in [`src/database/walletsDbService.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/walletsDbService.ts), the method performs a cascading cleanup operation. It executes `Promise.all()` to simultaneously delete all records from the `addresses`, `assets`, `connectedDApps`, and `utxos` tables where `walletId` matches the deleted wallet, followed by the wallet record itself, ensuring no orphan data remains in IndexedDB.

### Can Nautilus Wallet handle an unlimited number of wallets?

Yes, the architecture supports an effectively unlimited number of wallets limited only by the browser's IndexedDB storage quota. Because each wallet's data is tagged with a numeric `walletId` and indexed in Dexie, queries remain performant at O(1) complexity regardless of how many wallets exist in the database, allowing users to create, switch between, and delete wallets without performance degradation.

### How does the database schema handle migrations when adding new features?

The database schema handles migrations through Dexie's versioning system defined in [`src/database/dbContext.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/database/dbContext.ts). Each schema change increments the version number and specifies new indexes or columns; Dexie automatically handles the migration by preserving existing data and adding new fields, while the TypeScript interfaces in [`src/types/database.ts`](https://github.com/nautls/nautilus-wallet/blob/main/src/types/database.ts) ensure that new code accounts for the `walletId` field on any newly added tables.