Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,37 @@ jobs:
# flutter config --enable-linux-desktop
# flutter test integration_test/basics_test.dart -d linux

test-viewer:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- name: Set node version
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"

- name: Reuse npm cache folder
uses: actions/cache@v5
env:
cache-name: cache-node-modules
with:
path: |
~/.npm
./node_modules
key: ${{ runner.os }}-npm-viewer-x4-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-npm-viewer-x4-

- name: install npm dependencies
run: npm install

- name: build
run: npm run build

- name: test viewer plugin
run: npm run transpile && cross-env DEFAULT_STORAGE=memory mocha --config ./config/.mocharc.cjs ./test_tmp/unit/viewer.test.js --timeout 20000

test-tutorials:
runs-on: ubuntu-22.04
timeout-minutes: 30
Expand Down
139 changes: 139 additions & 0 deletions docs-src/docs/viewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: RxDB Viewer - Remote Database Inspection
slug: viewer.html
description: Inspect and manage your RxDB database remotely with the Viewer plugin. Browse collections, run queries, edit documents and observe live changes through a WebRTC connection.
---

import {Steps} from '@site/src/components/steps';

# RxDB Viewer

The RxDB Viewer plugin provides a browser-based UI to inspect and manage any RxDB database remotely. It connects to your running application over WebRTC, so no additional server infrastructure is required. Data flows directly between your app and the viewer in a peer-to-peer connection.

## Features

- Browse all collections with document counts
- View collection schemas
- Run Mango queries with sorting
- Observe queries for live-updating results via RxDB's reactive system
- Create, edit and delete documents
- Export collections as JSON
- Syntax-highlighted JSON document viewer

## How it Works

The viewer plugin starts a lightweight WebRTC signaling process inside your application. When you open the viewer HTML page in a browser and paste the connection parameters, a direct peer-to-peer connection is established. All database operations (queries, writes, deletes) are sent as messages over this WebRTC data channel.

Because it uses WebRTC, the viewer works across different machines on the same network, across the internet (via the signaling server), and even between different JavaScript runtimes (Node.js, browser, React Native).

## Installation

```ts
import {
getDatabaseConnectionParams
} from 'rxdb/plugins/viewer';
```

## Usage

<Steps>

## Get connection parameters

Call `getDatabaseConnectionParams()` with your database instance. The viewer server starts lazily on the first call and is cached for subsequent calls.

```ts
import { getDatabaseConnectionParams } from 'rxdb/plugins/viewer';

const connectionParams = getDatabaseConnectionParams(myRxDatabase);
console.log(JSON.stringify(connectionParams, null, 2));
// {
// "topic": "rxdb-viewer-abc123...",
// "signalingServerUrl": "wss://signaling.rxdb.info/",
// "databaseName": "mydb"
// }
```

You can pass options to customize the signaling server or topic:

```ts
const connectionParams = getDatabaseConnectionParams(myRxDatabase, {
signalingServerUrl: 'wss://my-signaling-server.com/',
topic: 'my-custom-topic'
});
```

## Open the viewer

The viewer is a standalone HTML page bundled with the plugin. You can serve it from your application or open it directly. It is located at `node_modules/rxdb/dist/plugins/viewer/viewer.html`.

## Connect

Paste the connection parameters JSON into the viewer's connection form and click **Connect**. The viewer establishes a WebRTC peer-to-peer connection to your application and loads the database structure.

</Steps>

## Options

| Option | Type | Default | Description |
|---|---|---|---|
| `signalingServerUrl` | `string` | `wss://signaling.rxdb.info/` | WebSocket URL of the signaling server used to establish the WebRTC connection |
| `topic` | `string` | Auto-generated | A unique room identifier. Both sides must use the same topic to connect. |
| `webSocketConstructor` | `WebSocket` constructor | Global `WebSocket` | Custom WebSocket implementation, useful in Node.js environments. |

## Automatic Lifecycle

The viewer server is managed automatically:

- **Lazy start**: The server is created on the first call to `getDatabaseConnectionParams()` and cached per database.
- **Auto-close**: When the database is closed (via `database.close()`), the viewer server shuts down automatically, closing all WebRTC connections and cleaning up subscriptions.
- **Shared instance**: Multiple calls to `getDatabaseConnectionParams()` for the same database return the same connection parameters.

