/iblai-vibe-user-metadata
First time here? If
iblai.envhas noARCHITECTURE=, run/iblai-vibe-startfirst (four questions; two minutes) — it decides single-org / multi-org / headless and who signs in, and every skill reads the answer.
Every ibl.ai user carries one schemaless JSON object per organization. It is the place for everything your app needs to remember about a person that is not their profile identity: preferences, flags, onboarding progress, app state. No database, no localStorage — it follows the user to every device and every build of the app.
Common setup (brand, conventions, env files, verification): see docs/skill-setup.md.
What you get
useUserSettings<T>()— typed, namespaced read/merge/remove over the SDK'suseGetUserPlatformMetadataQuery/useUpdateUserPlatformMetadataMutation(PATCH — untouched keys survive).AppPreferences— a card with two example controls for/profile.app/api/admin/user-metadata/route.ts— the admin path: read/patch another user's metadata with the org's authority (the hook cannot).- Unit tests for the namespace rules.
vibe-starter ships all of this already; on an existing app, install per Step 2.
When to use / when not
| Use it for | Not for |
|---|---|
| theme, language, layout choices | name, bio, avatar, social links → /iblai-vibe-profile (useGetUserMetadataQuery / useUpdateUserMetadataEdxMutation) |
| onboarding step, "seen the tour", dismissed banners | what an agent remembers about the user → /iblai-vibe-memory-guide |
| per-user feature flags, beta access | org-wide config → /iblai-vibe-org-metadata |
| lightweight app state (last-opened item, favorites) | secrets, tokens, payment data — never |
Prerequisites
- Auth in place (
/iblai-vibe-auth, or vibe-starter). - The org key in
.env.local(NEXT_PUBLIC_MAIN_TENANT_KEY); the admin route also needsIBLAI_API_KEY(server-only).
Step 1: Understand the store
- One object per user × org at
…/dm/api/core/users/platform-metadata/?platform_key={org}. Auto-created on first use ({}); keys and values are arbitrary JSON; the platform enforces no schema. - Verbs:
PATCHmerges (metadata+delete_keys);PUTreplaces;DELETEresets to{}. The SDK hook does PATCH only — PUT/DELETE/delete_keysneed a direct call (the admin route shows how). - Cross-user:
&username=<other>— org admins only (403otherwise,404for an unknown user). Confirm with the user before writing to someone else's record. - Namespace: this skill writes under
metadata.apps.<slug>(slug fromNEXT_PUBLIC_APP_NAME, elsevibe-starter) so other apps on the same org keep their own keys. The same conventionvibe-agentuses.
Step 2: Install
Render the assets (strip .j2; no variables):
| Asset | Destination |
|---|---|
assets/metadata-core.ts.j2 |
lib/iblai/metadata-core.ts (pure helpers, no SDK) |
assets/metadata.ts.j2 |
lib/iblai/metadata.ts (the hooks; also exports useOrgSettings) |
assets/app-preferences.tsx.j2 |
components/settings/app-preferences.tsx |
assets/admin-user-metadata-route.ts.j2 |
app/api/admin/user-metadata/route.ts |
assets/metadata.test.ts.j2 |
__tests__/metadata.test.ts |
metadata.ts imports resolveAppTenant from lib/iblai/tenant.ts and the
route imports lib/iblai/platform.ts (from /iblai-vibe-api) — install that
helper first on an app that lacks it. Needs shadcn card and switch:
pnpm dlx shadcn@latest add card switch (fix import { cn } from "cn" to
@/lib/utils if the generator writes it).
Step 3: Use it
"use client";
import { useUserSettings } from "@/lib/iblai/metadata";
type MySettings = { favoriteTopic?: string; onboardingDone?: boolean };
export function FavoriteTopic() {
const { settings, update, isLoading } = useUserSettings<MySettings>({ favoriteTopic: "", onboardingDone: false });
if (isLoading) return null;
return (
<select
value={settings.favoriteTopic}
=> void update({ favoriteTopic: e.target.value })}
>
<option value="">Pick a topic</option>
<option value="billing">Billing</option>
<option value="shipping">Shipping</option>
</select>
);
}
update merges; remove(["favoriteTopic"]) deletes; reads fall back to your
defaults on error so the UI never breaks. Keep keys flat and named
consistently; batch related changes into one update.
Mount <AppPreferences /> on /profile (vibe-starter does) for the two example toggles.
Step 4: Admin — another user's data
The browser sends its session token; the route verifies the caller is an
org admin, then acts with IBLAI_API_KEY:
import { adminFetch } from "@/lib/iblai/admin-client";
await adminFetch("/api/admin/user-metadata", {
method: "PATCH",
json: { username: "jane", metadata: { role_label: "mentor" } },
});
const theirs = await adminFetch("/api/admin/user-metadata?username=jane");
Wire a UI for it only where it belongs (an admin page gated by /iblai-vibe-admin).
Verify
pnpm typecheck && pnpm test—__tests__/metadata.test.tsproves the namespace never clobbers other keys.pnpm dev, sign in, flip a toggle on/profile, reload — it sticks. Open the same account in another browser — it is there too.- Non-admin calls to
/api/admin/user-metadataget403. curl -s "https://api.$DOMAIN/dm/api/core/users/platform-metadata/?platform_key=$PLATFORM" -H "Authorization: Api-Token $TOKEN"showsapps.<slug>and nothing else changed.
Platform data
| Hook / call | Purpose |
|---|---|
useGetUserPlatformMetadataQuery({ tenantKey }) |
the signed-in user's object |
useUpdateUserPlatformMetadataMutation() → ({ tenantKey, metadata }) |
PATCH merge |
PUT / DELETE / delete_keys (REST only) |
replace / reset / drop keys |
&username= (REST, org admin) |
another user |
REST reference: iblai-api-profile-metadata
(concepts, best practices, and a migration strategy in its references/guide.md).
Related skills
/iblai-vibe-org-metadata— the org-wide twin (public read, PUT replaces)/iblai-vibe-profile— identity fields/iblai-vibe-onboard— an onboarding flow that would writeonboardingStep/iblai-vibe-api— the server-route pattern this skill's admin route follows