An on-chain social room built on Thebes Protocol: a Motoko backend that holds rooms, messages, member profiles and the roster, and a React frontend served as certified assets.
The property this example proves: an accountable conversation. Every kept
message is attributed and strictly ordered; deletion leaves a tombstone (the
record that something was said is never quietly erased); trimming stays on the
books (kept + trimmed = everything ever sent); and the anti-spam cooldown is
enforced by the contract, not the client. The public oracle
(invariantReportView) recomputes those laws for every room on every read.
Live demo: https://memphis.mercaturaforum.com/_/raw/186131445741167/index.html
frontend (React + Vite + Tailwind) → chat backend (Motoko)
@thebes/sdk ── boundary client mo:thebes-lib ── Admin / MemphisAuth / Users / Pagination
Memphis passkey gate messages · profiles · roster
- frontend/ uses
@thebes/sdkfor the boundary client, typed query/update calls, React hooks, and the Memphis passkey gate. The SDK is vendored underfrontend/vendor/@thebes/sdkand resolved as a local dependency (upstream source of truth:thebes-sdk). - motoko/ uses
thebes-libforAdmin(controller-gated operations),MemphisAuth(passkey session verification),Users(profiles), andPagination; the room logic lives inmain.mo. The library is vendored undermotoko/thebes-liband resolved as a local Mops dependency.
Both halves are self-contained: the repository builds with no external Git or Mops toolkit pins. The frontend asset-canister wasm is the one artifact fetched at deploy time (see Deploy).
Sign-in is Memphis, contract 921. Two strings decide everything here and they are not the same value:
| What it is | Changing it | |
|---|---|---|
origin (on the gate) |
The pseudonym namespace. It decides every user's principal. | Orphans every existing account. Frozen for the life of this app — including when the app moves to a new domain. |
AUDIENCE |
The web origin this app is served from. Memphis compares it byte-exactly against the origin the token was minted for. | Harmless. Moving this app to its own domain is this one line. |
The backend verifies with:
switch (await* MemphisAuth.verifyWithAudience(gate, token, AUDIENCE)) { ... }await*, not await. verifyWithAudience is async*, and a plain await on
a module-level async helper that calls another contract replies with the
inner value instead of this method's own return — which shows up as a
client-side Candid decode error naming a field the method never declared.
On your own domain, the passkey ceremony cannot run in the page: a page may
only claim a WebAuthn RP ID that is a registrable-domain suffix of its own
origin. Use <MemphisConnectGate> from @thebes/sdk, which runs the ceremony
at the Memphis origin and returns a token minted for yours.
Full guide, both halves: docs/memphis.md.
| Method | Kind | Purpose |
|---|---|---|
register |
update | Create or update the caller's display name. |
setMyAvatar / setMyAvatarOrTrap |
update | Store the caller's media-contract avatar path; the OrTrap form traps on a failed guard so the client never silently ignores an error. |
myProfile / profileOf / userCount |
query | Read profiles and the member count. |
roster / rosterView |
query | Paginated member roster (offset, limit). |
post |
update | Post a message as the transport sender. |
postAs |
update | Post under a Memphis signed-in per-app principal. |
recent |
query | The last N messages. |
seedDemo |
update | Populate demo members + messages on an empty room. |
claimOwner / addAdmin / setPaused |
update | Ownership and admin surface (from thebes-lib's Admin). |
Messages are an append-only, trimmed log; the roster is paginated via
thebes-lib's Pagination. Avatar images live on a separate media contract — the
backend stores only the returned path string, never image bytes.
- Motoko compiler 1.4.1.
mops installfetches the pinned compiler to~/.cache/mops/moc/1.4.1/moc(macOS:~/Library/Caches/mops/moc/1.4.1/moc). Use that binary — themocon a defaultPATHmay be a different version, or Qt's unrelated Meta-Object Compiler. - Node 18+ and Mops for the two builds.
thebes-deployto deploy. The prebuilt binary is Linux x86-64; on other platforms build it from the release source bundle (cargo build --release -p thebes-deploy).
# Frontend
cd frontend
npm install # resolves the vendored @thebes/sdk
npm run dev # sync-sdk copies the browser runtimes into public/, then Vite serves
# Backend (compile-check)
cd ../motoko
mops install # resolves the vendored thebes-lib + the pinned compiler
"$(ls "$HOME/.cache/mops/moc/1.4.1/moc" "$HOME/Library/Caches/mops/moc/1.4.1/moc" 2>/dev/null | head -1)" --check $(mops sources) main.mothebes.toml describes the deploy. The [networks.wan].validators array ships
pre-filled with the current cluster endpoints — run thebes-deploy init to refresh
them.
Deploying your own copy? The committed
cidvalues pin the live catalog deployment (that's what the demo links serve — only its controller can upgrade it). Before your first deploy, setcid = "auto"on each canister: the deploy allocates fresh canisters you control and writes their ids back into the manifest.
thebes-deploy identity new me # one-time local signing identity
thebes-deploy deploy chat # build + install + verify → prints the backend cidThe frontend installs an asset canister, then uploads your built bundle. Fetch the
asset-canister wasm once (it is referenced by thebes.toml as asset_canister.wasm):
curl -L -o asset_canister.wasm \
https://github.com/Mercatura-Forum/Thebes-Protocol-/releases/download/asset-canister-v0.1.0/asset_canister.wasmBuild the bundle and point it at your backend cid (the frontend reads
window.CHAT_CID at runtime), then deploy:
cd frontend && npm run build && cd ..
# inject the backend cid from step 1 into the built page:
sed -i 's#<head>#<head><script>window.CHAT_CID=YOUR_CHAT_CID;</script>#' frontend/dist/index.html
thebes-deploy deploy web # install asset canister + upload bundle + verifyThe deploy prints the live URL:
https://memphis.mercaturaforum.com/_/raw/<web-cid>/index.html.
Member avatars are served by a separate media canister via
window.MEDIA_CID. It is optional — without one, members render without avatar images.
For a machine-readable deploy contract, see AGENTS.md.
Apache-2.0. See LICENSE.