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— Returnnullif not found (don't throw) -
listBoards— Support all filter combinations -
updateBoard— Don't overwrite theidfield -
deleteBoard— Cascade delete pages, elements, members, share links -
createPage— Auto-name as "Untitled Page" if no name given -
getPages— Sort byorderascending -
getPage— Returnnullif not found -
reorderPages— Update each page'sorderfield -
deletePage— Also delete associated elements -
upsertElements— Create or update semantics; batch-friendly for performance -
getElements— Sort byzIndexascending -
deleteElements— Accept array of IDs, batch delete -
setMember— Create or update member role -
getMembers— Return all members for a board -
getMemberRole— Returnnullif not a member -
removeMember— Handle non-existent member gracefully -
createShareLink— Generate unique token -
resolveShareLink— Returnnullfor expired links -
deleteShareLink— Handle non-existent link gracefully