Skip to content

In this guide, you will learn how to create an Arkiv entity to store a user’s profile information.

You will learn what to store in the entity’s attributes vs in the entity’s payload.

Field TypeDescriptionWhat we will store
Attributesqueryable filtersuser’s handle and wallet address
Payloadfree-form fieldsuser’s display name and bio
Terminal window
npm install @arkiv-network/sdk viem
  1. Set up the clients

    We will first setup two clients:

    • one publicClient for read-only queries
    • one walletClient for write operations

    The project attribute below scopes every entity you will create to your app. Since Arkiv is a shared public database, this is to prevent queries from returning other projects’ profiles, if these projects use the same entity structure.

    arkiv.ts
    import { createPublicClient, createWalletClient } from "@arkiv-network/sdk"
    import { tiramisu } from "@arkiv-network/sdk/chains"
    import { http } from "viem"
    import { privateKeyToAccount } from "viem/accounts"
    /** Attribute name used to filter this project's entities. */
    export const PROJECT_ATTRIBUTE_NAME = "project_id" as const
    /** Replace with a globally unique string for your own app. */
    export const PROJECT_ATTRIBUTE_VALUE = "your-app-project-id" as const
    const rpcUrl = `${tiramisu.rpcUrls.default.http[0]}/${process.env.ARKIV_API_KEY}`
    export const publicClient = createPublicClient({
    chain: tiramisu,
    transport: http(rpcUrl),
    })
    export const walletClient = createWalletClient({
    chain: tiramisu,
    transport: http(rpcUrl),
    account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
    })
  2. Define the profile shape

    profile.ts
    export type Profile = {
    handle: string // attribute: unique, used for lookups
    wallet: string // attribute: used for lookups
    displayName: string // payload
    bio: string // payload
    }
  3. Create a profile

    create-profile.ts
    import { ExpirationTime, jsonToPayload } from "@arkiv-network/sdk"
    import { u64 } from "@arkiv-network/sdk/attr"
    import {
    PROJECT_ATTRIBUTE_NAME,
    PROJECT_ATTRIBUTE_VALUE,
    walletClient,
    } from "./arkiv"
    import type { Profile } from "./profile"
    export async function createProfile(profile: Profile) {
    const { entityKey } = await walletClient.createEntity({
    contentType: "application/json",
    attributes: {
    [PROJECT_ATTRIBUTE_NAME]: PROJECT_ATTRIBUTE_VALUE,
    entity_type: "profile",
    handle: profile.handle,
    wallet: profile.wallet,
    updated: u64(Date.now()),
    },
    payload: jsonToPayload({
    displayName: profile.displayName,
    bio: profile.bio,
    }),
    expires: ExpirationTime.fromDays(30),
    })
    return entityKey
    }
    const profileEntityKey = await createProfile({
    handle: "ada",
    wallet: "0x1111111111111111111111111111111111111111",
    displayName: "Ada Lovelace",
    bio: "Writes notes on engines.",
    })
    console.log("User profile created successfully! Profile entity key:", profileEntityKey)

    In the code example above, we are saving a new profile info to Arkiv using the createEntity function from the Arkiv SDK

    When creating this entity for a user profile, we are storing the user profile and app information in different places within the entity.

    We are storing in the attributes:

    • the user’s handle: so we can look up this specific profile by its handle
    • the user’s wallet address: so we can look up this specific profile by its wallet address
    • the project id: so we can query all profiles for this specific app

    We are storing in the payload the remaining generic user information:

    • the user’s display name
    • the user’s bio

Below are some examples of how to query user profiles and use Arkiv filtering capabilities.

Every query below filters on both the project attribute and entity_type. The project attribute alone is not enough, because your app writes other kinds of entities under the same project.

search-profile.ts
import { eq } from "@arkiv-network/sdk/query"
import {
PROJECT_ATTRIBUTE_NAME,
PROJECT_ATTRIBUTE_VALUE,
publicClient,
} from "./arkiv"
import type { Profile } from "./profile"
export async function getProfile(handle: string) {
const { entities } = await publicClient
.select({ key: true, payload: true, attributes: true })
.where(
eq(PROJECT_ATTRIBUTE_NAME, PROJECT_ATTRIBUTE_VALUE),
eq("entity_type", "profile"),
eq("handle", handle),
)
.limit(1)
.fetch()
const entity = entities[0]
if (!entity) return null
const { displayName, bio } = entity.toJson() as Pick<Profile, "displayName" | "bio">
return {
key: entity.key,
handle,
wallet: String(entity.attributes.wallet?.value),
displayName,
bio,
}
}

