npm package: @may-db/core
Description: Build collaborative apps, not backends.
Latest version: 0.1.10

# @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

```bash
pnpm add @may-db/core @may-db/react
```

## Compatibility policy

It is early days.

`@may-db/core@0.1.x` is validated against this dependency pair:

- `matrix-crdt@0.2.1-alpha.1`
- `matrix-js-sdk@40.2.0`

Support policy:

- Treat `matrix-crdt` and `matrix-js-sdk` as 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:

```ts
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.

```ts
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.

```tsx
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.

```tsx
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.

```tsx
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 + `db`
- `db.rooms.create/open/getOrCreateSingleton(...)`
- `useRoom({ db, roomId })` -> `room`, `doc`, `ready`, `canEdit`, `name`
- `room.members.*` to share a room with users
- `room.delegates.*` to share a room with the members of other rooms
- `useMayDbIndex({ db, indexName })` -> reactive index handle
- `useMayDbQuery({ index, after, where?, orderBy?, limit? })` -> `items`, `hasMore`, `loading`
- `useRoomPresence(...)` to exchange presence/cursor with other users