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.
Data model
Section titled “Data model”_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)1. Create the collections
Section titled “1. Create the collections”In Studio → Collections, create profiles with these access rules:
| Operation | Rule |
|---|---|
read | public |
create | authenticated |
update | owner |
delete | disabled |
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).
2. Create a profile on register
Section titled “2. Create a profile on register”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: "",});3. Let the user update their profile
Section titled “3. Let the user update their profile”Upload avatar and save profile
Section titled “Upload avatar and save profile”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, });}React profile editor
Section titled “React profile editor”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> );}4. Fetch posts with author expanded
Section titled “4. Fetch posts with author expanded”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 shapeconst 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)[] ✓5. Display author info
Section titled “5. Display author info”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> );}Private vs public data
Section titled “Private vs public data”metadata on _users | profiles collection | |
|---|---|---|
| Who can read | Owner + admins only | Anyone (read: public) |
| Who can write | Owner (via PATCH /auth/me) | Owner (via collection update) |
| Use for | App preferences, internal flags, sensitive profile fields | Display name, bio, avatar — anything shown publicly |
| Queryable | No | Yes — filter, sort, expand |
Use metadata for things like notification preferences, onboarding state, or private settings. Use profiles for anything rendered to other users.