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
3 changes: 3 additions & 0 deletions docs/develop/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ children:
- backpressure.md
- autopilot_provider_plugins.md
- course_calculations.md
- database_provider_plugins.md
- resource_provider_plugins.md
- weather_provider_plugins.md
- custom_renderers.md
Expand Down Expand Up @@ -44,6 +45,8 @@ For example, if the plugin you are looking to develop is providing access to inf

Or if you are looking to perform course calculations or integrate with an autopilot, you will want to review the _[Course API](../rest-api/course_api.md)_ documentation prior to commencing your project.

If your plugin needs to persist structured data (configuration, metadata, buffered records), use the _[Database API](../rest-api/database_api.md)_ instead of shipping your own database dependency. The server provides a managed SQLite database per plugin — see the _[Database API documentation](../rest-api/database_api.md)_ for usage, or _[Database Provider Plugins](./database_provider_plugins.md)_ if you want to provide an alternative backend.

**OpenApi description for your plugin's API**

If your plugin provides an API you should consider providing an OpenApi description. This promotes cooperation with other plugin/webapp authors and also paves the way for incorporating new APIs piloted within a plugin into the Signal K specification. _See [Add OpenAPI definition](#add-an-openapi-definition)_ below.
Expand Down
105 changes: 105 additions & 0 deletions docs/develop/plugins/database_provider_plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: Database Providers
---

# Database Provider Plugins

The Signal K server [Database API](../rest-api/database_api.md) provides plugins with access to a server-managed relational database. The actual storage backend is implemented by a **provider plugin**.

The server ships two built-in SQLite providers (`_builtin_nodesqlite` via node:sqlite, `_builtin` via better-sqlite3). A database provider plugin can register an alternative backend — for example PostgreSQL — to replace or supplement the built-in providers.

---

## Database Provider Interface

For a plugin to be a database provider it must implement the {@link @signalk/server-api!DatabaseProvider | `DatabaseProvider`} interface:

```typescript
export interface DatabaseProvider {
getPluginDb(pluginId: string): Promise<PluginDb>
close(): Promise<void>
}
```

The provider must return a {@link @signalk/server-api!PluginDb | `PluginDb`} handle that implements:

- `migrate(migrations)` — Apply schema migrations, tracking applied versions
- `query(sql, params)` — Execute a SELECT and return rows
- `run(sql, params)` — Execute INSERT/UPDATE/DELETE and return `{ changes, lastInsertRowid }`
- `transaction(fn)` — Execute a callback atomically with rollback on error

## Registering a Database Provider

A plugin registers itself as a database provider by calling the server's {@link @signalk/server-api!DatabaseProviderRegistry.registerDatabaseProvider | `registerDatabaseProvider`} function during startup.

Do this within the plugin `start()` method.

_Example:_

```javascript
module.exports = function (app) {
const plugin = {
id: 'signalk-database-postgres',
name: 'PostgreSQL Database Provider'
}

let provider

plugin.start = function (options) {
provider = new PostgresProvider(options)
app.registerDatabaseProvider(provider)
}

plugin.stop = function () {
app.unregisterDatabaseProvider()
}

plugin.schema = {
type: 'object',
properties: {
connectionString: {
type: 'string',
title: 'PostgreSQL connection string'
}
}
}

return plugin
}
```

When a provider is registered, it becomes the default provider. When unregistered (e.g. plugin stopped), the server falls back to the best available built-in provider.

> [!NOTE]
> Only one external database provider can be registered at a time. Registering a new provider replaces the previous one.

---

## Implementation Requirements

A database provider MUST:

1. **Isolate plugins from each other** — each `pluginId` must receive a separate database or schema. Plugins must not be able to read or write each other's data.

2. **Track migrations** — maintain a record of applied migration versions per plugin. The `migrate()` method must skip versions already applied and apply new ones in order.

3. **Support parameterized queries** — the `params` array must be bound safely (no string interpolation) to prevent SQL injection.

4. **Implement transactions** — the `transaction()` method must execute the callback atomically. If the callback throws, all changes within the transaction must be rolled back.

5. **Return correct `RunResult`** — the `run()` method must return `{ changes, lastInsertRowid }` where `changes` is the number of affected rows and `lastInsertRowid` is the row ID of the last INSERT.

6. **Handle `close()` gracefully** — the server calls `close()` during shutdown. The provider must release all connections and resources.

---

## SQL Portability

Plugin authors typically write SQLite-compatible SQL. If your provider uses a different database engine (e.g. PostgreSQL), be aware that:

- `INTEGER PRIMARY KEY AUTOINCREMENT` is SQLite-specific (PostgreSQL uses `SERIAL` or `GENERATED ALWAYS AS IDENTITY`)
- `REAL` type maps to `DOUBLE PRECISION` in PostgreSQL
- `JSON` column type and functions differ between engines
- SQLite's `ROWID` behavior has no direct PostgreSQL equivalent

A provider targeting PostgreSQL may need to translate common SQLite patterns. This is the provider's responsibility — consumer plugins should not need to change their SQL.
2 changes: 2 additions & 0 deletions docs/develop/rest-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ children:
- conventions.md
- autopilot_api.md
- course_api.md
- database_api.md
- history_api.md
- notifications_api.md
- radar_api.md
Expand All @@ -27,6 +28,7 @@ APIs are available via `/signalk/v2/api/<endpoint>`
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| [`Autopilot`](./autopilot_api.md) | Provide the ability to send common commands to an autopilot via a provider plugin. | `vessels/self/autopilot` |
| [Course](./course_api.md) | Set a course, follow a route, advance to next point, etc. | `vessels/self/navigation/course` |
| [Database](./database_api.md) | Server-managed relational database for plugin data storage via a provider plugin. | `database` |
| [History](./history_api.md) | Query historical data. | `history` |
| [Radar](./radar_api.md) | View and control marine radar equipment via a provider plugin. _(In development)_ | `vessels/self/radars` |
| [Resources](./resources_api.md) | Create, view, update and delete waypoints, routes, etc. | `resources` |
Expand Down
205 changes: 205 additions & 0 deletions docs/develop/rest-api/database_api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
title: Database API
---

# Database API

The _Database API_ provides plugins with access to a server-managed relational database for persisting structured data — configuration, metadata, buffered records, device registrations, etc.

Each plugin receives its own isolated database. A plugin cannot access another plugin's data. The server manages the database lifecycle (connections, file paths, shutdown) — plugins never open database files directly.

The actual storage backend is pluggable. The server ships two built-in SQLite providers and supports community-provided alternatives (e.g. PostgreSQL).

The API is available under the path `/signalk/v2/api/database`.

---

## Plugin Usage

Plugins access the Database API through the server's `getDatabaseApi()` method. This is the **consumer** interface — most plugins will use this.

### Getting a Database Handle

```javascript
const db = await app.getDatabaseApi().getPluginDb(plugin.id)
```

The returned {@link @signalk/server-api!PluginDb | `PluginDb`} handle is cached — calling `getPluginDb()` multiple times with the same plugin ID returns the same instance.

### Running Migrations

Use `migrate()` to create or update your schema. Migrations are tracked by version number and only applied once. Always call this before querying.

```javascript
await db.migrate([
{
version: 1,
sql: `CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)`
},
{
version: 2,
sql: `ALTER TABLE settings ADD COLUMN updated_at TEXT`
}
])
```