## Using `startRxDBViewer()` directly

If you need access to the `ViewerState` object (for example, to manually close the viewer before the database closes), you can use `startRxDBViewer()`:

```ts
import { startRxDBViewer } from 'rxdb/plugins/viewer';

const viewerState = await startRxDBViewer(myRxDatabase, {
signalingServerUrl: 'wss://signaling.rxdb.info/'
});

// The connection params are available on the state
console.log(viewerState.connectionParams);

// Manually close the viewer
await viewerState.close();
```

## Viewer Capabilities

Once connected, the viewer UI supports these operations:

**Browsing**: The left sidebar lists all collections with document counts. Click a collection to load its documents in a table view. Click any row to see the full document JSON in the detail panel.

**Querying**: Enter a [Mango query](./rx-query.md) in the query bar and press Run. Click column headers to sort. Example queries:

```json
{ "selector": { "age": { "$gt": 18 } } }
```

```json
{ "selector": { "name": { "$regex": "^A" } }, "limit": 10 }
```

**Observing**: Click **Observe** to start a live query subscription. The Query Observer panel at the bottom shows all active subscriptions with their result counts and last update times. Results update in real-time as documents change in the database.

**Editing**: Click **+ Add** to create a new document (pre-filled from the collection schema). Select a document and click **Edit** to modify it. Click **Delete** to remove it.

**Exporting**: Click **Export** to download all documents in the current collection as a JSON file.

## Security Considerations

The viewer grants full read and write access to the connected database. Keep the connection parameters private. Anyone with the topic and signaling server URL can connect to your database.

For production use, consider:
- Using a private signaling server instead of the default public one
- Generating unique topics per session
- Only enabling the viewer in development or debug builds
8 changes: 4 additions & 4 deletions docs-src/refactor-charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ const path = require('path');

