@mexty/database
v0.1.1
Published
Persistent per-block data storage for Mexty blocks
Downloads
90
Readme
@mexty/database
Persistent data storage for Mexty blocks. Rows survive the session, so a learner's answer is still there tomorrow, and a block later in a learning path can read what an earlier one collected.
If you want live state shared between people in the room right now, that is
@mexty/multiplayer — it is gone when the room empties. Use this when the data
has to outlive the visit. The two work together.
Before you write any code
Linking a database to a block is Mexty staff only, and it does not happen in
code. A staff member creates the database at /dashboard/databases and
attaches it to the block from the blueprint editor's Mexty database property.
This package reads whatever id ended up in the block's props.
Two things are required, and the first is the one that gets forgotten:
1. Declare the property in your BlockProps interface, verbatim:
/** Mexty database this activity reads and writes. Staff-managed. */
mextyDatabaseId?: string;Without that line the property cannot be linked from the editor at all —
the editor only offers keys it finds in your block's parsed BlockProps
schema. Declaring it is what makes the block linkable.
2. Add the dependency. The block scaffold does not ship it:
"dependencies": {
"@mexty/database": "^0.1.0"
}A range rather than the exact pin @mexty/multiplayer uses, and deliberately.
A block's dependency is resolved when the block is built, and the fixes that
matter here are client-side ones a block author cannot apply themselves — a
session that stops renewing, say. An exact pin means every block has to be
edited to receive one. ^0.1.0 picks up 0.1.x on the next rebuild and never
resolves to a version that is not on the registry yet.
⚠️ Everything in a database is readable
Any viewer who can open any block linked to a database can read every row in it, including other learners' rows. This is deliberate — it is what makes a shared leaderboard or a class-wide wall work — but it means:
- Never store secrets, contact details, or anything you would not print on the page.
- Store only what the learner would expect other learners to see.
- Rows carry an opaque
ownerId(a user id, oranon:…) and never an email or a display name.
Writing is narrower than reading: a row belongs to whoever wrote it, and only they — or staff — can change it.
Usage
Basic — one row per learner
import { useMextyDatabase, useMextyRecord } from '@mexty/database';
function MyBlock({ mextyDatabaseId }: BlockProps) {
const db = useMextyDatabase(mextyDatabaseId);
const { value, save, loading } = useMextyRecord(db, 'profile', 'main', {
nickname: '',
});
if (loading) return <p>Loading…</p>;
return (
<input
value={value.nickname}
onChange={(e) => save({ ...value, nickname: e.target.value })}
/>
);
}save upserts the caller's own row and is debounced, so calling it on
every keystroke is the intended usage, not an abuse of it.
Everyone's rows
import { useMextyRecords } from '@mexty/database';
const { records, reload } = useMextyRecords(db, 'answers');
const mine = useMextyRecords(db, 'answers', { mine: true });records is a snapshot, not a live feed — call reload() after you write, or
pair the database with @mexty/multiplayer if viewers need to see each other's
writes as they happen.
Appending rows
await db.insert('answers', { choice: 'b', correct: true });insert appends a new row every call and is not debounced. Never call it
in a render or from an unthrottled handler.
Handling the states a block can be in
const db = useMextyDatabase(props.mextyDatabaseId);
if (db.status === 'unauthenticated') return <SignInPrompt />;db.status is one of:
| status | meaning |
|---|---|
| ready | linked and reachable — reads and writes work |
| no-database | no database is linked to this block |
| sandbox | the block is not running with a blockId |
| unauthenticated | this database needs a signed-in viewer and there is none |
Render normally on all four. A non-ready client resolves reads empty and makes writes a no-op, so a block whose database was never linked still works — which is its normal state in the marketplace, and after anyone forks it.
API
useMextyDatabase(databaseId?)
The client for this block. Recreated only when the id changes, so it is safe to
call straight from props. Re-renders when status or callerId changes.
useMextyRecord(db, collection, key, initial)
{ value, save, loading, error, record } — one row, this viewer's own,
upserted at key. value updates immediately; the debounced write follows.
useMextyRecords(db, collection, options?)
{ records, loading, error, reload }. Options: mine, key, courseId,
programId, limit (server caps at 200).
The client directly
db.list(collection, options?) // rows, newest first
db.get(collection, key) // this viewer's row at key, or null
db.set(collection, key, data) // upsert, debounced
db.insert(collection, data) // append
db.update(recordId, data) // replace a row's data (owner or staff)
db.remove(recordId) // delete a row (owner or staff)
db.flush() // await pending debounced writes
db.callerId // which owner the server sees you as
db.context // blockId, courseId, programId… from the URLSemantics & limits
- Collections are
[a-zA-Z0-9_-], 1–64 characters. Keys additionally allow.and:, up to 128. - Rows are plain JSON, 64 KB each. No
Date,Map,Set, class instances or functions — store an ISO string, not aDate. setis per (collection, key, viewer). Two viewers writing the same key get their own rows; the same viewer writing it twice replaces theirs.updatereplacesdatawholesale. Read, modify, write if you want a merge.- Every database has a row ceiling (100,000 by default). Past it, appends fail with a clear error while keyed upserts keep working.
- Rows written from an editor preview are flagged as test data and excluded from staff exports, so trying your own block out does not pollute results.
- A
429is retried with backoff. The budget is 3000 requests per 5 minutes, per viewer, shared with everything else the page does. - An expired session is renewed once and the request replayed, so a block does
not go quiet ten minutes into a visit.
unauthenticatedmeans the renewal failed too, and the viewer really is signed out. - Do NOT call
configure()on the platform. The server URL is resolved from the page's hostname; overriding it points a block at the wrong environment.
Development
npm install
npm run typecheck
npm test
npm run build