Swap eq("handle", handle) for eq("wallet", address) to look the same profile up by wallet address instead.

Search profiles starting with a specific handle prefix

Section titled “Search profiles starting with a specific handle prefix”

String attributes support prefix matching. You can also use limit(number) to limit the number of results you want to get returned.

search-profiles-by-prefix.ts
import { eq, startsWith } from "@arkiv-network/sdk/query"
import {
PROJECT_ATTRIBUTE_NAME,
PROJECT_ATTRIBUTE_VALUE,
publicClient,
} from "./arkiv"
const { entities: results } = await publicClient
.select({ key: true, payload: true, attributes: true })
.where(
eq(PROJECT_ATTRIBUTE_NAME, PROJECT_ATTRIBUTE_VALUE),
eq("entity_type", "profile"),
startsWith("handle", "ad"),
)
.limit(20)
.fetch()
console.log("Entity search results:", results)

Ask for a single attribute instead of the whole entity. attributes accepts a map of names, so the network never sends the payloads you are not going to read.

get-all-profile-handles.ts
import { eq } from "@arkiv-network/sdk/query"
import {
PROJECT_ATTRIBUTE_NAME,
PROJECT_ATTRIBUTE_VALUE,
publicClient,
} from "./arkiv"
const { entities } = await publicClient
.select({ attributes: { handle: true } })
.where(
eq(PROJECT_ATTRIBUTE_NAME, PROJECT_ATTRIBUTE_VALUE),
eq("entity_type", "profile"),
)
.limit(100)
.fetch()
const handles = entities.map((entity) => String(entity.attributes.handle?.value))
console.log("All profile handles:", handles)

A page holds at most 200 results, so follow the cursor when you want the whole set.

get-all-profiles.ts
import { eq } from "@arkiv-network/sdk/query"
import {
PROJECT_ATTRIBUTE_NAME,
PROJECT_ATTRIBUTE_VALUE,
publicClient,
} from "./arkiv"
let page = await publicClient
.select({ key: true, payload: true, attributes: true })
.where(
eq(PROJECT_ATTRIBUTE_NAME, PROJECT_ATTRIBUTE_VALUE),
eq("entity_type", "profile"),
)
.limit(100)
.fetch()
const allProfiles = [...page.entities]
while (page.hasNextPage()) {
page = await page.next()
allProfiles.push(...page.entities)
}
console.log("All profile entities result:", allProfiles.length)

Use the patchEntity function from the Arkiv SDK to update an entity.

patchEntity treats attributes and the payload differently:

  • Attributes are merged. Name only the ones you change in set. The rest keep their current value, so you do not need to read them first.
  • The payload is replaced whole. If you pass a payload, it overwrites the old one. Leave it out to keep the current payload untouched.

That is why the example below reads the profile first. It needs the current displayName to write it back next to the new bio.

update-profile.ts
import { jsonToPayload } from "@arkiv-network/sdk"
import { u64 } from "@arkiv-network/sdk/attr"
import { walletClient } from "./arkiv"
import { getProfile } from "./search-profile"
const profile = await getProfile("ada")
if (!profile) throw new Error(`No entity found for profile handle "ada"`)
// Update Ada's wallet address
const newWalletAddress = "0x1010101010101010101010101010101010101010"
// Update the payload but keep the current displayed name
const { displayName } = profile
const updatedBio = "Born in 1815, Ada Lovelace is considered to be the first computer programmer."
await walletClient.patchEntity({
entityKey: profile.key,
payload: jsonToPayload({ displayName, bio: updatedBio }),
contentType: "application/json",
set: { updated: u64(Date.now()), wallet: newWalletAddress },
})
  • Call extendEntity on a schedule to keep active profiles alive past their expiry.
  • Validate payloads with a schema library before trusting them. See Best Practices.
  • Link other entities to a profile with a shared handle attribute, Arkiv’s version of a foreign key.