function walkDir(dir, callback) {
fs.readdirSync(dir).forEach(f => {
let dirPath = path.join(dir, f);
let isDirectory = fs.statSync(dirPath).isDirectory();
const dirPath = path.join(dir, f);
const isDirectory = fs.statSync(dirPath).isDirectory();
if (isDirectory) {
walkDir(dirPath, callback);
} else if (f.endsWith('.md') || f.endsWith('.mdx')) {
Expand Down Expand Up @@ -42,10 +42,10 @@ walkDir(path.join(__dirname, 'docs'), (filePath) => {
// Prepare new imports
let newImports = '';
if (needsBrowser) {
newImports += "import PerformanceBrowser from '@site/src/components/performance-browser';\n";
newImports += 'import PerformanceBrowser from \'@site/src/components/performance-browser\';\n';
}
if (needsNode) {
newImports += "import PerformanceNode from '@site/src/components/performance-node';\n";
newImports += 'import PerformanceNode from \'@site/src/components/performance-node\';\n';
}

// Insert new imports after frontmatter
Expand Down
4 changes: 2 additions & 2 deletions docs-src/replace-charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ const nData = `

function walkDir(dir, callback) {
fs.readdirSync(dir).forEach(f => {
let dirPath = path.join(dir, f);
let isDirectory = fs.statSync(dirPath).isDirectory();
const dirPath = path.join(dir, f);
const isDirectory = fs.statSync(dirPath).isDirectory();
if (isDirectory) {
walkDir(dirPath, callback);
} else if (f.endsWith('.md')) {
Expand Down
5 changes: 5 additions & 0 deletions docs-src/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,11 @@ const sidebars = {
id: 'webmcp',
label: 'WebMCP'
},
{
type: 'doc',
id: 'viewer',
label: 'Viewer'
},
{
type: 'doc',
id: 'third-party-plugins',
Expand Down
2 changes: 1 addition & 1 deletion docs-src/src/components/performance-chart-impl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type PerformanceDataPoint = {

type PerformanceChartImplProps = {
data: PerformanceDataPoint[];
metrics: { key: string; name: string; color: string }[];
metrics: { key: string; name: string; color: string; }[];
title?: string;
};

Expand Down
4 changes: 2 additions & 2 deletions docs-src/src/components/performance-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Suspense } from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import { PERFORMANCE_METRICS, PERFORMANCE_DATA_BROWSER, PERFORMANCE_DATA_NODE } from './performance-data';
// performance-data exports available: PERFORMANCE_METRICS, PERFORMANCE_DATA_BROWSER, PERFORMANCE_DATA_NODE

// Lazy load the chart implementation so recharts isn't in the main bundle
const PerformanceChartImpl = React.lazy(() => import('./performance-chart-impl'));
Expand All @@ -12,7 +12,7 @@ type PerformanceDataPoint = {

type PerformanceChartProps = {
data: PerformanceDataPoint[];
metrics?: { key: string; name: string; color: string }[];
metrics?: { key: string; name: string; color: string; }[];
title?: string;
};

Expand Down
16 changes: 8 additions & 8 deletions docs-src/src/components/performance-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { browserMetrics, PerformanceData } from './performance-browser';
const nodeData: PerformanceData[] = [
{
name: 'MongoDB',
"time-to-first-insert": 276.906,
"insert-documents-500": 47.497,
"find-by-ids-3000": 57.31,
"serial-inserts-50": 209.467,
"serial-find-by-id-50": 23.09,
"find-by-query": 42.315,
"find-by-query-parallel-4": 38.854,
"4x-count": 6.898
'time-to-first-insert': 276.906,
'insert-documents-500': 47.497,
'find-by-ids-3000': 57.31,
'serial-inserts-50': 209.467,
'serial-find-by-id-50': 23.09,
'find-by-query': 42.315,
'find-by-query-parallel-4': 38.854,
'4x-count': 6.898
},
{
name: 'Memory',
Expand Down
4 changes: 2 additions & 2 deletions docs-src/update-colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ const path = require('path');

function walkDir(dir, callback) {
fs.readdirSync(dir).forEach(f => {
let dirPath = path.join(dir, f);
let isDirectory = fs.statSync(dirPath).isDirectory();
const dirPath = path.join(dir, f);
const isDirectory = fs.statSync(dirPath).isDirectory();
if (isDirectory) {
walkDir(dirPath, callback);
} else if (f.endsWith('.md')) {
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,12 @@
"import": "./dist/esm/plugins/webmcp/index.js",
"default": "./dist/esm/plugins/webmcp/index.js"
},
"./plugins/viewer": {
"types": "./dist/types/plugins/viewer/index.d.ts",
"require": "./dist/cjs/plugins/viewer/index.js",
"import": "./dist/esm/plugins/viewer/index.js",
"default": "./dist/esm/plugins/viewer/index.js"
},
"./plugins/attachments-compression": {
"types": "./dist/types/plugins/attachments-compression/index.d.ts",
"require": "./dist/cjs/plugins/attachments-compression/index.js",
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/dev-mode/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,14 @@ export const ERROR_MESSAGES = {
* null checks etc. so you do not have to increase the
* build size with error message strings.
*/
// plugins/viewer
VW1: {
message: 'getDatabaseConnectionParams() called but startRxDBViewer() was not called on this database before.',
cause: 'The viewer server was not started on the database before calling getDatabaseConnectionParams().',
fix: 'Call startRxDBViewer(database) before calling getDatabaseConnectionParams(database).',
docs: ''
},

SNH: {
message: 'This should never happen',
cause: 'Should never be thrown. This error code is used for internal things like null-checks etc.',
Expand Down
19 changes: 19 additions & 0 deletions src/plugins/viewer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export {
startRxDBViewer,
getDatabaseConnectionParams,
VIEWER_DEFAULT_SIGNALING_SERVER
} from './viewer-server.ts';

export type {
ViewerConnectionParams,
ViewerServerOptions,
ViewerState,
ViewerMethod,
ViewerRequest,
ViewerResponse,
ViewerPushMessage,
ViewerDbInfo,
ViewerCollectionInfo,
ViewerSignalingMessage,
ViewerPeerState
} from './viewer-types.ts';
Loading