Skip to content
BunBase BunBase BunBase Docs Alpha v0.1.0

Public Author Profiles

metadata on _users is private — only the owner and admins can read it. For public-facing data like author name, avatar, and bio you need a separate profiles collection.

_users (private — auth only)
└─ metadata (readable by the owner only)
profiles (public collection — anyone can read)
├─ user_id (links to the authenticated user's id)
├─ display_name
├─ bio
└─ avatar_url
posts
├─ title
├─ body
└─ author_id (links to profiles._id)

In Studio → Collections, create profiles with these access rules:

OperationRule
readpublic
createauthenticated
updateowner
deletedisabled

This lets anyone read author info, but only the profile owner can edit it.

For posts, use whatever access rules fit your app (e.g. read: public, create: authenticated, update: owner, delete: owner).


Create an empty profile record immediately after registration so the relation always exists:

const session = await client.auth.register({ email, password });
await client.collection("profiles").create({
user_id: session.user.id,
display_name: "",
bio: "",
avatar_url: "",
});

async function saveProfile(imageFile: File | null, displayName: string, bio: string) {
let avatarUrl = currentProfile.avatar_url;
if (imageFile) {
const record = await client.storage.upload(imageFile, {
bucket: "avatars",
isPublic: true,
});
avatarUrl = client.storage.downloadUrl(record.id, record.filename ?? undefined);
}
await client.collection("profiles").update(currentProfile._id, {
display_name: displayName,
bio,
avatar_url: avatarUrl,
});
}
import { useBunBase, useList, useUpdate, useUpload } from "@bunbase/react-sdk";
function ProfileEditor({ userId }: { userId: string }) {
const { client } = useBunBase();
const { data: profiles } = useList("profiles", {
filter: { user_id: userId },
limit: 1,
});
const profile = profiles?.items[0];
const { mutate: updateProfile, loading } = useUpdate("profiles");
const { upload, loading: uploading, progress } = useUpload();
async function handleAvatarChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file || !profile) return;
const record = await upload(file, { bucket: "avatars", isPublic: true });
await updateProfile(profile._id, {
avatar_url: client.storage.downloadUrl(record.id, record.filename ?? undefined),
});
}
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!profile) return;
const form = new FormData(e.currentTarget);
await updateProfile(profile._id, {
display_name: form.get("display_name") as string,
bio: form.get("bio") as string,
});
}
if (!profile) return null;
return (
<div>
<div>
{profile.avatar_url && (
<img src={profile.avatar_url as string} alt="avatar" width={80} height={80} />
)}
<label>
<input type="file" accept="image/*" hidden onChange={handleAvatarChange} />
<span>{uploading ? `${Math.round((progress ?? 0) * 100)}%` : "Change photo"}</span>
</label>
</div>
<form onSubmit={handleSubmit}>
<input name="display_name" defaultValue={profile.display_name as string} placeholder="Display name" />
<textarea name="bio" defaultValue={profile.bio as string} placeholder="Bio" rows={3} />
<button disabled={loading}>Save</button>
</form>
</div>
);
}

Use expand to join the author’s profile in a single request. Pass the expand shape as a type parameter to get full type safety:

import type { WithExpand } from "@bunbase/js";
type Post = { title: string; body: string; author_id: string };
type Profile = { display_name: string; bio: string; avatar_url: string };
// CollectionClient — second type param = expand shape
const posts = await client.collection<Post>("posts").list<{ author_id: Profile }>({
expand: ["author_id"],
sort: "-_created_at",
});
posts.items[0].expand?.author_id // Profile & BunBaseRecord ✓

Or define the type upfront with WithExpand:

type PostWithAuthor = WithExpand<Post, { author_id: Profile }>;
const posts = await client.collection<Post>("posts").list<{ author_id: Profile }>({
expand: ["author_id"],
});
// posts.items is PostWithAuthor[] ✓

For “many” relations, use an array in the expand shape:

type PostWithComments = WithExpand<Post, { comments: Comment[] }>;
// post.expand?.comments is (Comment & BunBaseRecord)[] ✓

import { useList, type WithExpand } from "@bunbase/react-sdk";
type Post = { title: string; body: string; author_id: string };
type Profile = { display_name: string; bio: string; avatar_url: string };
function PostCard({ post }: { post: WithExpand<Post, { author_id: Profile }> }) {
const author = post.expand?.author_id; // Profile & BunBaseRecord | undefined ✓
return (
<article>
<h2>{post.title}</h2>
<p>{post.body}</p>
{author && (
<div className="author">
{author.avatar_url && (
<img
src={author.avatar_url}
alt={author.display_name}
width={40}
height={40}
style={{ borderRadius: "50%" }}
/>
)}
<div>
<strong>{author.display_name}</strong>
{author.bio && <p>{author.bio}</p>}
</div>
</div>
)}
</article>
);
}
function BlogPage() {
// Second type param = expand shape → data is fully typed
const { data, loading } = useList<Post, { author_id: Profile }>("posts", {
expand: ["author_id"],
sort: "-_created_at",
});
if (loading) return <p>Loading…</p>;
return (
<div>
{data?.items.map((post) => (
<PostCard key={post._id} post={post} />
))}
</div>
);
}

metadata on _usersprofiles collection
Who can readOwner + admins onlyAnyone (read: public)
Who can writeOwner (via PATCH /auth/me)Owner (via collection update)
Use forApp preferences, internal flags, sensitive profile fieldsDisplay name, bio, avatar — anything shown publicly
QueryableNoYes — filter, sort, expand

Use metadata for things like notification preferences, onboarding state, or private settings. Use profiles for anything rendered to other users.