Quick Start
Test in Memory
WeaveDB is built in such a modular way that you can run a rollup node and deploy databases without HyperBEAM and Arweave.
You can also build databases and test everything in memory without any server.
The easiest way to get started is to create a project with wdb-cli
and test basic features in memory with WDB SDK.
Create Project with WDB CLI
create a db project using the web-cli create
command.
npx wdb-cli create mydb && cd mydb
Now you havetest
directory to write tests.
Write Tests with WDB SDK
Replace /test/main.test.js
with the following code to test basic features.
import assert from "assert"
import { describe, it } from "node:test"
import { acc } from "wao/test"
import { DB } from "wdb-sdk"
import { mem } from "wdb-core"
const Bob = { name: "Bob", age: 20 }
const Alice = { name: "Alice", age: 30 }
const Mike = { name: "Mike", age: 25 }
describe("Basic API", () => {
it("should query DB", async () => {
const { q } = mem()
const db = new DB({ jwk: acc[0].jwk, hb: null, mem: q })
// create a new DB instance
const id = await db.init({ id: "mydb" })
console.log(`new DB ID: ${id}`)
// create users dir
await db.mkdir({
name: "users",
schema: { type: "object", required: ["name", "age"] },
auth: [["set:user,del:user", [["allow()"]]]],
})
// add users
await db.set("set:user", Bob, "users", "Bob")
await db.set("set:user", Alice, "users", "Alice")
await db.set("set:user", Mike, "users", "Mike")
// get Alice
const user = await db.get("users", "Alice") // Alice
assert.deepEqual(user, Alice)
// get users
const users = await db.get("users") // [Alice, Bob, Mike]
assert.deepEqual(users, [Alice, Bob, Mike])
// sort by age
const users2 = await db.get("users", ["age", "desc"]) // [Alice, Mike, Bob]
assert.deepEqual(users2, [Alice, Mike, Bob])
// only get 2
const users3 = await db.get("users", ["age", "asc"], 2) // [Bob, Mike]
assert.deepEqual(users3, [Bob, Mike])
// delete Mike
await db.set("del:user", "users", "Mike")
// get 2 again
const users4 = await db.get("users", ["age", "asc"], 2) // [Bob, Alice]
assert.deepEqual(users4, [Bob, Alice])
// get where age equals 30
const users5 = await db.get("users", ["age", "==", 30]) // [Alice]
assert.deepEqual(users5, [Alice])
})
})
Run Tests
Tests can run with yarn test-all
or yarn test test/main.test.js
.
yarn test-all
Building Minimum Viable Social Dapp
This tutorial will guide you through building a minimum viable social dapp (a decentralized Twitter/X) with WeaveDB.
create another db project using the web-cli create
command.
npx wdb-cli create social && cd social
Now you have db
directory to put config files, and test
directory to write tests.
Replace /test/main.test.js
with the following skelton, which initializes a database with the config files in db
.
import assert from "assert"
import { describe, it } from "node:test"
import { acc } from "wao/test"
import { DB } from "wdb-sdk"
import { init } from "./utils.js"
const actor1 = acc[1]
const actor2 = acc[2]
describe("Social Dapp", () => {
it("should post notes", async () => {
const { id, db, q: mem } = await init()
const a1 = new DB({ jwk: actor1.jwk, id, mem })
const a2 = new DB({ jwk: actor2.jwk, id, mem })
})
})
Now you have 3 clients, the DB owner (db
) and 2 users (a1
and a2
).
Create Notes with Schema
and Auth
What is truly magical about WeaveDB is that you only need JSON configuration files. No smart contracts required to build any complex applications. The DB itself is as powerful as any smart contract, thanks to FPJSON, code as data.
We are going to borrow as much vocabulary as possible from Activity Streams and Activity Vocabulary, which are web standard protocols for social apps.
Text-based posts are called notes, and users are called actors. Let's create a schema for notes
using JSON Schema.
export default {
notes: {
type: "object",
required: ["id", "actor", "content", "published"],
properties: {
id: { type: "string" },
actor: { type: "string", pattern: "^[a-zA-Z0-9_-]{43}quot; },
content: { type: "string", minLength: 1, maxLength: 140 },
published: { type: "integer" },
},
additionalProperties: false,
}
}
Now we can have a note like the following.
{
"id": "A",
"actor": "Tbun4iRRQW93gUiSAmTmZJ2PGI-_yYaXsX69ETgzSRE",
"content": "Hello, World!",
"published": 1757588601
}
id
is auto-incremented starting from A
, actor
is the signer
of the query, and published
is auto-asigned by the auth rules so users cannot set an arbitrary timestamp. The only thing users should specify is content
.
Create a custom query type called add:note
to achieve this.
export default {
notes: [
[
"add:note",
[
["fields()", ["*content"]],
["mod()", { id: "$doc", actor: "$signer", published: "$ts" }],
["allow()"],
],
],
],
}
fields()
can specify required fields from users, and *
makes content
mandatory.
mod()
modifies the uploaded data by adding values to id
, actor
, and published
.
Finally, allow()
gives you the access to write the transformed data to the database.
With these schema and rules, users can now add notes.
await a1.set("add:note", { content: "Hello, World!" }, "notes")
Update the test file.
import assert from "assert"
import { describe, it } from "node:test"
import { acc } from "wao/test"
import { DB } from "wdb-sdk"
import { init } from "./utils.js"
const actor1 = acc[1]
const actor2 = acc[2]
describe("Social Dapp", () => {
it("should post notes", async () => {
const { id, db, q: mem } = await init()
const a1 = new DB({ jwk: actor1.jwk, id, mem })
const a2 = new DB({ jwk: actor2.jwk, id, mem })
await a1.set("add:note", { content: "Hello, World!" }, "notes")
await a2.set("add:note", { content: "GM, World!" }, "notes")
console.log(await db.get("notes"))
})
})
Create Likes and Add Multi-Field Indexes
Now, let's add the good old like feature. Users can like notes, and notes will be sorted by like counts.
We will add likes
dir with actor
, object
, and published
. object
is the note id
actor
likes.
export default {
notes: {
type: "object",
required: ["id", "actor", "content", "published"],
properties: {
id: { type: "string" },
actor: { type: "string", pattern: "^[a-zA-Z0-9_-]{43}quot; },
content: { type: "string", minLength: 1, maxLength: 140 },
published: { type: "integer" },
},
additionalProperties: false,
},
likes: {
type: "object",
required: ["actor", "object", "published"],
properties: {
actor: { type: "string", pattern: "^[a-zA-Z0-9_-]{43}quot; },
object: { type: "string" },
published: { type: "integer" },
},
additionalProperties: false,
},
}
In the auth rules, we should check if the like already exists with the same actor
and the same object
.
Create a custom query called add:like
.
export default {
notes: [
[
"add:note",
[
["fields()", ["*content"]],
["mod()", { id: "$doc", actor: "$signer", published: "$ts" }],
["allow()"],
],
],
],
likes: [
[
"add:like",
[
["fields()", ["*object"]],
["mod()", { actor: "$signer", published: "$ts" }],
[
"=$likes",
[
"get()",
[
"likes",
["actor", "==", "$signer"],
["object", "==", "$req.object"],
],
],
],
["=$ok", ["o", ["equals", 0], ["length"], "$likes"]],
["denyifany()", ["!$ok"]],
["allow()"],
],
],
],
}
get()
queries where actor
is the $signer
and object
is $req.object
. This query requires a multi-field index to sort by actor
first, then by object
. So let's define the index.
export default {
likes: [[["actor"], ["object"]]],
}
Now users can like notes.
await a1.set("add:like", { object: noteID }, "likes")
Update the tests.
import assert from "assert"
import { describe, it } from "node:test"
import { acc } from "wao/test"
import { DB } from "wdb-sdk"
import { init } from "./utils.js"
const actor1 = acc[1]
const actor2 = acc[2]
describe("Social Dapp", () => {
it("should post notes", async () => {
const { id, db, q: mem } = await init()
const a1 = new DB({ jwk: actor1.jwk, id, mem })
const a2 = new DB({ jwk: actor2.jwk, id, mem })
await a1.set("add:note", { content: "Hello, World!" }, "notes")
await a2.set("add:note", { content: "GM, World!" }, "notes")
const notes = await db.get("notes", ["published", "desc"])
await a1.set("add:like", { object: notes[0].id }, "likes")
await a2.set("add:like", { object: notes[1].id }, "likes")
console.log(await db.get("likes"))
})
})
Count Likes with Triggers
Now, we can add likes
field to notes
to count up the likes.
export default {
notes: {
type: "object",
required: ["id", "actor", "content", "published", "likes"],
properties: {
id: { type: "string" },
actor: { type: "string", pattern: "^[a-zA-Z0-9_-]{43}quot; },
content: { type: "string", minLength: 1, maxLength: 140 },
published: { type: "integer" },
likes: { type: "integer" },
},
additionalProperties: false,
},
likes: {
type: "object",
required: ["actor", "object", "published"],
properties: {
actor: { type: "string", pattern: "^[a-zA-Z0-9_-]{43}quot; },
object: { type: "string" },
published: { type: "integer" },
},
additionalProperties: false,
},
}
Add likes=0
to notes when created.
export default {
notes: [
[
"add:note",
[
["fields()", ["*content"]],
["mod()", { id: "$doc", actor: "$signer", published: "$ts", likes: 0 }],
["allow()"],
],
],
],
likes: [
[
"add:like",
[
["fields()", ["*object"]],
["mod()", { actor: "$signer", published: "$ts" }],
[
"=$likes",
[
"get()",
[
"likes",
["actor", "==", "$signer"],
["object", "==", "$req.object"],
],
],
],
["=$ok", ["o", ["equals", 0], ["length"], "$likes"]],
["denyifany()", ["!$ok"]],
["allow()"],
],
],
],
}
But how do we increment likes
? It turned out that we can use triggers to execute data transformations on data changes.
export default {
likes: [
{
key: "inc_likes",
on: "create",
fn: [
["update()", [{ likes: { _$: ["inc"] } }, "notes", "$after.object"]],
],
},
],
}
This trigger will increment likes
of $after.object
in the notes
dir, when a new like
is created.
Update the test file, and see the likes
counts go up.
import assert from "assert"
import { describe, it } from "node:test"
import { acc } from "wao/test"
import { DB } from "wdb-sdk"
import { init } from "./utils.js"
const actor1 = acc[1]
const actor2 = acc[2]
describe("Social Dapp", () => {
it("should post notes", async () => {
const { id, db, q: mem } = await init()
const a1 = new DB({ jwk: actor1.jwk, id, mem })
const a2 = new DB({ jwk: actor2.jwk, id, mem })
await a1.set("add:note", { content: "Hello, World!" }, "notes")
await a2.set("add:note", { content: "GM, World!" }, "notes")
const notes = await db.get("notes", ["published", "desc"])
await a1.set("add:like", { object: notes[0].id }, "likes")
await a2.set("add:like", { object: notes[1].id }, "likes")
const notes2 = await db.get("notes", ["published", "desc"])
assert.equal(notes2[0].likes, 1)
assert.equal(notes2[1].likes, 1)
})
})
Test
Finally, run the tests.
yarn test-all
Running Rollup Node
A WeaveDB rollup node can automatically start with HyperBEAM.
Clone the weavedb
branch from our HyperBEAM repo.
git clone -b weavedb https://github.com/weavedb/HyperBEAM.git
cd HyperBEAM
Start HyperBEAM rebar3 shell
with as weavedb
.
rebar3 as weavedb shell --eval 'hb:start_mainnet(#{ port => 10001, priv_key_location => <<".wallet.json">> })'
You can explicitlyt start the WeaveDB rollup node by visiting http://localhost:10001/~weavedb@1.0/start.
Then check the rollup node status at http://localhost:6364/status.
Or simply run yarn start
, which handles everything above and some HyperBEAM memory leak issues (under investigation).
yarn start
Now you can interact with the nodes with wdb-sdk
.
- HyperBEAM : http://localhost:10001
- WeaveDB Rollup : http://localhost:6364
Deploy Database
Make sure you are running a local rollup node and a HyperBEAM node, then have .wallet.json
in the app root directory.
Let's deploy the DB.
yarn deploy --wallet .wallet.json
If you go to http://localhost:6364/status, you will see your newly deployed DB is listed under processes
. Save the database ID. You will need it later.
WeaveDB Scan
When running local servers, you can also run a local explorer to view transactions.
git clone https://github.com/weavedb/weavedb.git
cd weavedb/scan && yarn
yarn dev --port 4000
Now the explorer is runnint at localhost:4000.
We have a simple public explorer for the demo at scan.weavedb.dev.
Build Frontend Dapp
We are going to build the simplest social app ever using NextJS!
For simplicity, use the old pages
structure insted of apps
.
npx create-next-app myapp && cd myapp
yarn add wdb-sdk
import { useRef, useEffect, useState } from "react"
import { DB } from "wdb-sdk"
export default function Home() {
const [notes, setNotes] = useState([])
const [likes, setLikes] = useState([])
const [slot, setSlot] = useState(null)
const [body, setBody] = useState("")
const [likingNotes, setLikingNotes] = useState({})
const [showToast, setShowToast] = useState(false)
const db = useRef()
const getNotes = async () => {
const _notes = await db.current.cget("notes", ["published", "desc"], 10)
const ids = _notes.map(v => v.id)
const _likes = await db.current.get("likes", ["object", "in", ids])
setNotes(_notes)
setLikes(_likes.map(v => v.object))
}
const handleLike = async post => {
if (likingNotes[post.id]) return
setLikingNotes(prev => ({ ...prev, [post.id]: true }))
try {
if (window.arweaveWallet) {
await window.arweaveWallet.connect([
"ACCESS_ADDRESS",
"SIGN_TRANSACTION",
])
}
const res = await db.current.set(
"add:like",
{ object: post.data.id },
"likes",
)
const { success, result } = res
if (success) {
setSlot(result.result.i)
await getNotes()
} else {
alert("Failed to like post!")
}
} catch (error) {
console.error("Error liking post:", error)
alert("Something went wrong!")
} finally {
setLikingNotes(prev => ({ ...prev, [post.id]: false }))
}
}
const handlePost = async () => {
if (body.length <= 140 && body.trim().length > 0) {
try {
if (window.arweaveWallet) {
await window.arweaveWallet.connect([
"ACCESS_ADDRESS",
"SIGN_TRANSACTION",
])
}
const res = await db.current.set("add:note", { content: body }, "notes")
const { success, result } = res
if (success) {
setSlot(result.result.i)
setShowToast(true)
setBody("")
await getNotes()
// Auto-close removed - manual close only
} else {
alert("something went wrong!")
}
} catch (error) {
console.error("Error posting:", error)
alert("Something went wrong!")
}
}
}
const formatTime = date => {
const now = new Date()
const posted = new Date(date)
const diffInMinutes = Math.floor((now - posted) / (1000 * 60))
if (diffInMinutes < 1) return "now"
if (diffInMinutes < 60) return `${diffInMinutes}m`
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h`
return `${Math.floor(diffInMinutes / 1440)}d`
}
const truncateAddress = address => {
if (!address) return "Unknown"
if (address.length <= 16) return address
return `${address.slice(0, 8)}...${address.slice(-8)}`
}
useEffect(() => {
void (async () => {
db.current = new DB({
id: process.env.NEXT_PUBLIC_DB_ID,
url: process.env.NEXT_PUBLIC_RU_URL,
})
await getNotes()
})()
}, [])
return (
<>
<div className="app-container">
{/* Header */}
<header className="app-header">
<div className="logo">W</div>
<a
href={`${process.env.NEXT_PUBLIC_SCAN_URL}/db/${process.env.NEXT_PUBLIC_DB_ID}?url=${process.env.NEXT_PUBLIC_RU_URL}`}
target="_blank"
rel="noopener noreferrer"
className="scan-link"
>
Scan
</a>
</header>
{/* Composer */}
<div className="composer">
<div className="composer-avatar">
<div className="avatar">WDB</div>
</div>
<div className="composer-main">
<textarea
className="composer-input"
placeholder="What's happening?"
value={body}
onChange={e => {
if (e.target.value.length <= 140) {
setBody(e.target.value)
}
}}
maxLength={140}
/>
<div className="composer-footer">
<span
className={`char-count ${body.length > 120 ? "warning" : ""} ${body.length === 140 ? "danger" : ""}`}
>
{140 - body.length}
</span>
<button
className="post-btn"
disabled={body.trim().length === 0}
onClick={handlePost}
>
Post
</button>
</div>
</div>
</div>
{/* Feed */}
<div className="feed">
{notes.map(post => (
<article key={post.id} className="post">
<div className="post-avatar">
<div className="avatar">
{post.data.actor?.slice(0, 2).toUpperCase() || "??"}
</div>
</div>
<div className="post-main">
<div className="post-header">
<span className="post-author">{post.data.actor}</span>
<span className="post-time">
{formatTime(post.data.published)}
</span>
</div>
<div className="post-content">{post.data.content}</div>
<div className="post-actions">
<button
className={`like-btn ${likes.includes(post.id) ? "liked" : ""} ${likingNotes[post.id] ? "loading" : ""}`}
onClick={() => handleLike(post)}
disabled={likingNotes[post.id]}
>
<svg viewBox="0 0 24 24" className="heart">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
<span>{post.data.likes || ""}</span>
</button>
</div>
</div>
</article>
))}
</div>
{/* Footer */}
<footer className="app-footer">
<div className="footer-content">
Built by{" "}
<a
href="https://weavedb.dev"
target="_blank"
rel="noopener noreferrer"
className="footer-brand"
>
WeaveDB
</a>
</div>
</footer>
</div>
{/* Toast */}
{showToast && (
<div className="toast-overlay">
<div className="toast-card">
<div className="toast-icon">✓</div>
<div className="toast-content">
<div className="toast-title">Post successful!</div>
<a
href={`${process.env.NEXT_PUBLIC_SCAN_URL}/db/${process.env.NEXT_PUBLIC_DB_ID}/tx/${slot}?url=${process.env.NEXT_PUBLIC_RU_URL}`}
target="_blank"
rel="noopener noreferrer"
className="toast-link"
>
View transaction
</a>
</div>
<button className="toast-close" onClick={() => setShowToast(false)}>
×
</button>
</div>
</div>
)}
</>
)
}
Add .env.local
with your DB_ID
.
NEXT_PUBLIC_DB_ID="c5ulwr94nkxiqkpek9skxzjmmvmfgwluod_btvvqwas"
NEXT_PUBLIC_RU_URL="http://localhost:6364"
NEXT_PUBLIC_SCAN_URL="http://localhost:4000"
Run the app.
yarn dev
Now the app is runnint at localhost:3000.
Demo
A working version is running at social-teal-zeta.vercel.app.