### Querying Data

```javascript
const rows = await db.query('SELECT * FROM settings WHERE key = ?', ['theme'])
// rows: [{ key: 'theme', value: 'dark', updated_at: '2026-03-05T...' }]
```

### Inserting, Updating, Deleting

```javascript
const result = await db.run('INSERT INTO settings (key, value) VALUES (?, ?)', [
'theme',
'dark'
])
console.log(result.changes) // 1
console.log(result.lastInsertRowid) // row ID
```

### Transactions

```javascript
await db.transaction(async (tx) => {
await tx.run('DELETE FROM settings WHERE key = ?', ['old_key'])
await tx.run('INSERT INTO settings (key, value) VALUES (?, ?)', [
'new_key',
'value'
])
})
// If either statement fails, both are rolled back.
```

### Complete Plugin Example

```javascript
module.exports = function (app) {
let db
const plugin = {
id: 'my-plugin',
name: 'My Plugin'
}

plugin.start = function (options) {
app
.getDatabaseApi()
.getPluginDb(plugin.id)
.then((pluginDb) => {
db = pluginDb
return db.migrate([
{
version: 1,
sql: `CREATE TABLE IF NOT EXISTS buffer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data TEXT NOT NULL,
created_at TEXT NOT NULL
)`
}
])
})
.catch((err) => {
app.error('Failed to initialize database: ' + err)
})

// Use db in intervals or event handlers — guard against async init
setInterval(() => {
if (!db) return
db.query('SELECT COUNT(*) as count FROM buffer').then((rows) => {
app.setPluginStatus(`${rows[0].count} records buffered`)
})
}, 30000)
}

plugin.stop = function () {
// No db.close() needed — the server manages the database lifecycle.
}

return plugin
}
```

