@may-db/core
v0.1.10
Published
Build collaborative apps, not backends.
Readme
@may-db/core
Build collaborative apps, not backends.
MayDB is a client-side "Bring your own server" database for collaborative, realtime applications.
- Developers are not responsible for a backend server
- Users select a Matrix provider to store their data
Install
pnpm add @may-db/core @may-db/reactCompatibility policy
It is early days.
@may-db/[email protected] is validated against this dependency pair:
Support policy:
- Treat
matrix-crdtandmatrix-js-sdkas a tested pair. - Avoid overriding either package version unless you are validating the combo yourself.
Yjs import rule
Use the Yjs instance re-exported by may-db:
import { Y } from "@may-db/core";Do not import from yjs directly in app code. Using different Yjs module instances can break constructor checks and CRDT behavior.
Key concepts
Room: The building block of MayDB. Each room has:
- Users who can edit it
- Users who can view it
- A Yjs CRDT containing data
- Delegate rooms, whose members can act as members here
- A room in Matrix with corresponding users, containing the data
Walkthrough
This example builds a collaborative note app: create notes, edit them live with others, and share them by Matrix ID.
Schema
Start by defining your room types and indexes, so you can list and query them.
import type { MayDbSchema } from "@may-db/core";
const SCHEMA: MayDbSchema = {
roomTypes: { note: {} },
indexes: {
notes: {
scope: "private",
roomTypes: ["note"],
columns: {
roomName: { source: { kind: "system", field: "roomName" } },
},
defaultOrderBy: { column: "roomName", direction: "asc" },
maxEntries: 5000,
},
},
};The notes index is private, so it automatically tracks rooms this user has created or been shared into.
Authentication
useMayDb handles Matrix authentication and wires up the session.
import { useEffect, useMemo, useState } from "react";
import type { MayDb } from "@may-db/core";
import { useMayDb, useRoom, useMayDbIndex, useMayDbQuery } from "@may-db/react";
export function TinyNoteApp() {
const { status, db, login } = useMayDb({
namespace: "com.may-db.tiny-note",
schema: SCHEMA,
});
const [roomId, setRoomId] = useState<string | null>(null);
if (status === "signed_out") {
return <button onClick={login}>Log in with Matrix</button>;
}
if (status !== "ready" || !db) {
return <p>Loading...</p>;
}
if (roomId) {
return <TinyNoteRoom db={db} roomId={roomId} onBack={() => setRoomId(null)} />;
}
return <NoteList db={db} onOpen={setRoomId} />;
}namespace scopes all rooms and indexes to your app. Rooms created by other apps never appear here, even on the same homeserver.
Querying for Rooms
useMayDbIndex opens an index, and should be inserted at the top level of your application. useMayDbQuery queries it reactively.
function NoteList({ db, onOpen }: { db: MayDb; onOpen: (id: string) => void }) {
const { index } = useMayDbIndex({ db, indexName: "notes" });
const { items } = useMayDbQuery({
index,
after: null,
});
return (
<main>
<button
onClick={async () => {
const room = await db.rooms.create({
name: "Untitled Note",
roomType: "note",
});
onOpen(room.id);
}}
>
New note
</button>
<ul>
{items.map((item) => (
<li key={item.roomId}>
<button onClick={() => onOpen(item.roomId)}>
{String(item.values.roomName)}
</button>
</li>
))}
</ul>
</main>
);
}By default, useMayDbQuery matches all rows (where: {}), uses the index defaultOrderBy, and returns up to the index maxEntries. Rooms you create appear here immediately. When another user shares a room with you, it appears in your list after a few seconds — no extra step needed in app code.
useMayDbQuery supports exact-match filtering per column and cursor pagination via after.
Collaboration
useRoom provides a Yjs doc and syncs it in real time with other users in the room. Call room.members.invite to share the room with someone by their Matrix ID.
function TinyNoteRoom({
db,
roomId,
onBack,
}: {
db: MayDb;
roomId: string;
onBack: () => void;
}) {
const { room, doc, ready, canEdit } = useRoom({ db, roomId });
const note = useMemo(() => doc.getText("note"), [doc]);
const [, invalidate] = useState(0);
const [invitee, setInvitee] = useState("");
// Minimal Yjs -> React subscription to illustrate. Libraries exist to handle this.
useEffect(() => {
const onChange = () => invalidate((n) => n + 1);
note.observe(onChange);
return () => note.unobserve(onChange);
}, [note]);
if (!ready) {
return <p>Syncing room...</p>;
}
return (
<main>
<button onClick={onBack}>← Notes</button>
<textarea
value={note.toString()}
onChange={(e) => {
const next = e.target.value;
note.doc?.transact(() => {
note.delete(0, note.length);
note.insert(0, next);
});
}}
disabled={!canEdit}
/>
<form
onSubmit={async (e) => {
e.preventDefault();
await room.members.invite(invitee.trim(), "editor");
setInvitee("");
}}
>
<input
value={invitee}
onChange={(e) => setInvitee(e.target.value)}
placeholder="@user:matrix.org"
/>
<button type="submit">Share note</button>
</form>
</main>
);
}The Yjs doc is a CRDT. It supports long text, as well as hierarchical JSON-like structures, both allowing safe concurrent edits to different parts of the doc.
API surface (app-facing)
useMayDb({ namespace, schema })-> Matrix auth/session +dbdb.rooms.create/open/getOrCreateSingleton(...)useRoom({ db, roomId })->room,doc,ready,canEdit,nameroom.members.*to share a room with usersroom.delegates.*to share a room with the members of other roomsuseMayDbIndex({ db, indexName })-> reactive index handleuseMayDbQuery({ index, after, where?, orderBy?, limit? })->items,hasMore,loadinguseRoomPresence(...)to exchange presence/cursor with other users
