/iblai-vibe-house-journeys
Mirrored from
iblai/vibe·skills/users/iblai-vibe-house-journeys/SKILL.md. This page is generated — edit it in the repository, not here.
Only for ibl.ai official apps. Run this skill only when the user explicitly says the app
is an ibl.ai official app ("make an ibl.ai official app", "this is an official ibl.ai app").
Any other app — a customer's, a demo, an unspecified one — keeps vibe-starter's default first
run (/setup from the home page's empty state) and never gets these journeys. When in doubt,
ask; never assume.
What an official app does on first run, the way the OS does it, for three kinds of signed-in user — plus the organization's own form for its members:
- A user whose only organization is the community one (
main). The app runs on it, on the app's own read-only sample: every read answers from the sample, every change opens the upgrade dialog, and a notice above the page says so. The sidebar shows the admin cluster trial-gated: any click opens the SDK's upgrade dialog ("Subscribe to unlock full features" · Upgrade for free), a $0 Stripe checkout that gives them an organization of their own and returns them into this app as its admin. The top bar carries the credit badge ("Free" · Upgrade Plan). - A visitor on an organization they do not belong to (a link into
/platform/<org>). A preview instead of the SDK's silent join attempt: the organization's logo and name, Join (the auth page's join: it joins them and returns, or says the organization does not allow it — only its admins can read that flag ahead of time), Create your own organization (the same $0 checkout), Back to my organization. The app runs on their own organization underneath. - The admin of their own organization. Their first visit opens the SDK
OnboardingWizardsetup flow — organization → sector → invite your team → done — once per session until it is completed. Then the Stripe path: the credit badge (plan, Upgrade, Add Credits, Manage Billing; the SDK runs every checkout) and the Billing tab of the account dialog, where a 402 lands. - A member. The form the admin authored in Management › Onboarding, with its agent on the
last step, at
/platform/<org>/onboardinguntil they answer; nothing when none is set up.
Common setup (brand, conventions, env files, verification): see docs/skill-setup.md.
What you get
Real files under assets/, self-contained on purpose: they import only the SDK, React, Next,
next-intl, @/lib/iblai/config and @/components/loading-screen, so they drop into any
house-shell app unchanged.
| File | Role |
|---|---|
lib/iblai/house-journeys.ts | The pure rules: decideEntry (enter · community · preview), ownOrg, isTrialUser (the OS rule: on main, not its admin, nowhere else), createOrganizationUrl and loginReturnUrl (the $0 checkout told to come back here), onboardingFlow (the LMS's resolver), firstRunTarget |
__tests__/house-journeys.test.ts | Every branch of those rules |
components/house/organization-preview.tsx | The visitor's preview: logo and name from the public metadata, Join, Create your own organization, Back |
components/house/upgrade-modal.tsx | The SDK UpgradePackageModal, opened by setOpenPricingModal(true) — a trial-gated click, or a 402 with a pricing table; mounted once. Upgrade for free goes straight to the $0 checkout (checkoutUrl), which returns to config.appUrl() |
lib/iblai/sample-seam.ts | withSample(baseQuery, respond): on the community organization a read answers from the app's sample and anything else opens the upgrade dialog — nothing reaches the platform |
components/house/sample-notice.tsx | The line above every page on the sample, with Create your own organization |
components/house/credit-badge.tsx | The SDK CreditBalance under its own isPlatformNavbarCreditBalanceVisible rule, for the top bar |
components/house/first-run-gate.tsx | Sends an admin to the setup flow once per session until completed, a member to the form until answered; the onboarding route, the community organization and a failed read pass through |
components/house/onboarding-page.tsx | The SDK OnboardingWizard inside the shell, the LMS's placement: the member form by default, the setup flow for an admin's first run or ?flow=admin, the app's own last step |
e2e/journeys/house.journey.spec.ts | Checkpoints H1–H11 |
messages/house-journeys.{en,es,fr,zh}.json | The houseJourneys namespace, all four locales |
Already in the SDK and in every house-shell app: the sidebar footer's trial-gated cluster
(showTrialGatedAdminMenu), the account dialog's Billing tab and its Management › Onboarding
tab (the form editor; its copied link is <origin>/platform/<org>/onboarding/<agent-id> once
onboardingBasePath is the platform path, see Customize).
When to use / when not
| Use it for | Not for |
|---|---|
| An app the user explicitly calls an ibl.ai official app | Any other app — vibe-starter's /setup and empty states are the default; /iblai-vibe-ops-init |
The OS's experience for main users, a preview of another organization, the SDK setup flow | A consumer questionnaire funnel — /iblai-vibe-onboard |
| The platform-credits Stripe path (plan, credits, billing) | Charging your own users — /iblai-vibe-pricing, /iblai-vibe-monetization-app-paywall |
Prerequisites
- A multi-organization app on the house shell:
/platform/[tenantKey]routes, the SDKPlatformSidebarandPlatformAccountSheet, providers withrequestedTenant(the/iblai-vibe-auth"Going multi-org" moves), next-intl withmessages/{en,es,fr,zh}.json. config.authUrl(),config.dmUrl(),config.mainTenantKey(),config.appName(),config.iblPlatform()(the app'sapp=id on the auth page;"mentor"when absent) andconfig.appUrl()— where the $0 checkout returns:NEXT_PUBLIC_APP_URL, else the page's origin (vibe-starter's); an official app defaults it to its hosted origin (https://<app>.iblai.me).@iblai/iblai-js2.59.0 or later:UpgradePackageModal'scheckoutUrlarrived there.- The providers'
handleTenantSwitchhands the SDK's own tenant to the switch andonTenantMismatchgoes to/(/iblai-vibe-auth, Going multi-org, move 2). The $0 checkout leaves the user holdingmainand their new organization, and the SDK moves such a user to the new one; a handler that re-authenticates into anything else leaves the loading screen up. - The credit badge shows only on an organization with
show_paywall(an ibl.ai operator sets it) and, for the community organization, to a user in no other organization.
Step 1: Copy the assets
cp -a <skills-dir>/iblai-vibe-house-journeys/assets/lib <skills-dir>/iblai-vibe-house-journeys/assets/components <skills-dir>/iblai-vibe-house-journeys/assets/__tests__ .
cp <skills-dir>/iblai-vibe-house-journeys/assets/e2e/journeys/house.journey.spec.ts e2e/journeys/
mkdir -p 'app/platform/[tenantKey]/onboarding/[[...agentId]]'
echo 'export { OnboardingPage as default } from "@/components/house/onboarding-page";' > 'app/platform/[tenantKey]/onboarding/[[...agentId]]/page.tsx'
The route file is that one line: the page lives in components/house/ so it compiles in any
app and the route stays yours. If the installed skills carry no assets/ (some installers
strip them), the same bodies are in
references/app-files.md.
Step 2: Merge the messages
Add the houseJourneys object from assets/messages/house-journeys.<locale>.json to each
messages/<locale>.json — all four or none; typographic ’ as in the partials.
Step 3: The app edits
Eight edits, shown here for the SIS's file names; the CRM and the wallet differ only in paths.
references/app-edits.md has each in full.
The providers — a route organization the user does not belong to is previewed, never joined from a link (the SDK joins any
requestedTenantit does not find intenants). The membership list is read first: what the sign-in stored, else one fetch — a cold load right after sign-in has none yet:+ const storedTenants = useMemo(() => (mounted ? readTenants() : []), [mounted]); + const needFetch = mounted && !!routeTenant && storedTenants.length === 0; + const [fetchUserTenants] = useLazyGetUserTenantsQuery(); + const [fetched, setFetched] = useState<OrgEntry[] | null>(null); + useEffect(() => { + if (!needFetch || fetched) return; + fetchUserTenants().unwrap() + .then((list) => { /* keep it in localStorage; */ setFetched(list); }) + .catch(() => setFetched([])); + }, [needFetch, fetched, fetchUserTenants]); + const tenants = needFetch ? fetched : storedTenants; … - if (!isInitialized || !mounted) return LOADING; - const requestedTenant = routeTenant || storedTenant; + if (!isInitialized || !mounted || tenants === null) return LOADING; + const requestedTenant = decideEntry({ routeOrg: routeTenant, stored: storedTenant, tenants }).org;The landing organization — a user whose only organization is the community one lands on it instead of a notice: in
pickLandingTenant,?? (tenants.length ? main : null)where it returned?? null.The platform layout — the notice for
/platform/maingoes; the preview renders instead of the shell; the dialog is mounted once; the sample notice sits above a trial user's page; the gate wraps the page:- if (tenantKey === config.mainTenantKey()) return <NoSchoolNotice />; + const entry = decideEntry({ routeOrg: tenantKey, stored: readStoredTenantKey(), tenants }); + if (entry.kind === "preview" && entry.preview) { + return <OrganizationPreview org={entry.preview} app={config.iblPlatform()} />; + } … + <UpgradeModal org={tenantKey} app={config.iblPlatform()} /> … + {isTrialUser(tenantKey, tenants) ? <SampleNotice /> : null} - {children} + <FirstRunGate org={tenantKey} isAdmin={isAdmin}> + {children} + </FirstRunGate>The sidebar — the footer's trial-gated cluster, and every click on it upgrades:
+ const trial = isTrialUser(tenantKey, tenants); const onFooterAction = (id: PlatformSidebarFooterActionId) => { if (isMobile) setOpenMobile(false); + if (trial) { + dispatch(setOpenPricingModal(true)); + return; + } … isLiveAdmin, + showTrialGatedAdminMenu: trial,The top bar —
<CreditBadge org={tenantKey} isLiveAdmin={isAdmin && adminMode} className="hidden md:flex" />before the Admin switch.The ledger —
e2e/coverage.jsongains thehousejourney (H1–H8,sourceFiles: the onboarding page and the platform home),COVERAGE.mdits row.The e2e seed — the admin account's first visit now opens the setup flow, so the seed in
e2e/auth.setup.tswalks it once (organization name, a sector, Continue past the invite step, Complete) before it opens anything else; the second run finds it completed.The base query — every RTK Query slice of the app goes through the sample's seam (Step 4):
baseQuery: withSample(rawBaseQuery, sampleResponse).
A 402 from the SDK chat already lands admins in Billing through
navigateToAdminBilling (openTab("billing") on the account dialog); an app without a chat
adds 402 to initializeDataLayer's handler map the same way.
Step 4: The sample on the community organization
A trial user sees the app on sample data that never reaches the platform — the OS's experience, so a visitor learns the app before creating an organization. The seam is the app's base query, so no page knows:
- The data —
lib/<app>/sample/data.ts: one small, fixed, fictional dataset in the shapes your API returns (no real people; dates relative to today so it never ages). - The answers —
lib/<app>/sample/respond.ts:sampleResponse(method, url, body)returns the body your API would answer for each read the app makes, andundefinedfor anything else (a write, an unknown record, an endpoint the sample does not cover). - The seam — wrap the base query every slice uses:
baseQuery: withSample(rawBaseQuery, sampleResponse). On the community organization a read answers from the sample; anything else opens the upgrade dialog and fails withstatus: "SAMPLE"(show nothing for it: the dialog is the answer). - The notice —
<SampleNotice />above the page for a trial user (Step 3, edit 3).
Pages render the sample like any organization's data. The trial user is not an admin: decide per app whether its admin pages show read-only on the sample or stay hidden. Data the app reads through the SDK's own components (the profile, the notifications) stays the user's own.
Step 5: The project notes
Add to the app's AGENTS.md, under what the app is: "House journeys (/iblai-vibe-house-journeys):
a main-only user gets the trial-gated shell on read-only sample data and the upgrade dialog; a visitor on another
organization gets the preview (Join, Create your own); an admin's first run is the SDK setup
flow at /platform/<org>/onboarding?flow=admin, a member's the organization's form. Because
this is an ibl.ai official app."
Verify
Run /iblai-vibe-ops-test before telling the user the work is ready, and give every page,
control and state this skill added its own checkpoint — one Playwright test titled
<ID> · … in e2e/journeys/, listed in e2e/coverage.json; pnpm e2e:coverage must pass:
pnpm typecheck,pnpm lint,pnpm test(the rules' unit test and the ledger test).pnpm test:e2ewith the shipped journey: H1–H2 the community organization (the member session told it is in no other organization, in storage and by both membership lists: the shell, the cluster, the dialog), H9–H10 its sample (the notice opens the dialog; Upgrade for free is the $0 checkout returning toconfig.appUrl()), H3–H4 the preview of an unknown organization (Join's target is the auth page's join; Create's is the $0 checkout with this app nested inredirect_url), H5–H6 the admin's first run (the setup flow end to end with the metadata answered bypage.route; a completed one stays home), H7–H8 the member's (the form answered end to end; nothing configured says so), H11 the session a checkout returns to (still onmainwhile the platform lists their own: the SDK's switch goes to/login/completefor it). Add the app's own sample checkpoints: a sample record opens on each list, and a write opens the dialog while no non-GET request leaves the page. Organization-wide state is served bypage.route, never written — on the membership list too, where web-utils 2.18 reads an organization's metadata; a navigation the test only inspects is answered with a page, never a 204 (a click waits for a navigation to commit).- Touch test as
/iblai-vibe-ops-testsays, at desktop and phone width: the shell on/platform/mainwith amain-only account, the preview on/platform/<an organization you are not in>,/platform/<org>/onboarding?flow=adminas the admin,/onboardingas a member once a form exists (Management › Onboarding). Console clean.
Not proven by the journey, by design: the real join and the real $0 checkout (the platform's
side; the OS's own journeys cover them), and the badge on a real main-only session.
Customize
get_component_info("OnboardingWizard")
get_component_info("UpgradePackageModal")
get_component_info("CreditBalance")
| Prop / option | Where | Description |
|---|---|---|
app | OrganizationPreview, UpgradeModal | The app's app= id on the auth page (config.iblPlatform()); the login page nests config.appUrl() so the platform returns the user here |
NEXT_PUBLIC_APP_URL | config.appUrl() | Where every $0 checkout returns; set it on a deployment that serves another host (a preview) |
checkoutUrl | UpgradePackageModal (in upgrade-modal.tsx) | Where Upgrade for free goes instead of the platform's payment links: the direct $0 checkout |
respond, isSample | withSample | The app's sample answers, and who sees them (default: the trial user on the community organization) |
brandName | OnboardingWizard (in onboarding-page.tsx) | "Welcome to {brandName}" on the setup flow's first step — config.appName() |
sectors | OnboardingWizard | The sector catalogue of step 2; defaults to the SDK's eight |
renderFinalStep | OnboardingWizard | The last step is the app's: AllSetStep (setup flow) and MeetAgentStep (member form) in onboarding-page.tsx — replace them to hand over your own first task |
agentEmbed | OnboardingWizard | Omitted on purpose: the member flow's last step names the agent and continues into the app. An app with an agent host may pass { mentorUrl, authUrl, lmsUrl } and drop renderFinalStep to embed the agent |
onboardingBasePath | PlatformAccountSheet | `${origin}/platform/${org}` so Management › Onboarding's copied link opens this app's /onboarding/<agent-id> |
?flow=admin / ?flow=user | /platform/<org>/onboarding | Which flow an admin sees; a member always gets the form |
isLiveAdmin | CreditBadge | Admin AND Admin mode; the badge hides when an admin views as a member |
house_setup_sent | sessionStorage | The organization an admin was sent to setup for this session; clear it to be sent again |
Copy is in the houseJourneys namespace of messages/<locale>.json; the SDK's own strings
(the wizard's steps, the dialog, the badge) are the SDK's, in its four locales.
Platform data
| Hook / call | Purpose |
|---|---|
useTenantMetadata({ org }) (web-utils) | The organization's public metadata: its name for the preview, user_onboarding_form and user_onboarding_submission for the gate and the page |
useGetUserPlatformMetadataQuery({ tenantKey }) (data-layer) | The admin's own onboarding.completed for the organization — what the SDK setup flow writes |
GET …/dm/api/service/stripe/checkout/redirect/credits-free-plan/?redirect_url=&cancel_url=&email= | ibl.ai's $0 sign-up — the dialog's Upgrade for free and the preview's Create; the platform merges platform_key, email and the sign-in token into redirect_url and the login page hands back to the app |
GET <auth>/join?tenant=<org>&redirect-to= | The auth page's join: joins a signed-in user when the organization allows it, otherwise its own notice |
useStripeUpgrade (inside the SDK dialog and badge) | The pricing-page session on the community organization, then Stripe |
REST reference: feature, billing, org.
Related skills
/iblai-vibe-ops-init— every other app's first run (/setup, the empty states)/iblai-vibe-auth— "Going multi-org": the route organization and the switcher this skill assumes/iblai-vibe-credit,/iblai-vibe-billing— the badge and the Billing tab on their own/iblai-vibe-admin— the Management dialog, where the member form is authored/iblai-vibe-monetization-app-paywall— pay to enter on the organization's own Stripe account/iblai-vibe-onboard— a consumer questionnaire funnel, a different product