> [!NOTE]
> The `getDatabaseApi().getPluginDb()` call is **async**. If your plugin uses `setInterval` or event-driven callbacks, you must guard against the database handle not being ready yet: `if (!db) return`.

---

## What This API Is Not For

The Database API is for **plugin-owned structured data** — configuration, metadata, buffered records. It is NOT for:

- **Time-series data** — use the [History API](./history_api.md)
- **Routes, waypoints, charts** — use the [Resources API](./resources_api.md)
- **Opening external SQLite files** (e.g. MBTiles) — those require direct file access outside this API's scope

---

## Providers

The Database API supports multiple provider backends. The server ships two built-in providers:

| Provider ID | Backend | Availability | Default when |
| --------------------- | --------------------------- | -------------------------- | ------------------------------ |
| `_builtin_nodesqlite` | SQLite via `node:sqlite` | Node.js >= 22.5.0 | `node:sqlite` is available |
| `_builtin` | SQLite via `better-sqlite3` | When native addon compiles | `node:sqlite` is not available |

Community plugins can register alternative providers (e.g. PostgreSQL). See [Database Provider Plugins](../plugins/database_provider_plugins.md).

### Listing Available Providers

To retrieve a list of registered database providers, submit an HTTP `GET` request to `/signalk/v2/api/database/_providers`.

_Example:_

```typescript
HTTP GET "/signalk/v2/api/database/_providers"
```

_Response:_

```JSON
{
"_builtin_nodesqlite": {
"isDefault": true
},
"_builtin": {
"isDefault": false
}
}
```

### Getting the Default Provider

To get the id of the _default_ provider, submit an HTTP `GET` request to `/signalk/v2/api/database/_providers/_default`.

_Example:_

```typescript
HTTP GET "/signalk/v2/api/database/_providers/_default"
```

_Response:_

```JSON
{
"id": "_builtin_nodesqlite"
}
```

### Setting the Default Provider

To change the default database provider, submit an HTTP `POST` request to `/signalk/v2/api/database/_providers/_default/{id}` where `{id}` is the identifier of the provider to use as the _default_.

_Example:_

```typescript
HTTP POST "/signalk/v2/api/database/_providers/_default/_builtin_nodesqlite"
```

> [!NOTE] Any registered provider can be set as the default. Changing the default provider does not migrate data between providers.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"as-fetch": "^2.1.4",
"baconjs": "^1.0.1",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^12.6.2",
"body-parser": "^1.20.3",
"bonjour-service": "^1.3.0",
"busboy": "^1.6.0",
Expand Down Expand Up @@ -153,6 +154,7 @@
"@tsconfig/node22": "^22.0.5",
"@types/baconjs": "^0.7.34",
"@types/bcryptjs": "^2.4.6",
"@types/better-sqlite3": "^7.6.13",
"@types/busboy": "^1.5.0",
"@types/chai": "^4.2.15",
"@types/command-exists": "^1.2.0",
Expand Down
Loading
Loading