# User Profiles

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 Type     | Description               | What we will store                     |
|----------------|---------------------------|-----------------------------------------|
| [**Attributes**](https://docs.arkiv.network/start-here/fundamentals/#entities) | queryable filters         | user's handle and wallet address        |
| [**Payload**](https://docs.arkiv.network/start-here/fundamentals/#entities)    | free-form fields          | user's display name and bio    |

## Prerequisites

- [`@arkiv-network/sdk`](https://www.npmjs.com/package/@arkiv-network/sdk) installed
- [`viem`](https://www.npmjs.com/package/viem) installed
- a wallet funded with testnet tokens from the [faucet](https://docs.arkiv.network/networks/tiramisu/)
- optional: an [access key](https://docs.arkiv.network/start-here/access-keys/) for the Tiramisu RPC endpoint, to raise the rate limit

```bash
npm install @arkiv-network/sdk viem
```

## Create a user profile

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.

   ```ts title="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}`),
   })
   ```
**Caution:** Always ensure to store sensitive information - such as the private key in this example - in environment variables.
**Caution:** Attribute names must be lowercase. Tiramisu rejects an uppercase letter in a name, so use `project_id`, not `projectId`.
**Tip:** Anonymous access to the RPC endpoint is rate limited. Register a project on the [access keys page](https://docs.arkiv.network/start-here/access-keys/) and pass the key in the RPC URL, as a `X-API-KEY` header, or as a bearer token.

2. **Define the profile shape**

   ```ts title="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**

   ```ts title="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`](https://docs.arkiv.network/typescript-sdk/api-reference/main/functions/createwalletclient/#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
**Note:** `updated` is wrapped in `u64(...)` from `@arkiv-network/sdk/attr`. A bare number defaults to an `i32`, and a millisecond timestamp is far too large for that range, so it would throw `InvalidValueError`. `u64` is the width the protocol counts in, and it stays comparable with `gt()` and `lt()`.
**Caution:** Every entity needs an `expires`. It is a required parameter, and there is no default. Start short and call `extendEntity` when you need more time, because over-allocating wastes storage fees.

## Query examples

Below are some examples of how to query user profiles and use Arkiv filtering capabilities.
**Tip:** See more advanced examples for the [`select()`](https://docs.arkiv.network/typescript-sdk/api-reference/main/type-aliases/publicarkivactions/#select) function to see how to retrieve only specific fields from the entity.

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.

### Look up a profile by handle or address

```ts title="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.
**Caution:** `toJson()` returns `any`, so the cast above buys you editor types but checks nothing at runtime. Anyone can write an entity carrying your project attribute, so for a payload you did not write yourself, parse it with a schema library such as zod or valibot instead of casting.
**Tip:** You can also specify what you want to retrieve from the `select()` function. Every field is opt-in, including `key`, and the network only sends what you name. For instance, to retrieve only the attributes:

```ts
select({ attributes: true })
```

or only the keys:

```ts
select({ key: true })
```

### 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.

```ts title="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)
```

### Get all profile handles

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.

```ts title="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)
```

### Get all profiles for this app

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

```ts title="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)
```
**Caution:** Handle uniqueness is your app's job. Arkiv does not enforce it. Check with `getProfile()` before creating, and treat the query as advisory since another writer can create the same handle in between.
**Tip:** If a trusted backend writes every profile, add `.createdBy(yourBackendAddress)` to these queries. `$creator` is set at creation and never changes, so nobody can write an entity that fakes it and slip a fake profile into your results.

## Update a profile

Use the [`patchEntity`](https://docs.arkiv.network/typescript-sdk/api-reference/main/functions/createwalletclient/#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.

```ts title="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 },
})
```

## Next steps

- Call [`extendEntity`](https://docs.arkiv.network/typescript-sdk/api-reference/main/functions/createwalletclient/#extendentity) on a schedule to keep active profiles alive past their expiry.
- Validate payloads with a schema library before trusting them. See [Best Practices](https://docs.arkiv.network/typescript-sdk/best-practices/).
- Link other entities to a profile with a shared `handle` attribute, Arkiv's version of a foreign key.