My App
BoardKitGuides

Custom Storage Adapter

Step-by-step guide to implementing BoardStorage for a custom database

This guide walks through implementing a custom BoardStorage adapter for a database that is not supported out of the box.

Overview

BoardStorage is an abstract class with 17 methods covering boards, pages, elements, members, and share links. You implement all of them for your database of choice.

Step 1: Extend BoardStorage

import {
    BoardStorage,
    type CreateBoardInput,
    type CreatePageInput,
    type BoardFilters,
    type ElementUpsert,
} from '@hfu.digital/boardkit-nestjs';
import type { Board, Page, Element, BoardMember, ShareLink } from '@hfu.digital/boardkit-core';

export class MongoDBBoardStorage extends BoardStorage {
    constructor(private readonly db: Db) {
        super();
    }

    // Implement all abstract methods...
}

Step 2: Board CRUD

async createBoard(data: CreateBoardInput): Promise<Board> {
    const board: Board = {
        id: new ObjectId().toString(),
        name: data.name,
        ownerId: data.ownerId,
        sessionType: data.sessionType ?? 'persistent',
        isArchived: false,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
    };
    await this.db.collection('boards').insertOne(board);
    return board;
}

async getBoard(id: string): Promise<Board | null> {
    return this.db.collection<Board>('boards').findOne({ id }) ?? null;
}

async listBoards(filters: BoardFilters): Promise<Board[]> {
    const query: Record<string, unknown> = {};
    if (filters.ownerId) query.ownerId = filters.ownerId;
    if (filters.isArchived !== undefined) query.isArchived = filters.isArchived;
    if (filters.sessionType) query.sessionType = filters.sessionType;
    return this.db.collection<Board>('boards').find(query).toArray();
}

async updateBoard(id: string, data: Partial<Board>): Promise<Board> {
    const { id: _id, ...updateData } = data;
    await this.db.collection('boards').updateOne(
        { id },
        { $set: { ...updateData, updatedAt: new Date().toISOString() } },
    );
    return (await this.getBoard(id))!;
}

async deleteBoard(id: string): Promise<void> {
    // Cascade delete related data
    const pages = await this.getPages(id);
    for (const page of pages) {
        await this.db.collection('elements').deleteMany({ pageId: page.id });
    }
    await this.db.collection('pages').deleteMany({ boardId: id });
    await this.db.collection('members').deleteMany({ boardId: id });
    await this.db.collection('shareLinks').deleteMany({ boardId: id });
    await this.db.collection('boards').deleteOne({ id });
}

Step 3: Element Operations

The most important methods for performance are the element operations, since they are called frequently during collaboration.

async upsertElements(pageId: string, elements: ElementUpsert[]): Promise<void> {
    const bulk = this.db.collection('elements').initializeUnorderedBulkOp();
    const now = new Date().toISOString();

    for (const el of elements) {
        bulk.find({ id: el.id }).upsert().updateOne({
            $set: {
                pageId: el.pageId,
                type: el.type,
                data: el.data,
                zIndex: el.zIndex,
                updatedAt: now,
            },
            $setOnInsert: {
                id: el.id,
                createdBy: el.createdBy,
                createdAt: now,
            },
        });
    }

    if (elements.length > 0) {
        await bulk.execute();
    }
}

async getElements(pageId: string): Promise<Element[]> {
    return this.db.collection<Element>('elements')
        .find({ pageId })
        .sort({ zIndex: 1 })
        .toArray();
}

async deleteElements(ids: string[]): Promise<void> {
    await this.db.collection('elements').deleteMany({ id: { $in: ids } });
}

Step 4: Register with BoardModule

import { Module } from '@nestjs/common';
import { BoardModule } from '@hfu.digital/boardkit-nestjs';
import { MongoDBBoardStorage } from './mongodb-board-storage';

@Module({
    imports: [
        BoardModule.register({
            storage: new MongoDBBoardStorage(mongoDb),
            // ... other adapters
        }),
    ],
})
export class AppModule {}

Step 5: Test with InMemoryBoardStorage

Before writing your custom adapter, use InMemoryBoardStorage to verify your application logic works. Then swap in your custom adapter and run integration tests.

Implementation Checklist

  • createBoard — Generate unique ID, set defaults
  • getBoard — Return null if not found (don't throw)
  • listBoards — Support all filter combinations
  • updateBoard — Don't overwrite the id field
  • deleteBoard — Cascade delete pages, elements, members, share links
  • createPage — Auto-name as "Untitled Page" if no name given
  • getPages — Sort by order ascending
  • getPage — Return null if not found
  • reorderPages — Update each page's order field
  • deletePage — Also delete associated elements
  • upsertElements — Create or update semantics; batch-friendly for performance
  • getElements — Sort by zIndex ascending
  • deleteElements — Accept array of IDs, batch delete
  • setMember — Create or update member role
  • getMembers — Return all members for a board
  • getMemberRole — Return null if not a member
  • removeMember — Handle non-existent member gracefully
  • createShareLink — Generate unique token
  • resolveShareLink — Return null for expired links
  • deleteShareLink — Handle non-existent link gracefully

On this page