Skip to content

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.

/test/main.test.js
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.

/test/main.test.js
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.

/db/schema.js
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.

/db/auth.js
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.

/test/main.test.js
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.

/db/schema.js
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.

/db/auth.js
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.

/db/indexes.js
export default {
  likes: [[["actor"], ["object"]]],
}

Now users can like notes.

await a1.set("add:like", { object: noteID }, "likes")

Update the tests.

/test/main.test.js
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.

/db/schema.js
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.

/db/auth.js
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.

/db/triggers.js
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.

/test/main.test.js
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.

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
/pages/index.js
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.

/.env.local
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.