One creator, one AI agent, one paywall.
A single-platform app on ibl.ai: users sign in with ibl.ai SSO and chat with one agent, and access to the chat is sold on the platform's own Stripe account. Built from iblai/vibe's vibe-starter template on the @iblai/iblai-js SDK.
vibe-agent fronts one agent for one platform on ibl.ai. A creator (the platform admin) points it at an agent built on os.ibl.ai, decides whether access is free or paid, and members sign in with SSO to chat. Two kinds of users: platform admins (the creator and their staff) see Analytics in Admin mode and answer the one setup question; members chat on /. Membership is the entitlement: when the admin chose a fee, a signed-in user who is not a member yet pays on /paywall and becomes one, and a visitor without an account types their email on /paywall and pays on the same page. Everything is the SDK's — the shell, the chat, the analytics, the profile and account panels — connected to iblai.app.
| Screen | |
|---|---|
| Chat, with the canvas open | ![]() |
| Analytics · Users | ![]() |
| Feature | Description |
|---|---|
| Chat | / — the SDK Chat with the one agent (NEXT_PUBLIC_DEFAULT_AGENT_ID, the uuid at the end of its os.ibl.ai URL): streaming, sessions, files, voice. Paying users and platform admins. |
| Sidebar | The SDK PlatformSidebar — the shell the ibl.ai OS and LMS use — with this app's content: New chat, Recents (pinned chats first; each row can be pinned, unpinned or deleted) and, for admins in Admin mode, the Analytics menu. The bottom-left cluster is the SDK's: Notifications and Support for everyone, plus Invites, Management, Integrations, Monetization (when the platform sells credits) and Advanced for admins in Admin mode; the last four open the platform's account sheet in place. Collapses to an icon rail (Cmd/Ctrl+B), a drawer on phones. |
| Analytics | /analytics/* — the OS analytics section for this one agent: Overview, Users, Topics, Transcripts, Memory, Costs, Audit, Data Reports (SDK AnalyticsLayout + stats components). Platform admins, in Admin mode. |
| Paywall | /paywall — the public join page: a stranger types their email and pays on the platform's own Stripe account, one Stripe page and no sign-up; the account is made for the email, paying makes them a member, and they land in the app signed in (automatically where ibl.ai allows it, by email code otherwise). The platform owns payments; free access never needs a Stripe key. |
| Setup | /setup — one question: free access, one-time fee or monthly fee (USD); the Stripe product and price are created for you. Opens itself for a platform admin until answered; reachable later from the quiet "Payments setup" link on /account. |
| User / Admin mode | Platform admins get a User / Admin switch in the navbar (in the profile menu on narrow screens). User mode shows the app as a member sees it; Analytics and the admin cluster exist only in Admin mode. Starts on Admin, resets on reload. |
| Profile | /profile — the SDK Profile panel, from the profile dropdown. |
| Account | /account — the SDK Account panel (organization settings), admins. |
| Notifications | /notifications — the SDK notification centre, from the bell. |
| About | /about — off by default; NEXT_PUBLIC_SHOW_ABOUT=true enables it. The agent's public profile plus your copy (ABOUT_COPY in app/(app)/about/page.tsx). |
| Branding | The logo in the sidebar header (and in the navbar on phones) is the org logo set in the platform's org settings, falling back to the ibl.ai mark. |
| SSO | Login via iblai.app — no tokens to manage. Every origin the app runs on must be in the platform's allowed redirect origins. |
Membership is the entitlement: a signed-in member never sees a payment page. /paywall is the public join page for everyone else, outside app/(app)/ on purpose.
AGENTS.md (with CLAUDE.md as its symlink) is the guide for agents and people changing this app: the map, the invariants and why, the paywall and setup end to end, and the gotchas learned building it. Read it before touching the shell, the paywall or the SDK wrappers.
- Node.js 20 or newer (22 is what this app is built with) and pnpm
- Claude Code
Start Claude Code in the terminal (claude) and say:
get https://github.com/iblai/vibe-agent
It clones the repo and, as soon as it reads a file in it, follows the procedure in AGENTS.md in one run. It asks for your platform key (listed on login.iblai.app/me; never the shared main platform, that one is everyone's, not yours; no platform yet → ibl.ai/join), for a Platform API Token you create in the OS (os.ibl.ai → Integrations → APIs → Add API; paste it in the chat and it goes into .env.local and iblai.env at once, never repeated), and for your agent's URL from os.ibl.ai; suggests a name for the app; installs and starts the dev server and tells you to sign in — as the platform admin you land on /setup, the one setup question (free, one-time or monthly and the price; a paid answer asks for your restricted Stripe key on the next screen, saved to the platform from your browser); then publishes the app on ibl.ai hosting as <name>.vercel.app, asking only for the name. If it only clones, say follow AGENTS.md; or start inside the clone — git clone https://github.com/iblai/vibe-agent && cd vibe-agent && claude — and say run it.
By hand:
-
Platform credentials go in
iblai.env(gitignored):cp iblai.env.example iblai.env
PLATFORMis your platform key (listed on https://login.iblai.app/me);TOKENis a Platform API Token (os.ibl.ai → Integrations, in the sidebar's bottom cluster → APIs → Add API; the key is shown once). -
App env goes in
.env.local:cp .env.example .env.local
Fill
NEXT_PUBLIC_MAIN_TENANT_KEY(=PLATFORM),IBLAI_API_KEY(=TOKEN),NEXT_PUBLIC_DEFAULT_AGENT_ID(the agent's uuid, the last path segment ofhttps://os.ibl.ai/platform/<platform-key>/<agent-uuid>) andNEXT_PUBLIC_APP_NAME(what the join page and the browser tab call the app: two or three plain words in Title Case, e.g.Babatunde Tutor, never a slug);IBLAI_APP_BASE_URLcan stay empty (the app uses the origin it is reached on for its return URLs). The API, auth and websocket URLs default to hosted iblai.app inlib/iblai/config.ts. A missing or placeholder platform key shows an alert instead of an app, and so does anIBLAI_API_KEYthat is missing, a placeholder, rejected by the platform or another platform's (the app checks it against the platform before rendering anything); a missing agent shows one on/. -
Install and run:
pnpm install --ignore-scripts pnpm dev
Open http://localhost:3000 and sign in. As the platform admin you land on
/setup, the one setup question (see Paywall below). Every origin the app runs on (localhost and the deployed one) must be in the platform's allowed redirect origins, or sign-in never comes back.
pnpm build
pnpm startPaying the creator makes the buyer a member of the platform, and that membership is the entitlement: a signed-in member never sees a payment page. The platform (DM) owns every payment: it mints Stripe Checkout sessions on the platform's own Stripe key, records payments, and checks subscriptions live. No Stripe Connect, no commission, no webhooks, and no Stripe key in this app. The app has the server routes under app/api/paywall/, the join page (app/paywall/, public, headed "Join <NEXT_PUBLIC_APP_NAME>") and the setup screen (app/setup, components/setup/setup-screen.tsx).
Who gets in:
- Platform admins and invited members are members already: straight into the app.
- A visitor without an account types their email on the join page and pays: the app makes the ibl.ai account for that email first (the platform's ledger needs the username on the Stripe session), one Stripe page, no sign-up page. Back from Stripe they are linked as a member and signed in — automatically where ibl.ai has enabled token provisioning for the platform, otherwise through a sign-in code the platform emails them. An email that already has an ibl.ai account is told to sign in first.
- A signed-in user who is not a member lands on
/paywallwhen joining costs money: one plan, one button, the email locked to their account. Back from Stripe, the server verifies the session on the platform's account and links them with the platform's admin link API. On a free platform there is no page: setup opens self-join and the SDK joins them at sign-in. - A payer whose subscription lapsed is caught on their next visit: the platform's ledger says they paid, the live check says it no longer grants, the membership ends and they see
/paywallagain.
Setup is one question, asked of a platform admin the first time they open the app (and reachable later from the quiet "Payments setup" link on /account):
- Free access — anyone who signs in joins. No Stripe needed, ever.
- One-time fee or Monthly fee — enter the price (USD). The first time, a second screen ("Monetize Your Agent") asks for a restricted Stripe key (Stripe → Developers → API keys → Create restricted key: write on Products, Prices, Checkout Sessions, Customers; read on Subscriptions). It is saved as the platform's
stripeintegration credential on the platform, browser to platform; this app's server never sees it.
For a paid answer, Save creates the Stripe product (named after the platform, tagged metadata.app = PAYWALL_APP_SLUG) and the price, retires the previous price if the answer changed, closes self-join (payment is the only way in), and records the choice in the platform's metadata under apps.<PAYWALL_APP_SLUG>. Free opens self-join and records the choice, touching Stripe not at all; a price left behind by a paid → free switch stays active on Stripe but is never sold, since the app sells only the recorded one:
{
"apps": {
"vibe-agent": {
"access": "monthly",
"amount": 2900,
"currency": "usd",
"stripe": { "product_id": "prod_…", "price_id": "price_…" },
"updated_at": "…"
}
}
}That metadata is a public read on the platform (ids and amounts only, never a key), so the deployed app needs no extra credential to know what it sells. Runtime rule (lib/paywall.ts): PAYWALL_PRICE_IDS, if set, is what the app sells; otherwise the recorded choice; free or unanswered means everyone who signs in gets in. Test with card 4242 4242 4242 4242. A cancellation takes effect on the payer's next visit, after the platform's cache (about 75 s) and the app's 60 s session cache.
Headless alternative, with DOMAIN, PLATFORM, TOKEN and IBLAI_USERNAME from iblai.env:
PAY="https://api.$DOMAIN/dm/api/ai-mentor/orgs/$PLATFORM/users/$IBLAI_USERNAME/providers/stripe/payments"
AUTH="Authorization: Api-Token $TOKEN"
curl -s -H "$AUTH" "$PAY/products/?limit=1"
# 200 connected · 400 no `stripe` credential · 502 Stripe rejected the key · 404 backend too old
curl -s -H "$AUTH" -H 'Content-Type: application/json' -X POST "$PAY/products/" \
-d '{"name":"vibe-agent access","metadata":{"app":"vibe-agent"}}'
curl -s -H "$AUTH" -H 'Content-Type: application/json' -X POST "$PAY/prices/" \
-d '{"product":"prod_…","unit_amount":2900,"currency":"usd","recurring":{"interval":"month"}}'
# drop "recurring" for a one-time price
curl -s -H "$AUTH" "$PAY/paywall/payments/?app=vibe-agent" # who paid so farThen PAYWALL_PRICE_IDS=price_xxx,price_yyy in .env.local: the join page describes env-listed prices from Stripe itself.
/iblai-vibe-ops-deploy (from iblai/vibe) zips the source, uploads it to ibl.ai hosting with the platform token, polls until ready and returns the live URL. It regenerates .env.production from .env.local; confirm it carries IBLAI_API_KEY and PAYWALL_APP_SLUG (plus PAYWALL_PRICE_IDS if you use the override), or the paywall routes 500 in production. IBLAI_APP_BASE_URL is not needed there: the deployed app uses its own origin, and the allowlist drops the key anyway. Server mode is required: never set output: 'export'. Afterwards:
- add the deployed origin to the platform's allowed redirect origins;
- put it in
src-tauri/tauri.conf.json→build.frontendDist(see below).
src-tauri/ is a thin WebView shell: tauri dev loads http://localhost:3000 and release builds load build.frontendDist, the deployed origin. Nothing is bundled, so there is no static export and the paywall's server routes keep working. Until frontendDist holds a real URL it points at a .invalid host on purpose, so a forgotten value fails loudly instead of shipping a blank app.
Prerequisites: rustup, pnpm install --ignore-scripts, and nothing else on port 3000. Linux also needs libwebkit2gtk-4.1-dev build-essential libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev; Windows needs the Visual Studio Build Tools C++ workload and WebView2; macOS needs xcode-select --install.
pnpm exec tauri icon path/to/logo.png # every icon size from one square PNG (1024 px recommended)
pnpm exec tauri dev # desktop, this OS
pnpm exec tauri build # desktop release: .dmg/.app, .exe/.msi, .deb/.AppImageiOS (macOS with Xcode):
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
pnpm exec tauri ios init
pnpm exec tauri ios dev "iPhone 16 Pro Max" # device names: xcrun simctl list devices
pnpm exec tauri ios build # .ipa under src-tauri/gen/apple/build/Android (Android Studio with SDK and NDK):
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
pnpm exec tauri android init
pnpm exec tauri android dev "Pixel_9" # device names: adb devices
pnpm exec tauri android build # APK; add --aab for the Play StoreMobile SSO cannot return from an https:// page into a native app, so it comes back through the custom scheme: TAURI_CUSTOM_SCHEME=vibe-agent in iblai.env, mapped to NEXT_PUBLIC_TAURI_CUSTOM_SCHEME in the deployed env.
Signed desktop releases: copy desktop-signing.env.example to desktop-signing.env, then make -f desktop-release.mk macos-dmg (signed and notarized universal DMG) or make -f desktop-release.mk windows-nsis. .github/workflows/tauri-build-desktop.yml builds unsigned macOS, Windows and Linux artifacts on demand (Actions → Run workflow). The signed release workflows that trigger on app-v* tags are in /iblai-vibe-ops-build's assets/tauri/workflows/.
Stores: /iblai-vibe-ops-release generates a Makefile and Fastlane config (make ios-release, make android-release); /iblai-vibe-windows-msix packages an MSIX for the Microsoft Store. A binary locked to one platform: IBL_TENANT=<key> pnpm exec tauri build.
Releases are automated with release-it, the way the OS does it. Every push to main runs .github/workflows/release.yml: it reads the conventional commit subjects since the last tag, bumps the version in package.json (feat: minor, fix: patch, a ! or a BREAKING CHANGE footer major, anything else patch), prepends the entries to CHANGELOG.md, commits chore(release): vX.Y.Z, tags vX.Y.Z and publishes a GitHub Release with the same notes. The release commit is skipped by the workflow, so there is no loop. Only typed subjects reach the changelog, which is what the commitlint hook in .husky/commit-msg enforces. The first release, made while no v* tag exists, is 1.0.0.
Nothing to configure: the job releases with the built-in GITHUB_TOKEN, so the release commit, the tag and the GitHub Release show as github-actions[bot]. Pushes made with that token never trigger other workflows; a workflow that must run on the v* tag would need a personal access token instead, as the OS uses.
Never edit CHANGELOG.md or the package.json version by hand. pnpm release is for CI; locally use pnpm release --dry-run --git.pushRepo=<remote> to preview (release-it expects origin). The desktop shell is versioned on its own: src-tauri/tauri.conf.json is set by hand and built on demand, see above.
| Command | Does |
|---|---|
pnpm dev, pnpm build, pnpm start |
Next.js with Turbopack. next build type-checks with the project-local TypeScript 7 tsc |
pnpm typecheck |
oxlint --type-check (tsgo diagnostics) |
pnpm lint, pnpm check |
oxlint --type-aware; lint plus typecheck |
pnpm fmt, pnpm fmt:check |
oxfmt |
pnpm test |
vitest (config, source paths, platform resolution, paywall helpers, platform-metadata store, route handlers, chat-row labels) |
pnpm test:e2e |
Playwright against real SSO (credentials in e2e/.env.development) |
Run pnpm check, pnpm test and pnpm build before a pull request; pnpm test:e2e when you change a user journey.
TypeScript is installed twice on purpose: typescript is aliased to the TS 6 package (the JavaScript compiler API Next.js loads) and @typescript/native to TS 7, so tsc is the Go compiler and tsc6 the old one. That is the setup from vercel/next.js#95633; Next 16.3 runs the local tsc during next build by default (experimental.useTypeScriptCli, see node_modules/next/dist/docs).
app/
├── layout.tsx # Root layout — wraps everything in <IblaiProviders>
├── (app)/
│ ├── layout.tsx # Signed-in shell: SDK SidebarProvider + PlatformSidebar + navbar, one scroller
│ ├── (paid)/ # Chat and analytics; being signed in here means being a member
│ │ ├── page.tsx # Chat — SDK <Chat> (?session= restores, ?new= starts fresh)
│ │ └── analytics/ # SDK <AnalyticsLayout> over eight pages
│ ├── about/ profile/ account/ notifications/[[...id]]/
├── paywall/ # The join page (public), checkout hand-off, return page
├── setup/ # The one setup question (outside the shell, no navbar)
├── sso-login-complete/ # SSO landing
└── api/paywall/
├── access/ checkout/ prices/ # Buyer rail — this server as the buyer, with IBLAI_API_KEY
└── admin/setup/ # Admin rail — forwards the admin's own platform token
components/
├── sidebar/ # app-sidebar.tsx (PlatformSidebar wrapper), recent-chats.tsx, chat-row.tsx, flat-nav-row.tsx
├── navbar/ # nav-bar.tsx, logo.tsx, user-profile-button.tsx, admin-mode-switch.tsx
├── setup/setup-screen.tsx # The question, then the Stripe key screen
├── loading-screen.tsx # The one loading / busy screen (OS look)
└── plan-card.tsx
lib/
├── paywall.ts paywall-admin.ts # Server-only: checkout, join, standing, platform-metadata store, catalogue
├── paywall-client.ts # Browser side: token header, catalogue, setup and standing checks
├── chat-rows.ts # Recents row labels
└── iblai/ # config.ts (env), tenant.ts, admin-mode.tsx, auth-utils.ts, storage-service.ts
providers/iblai-providers.tsx # initializeDataLayer + AuthProvider + TenantProvider + i18n
store/iblai-store.ts # Redux store (slice keys fixed by the SDK)
proxy.ts # CSP and the 404 for /about when the flag is off
src-tauri/ # Thin WebView shell for desktop and mobile
.github/workflows/ # release.yml (release-it on every push to main), tauri-build-desktop.yml (desktop bundles on demand)
.husky/commit-msg # commitlint
- Next.js — App Router, Turbopack
- @iblai/iblai-js — SDK for auth, the shell, chat, analytics, profile and account
- Tailwind CSS — utility-first styling with ibl.ai design tokens
- shadcn/ui — accessible UI primitives (base-nova)
- Tauri v2 — native desktop and mobile shells
- iblai.app — the platform: auth, agents, entitlement, analytics
- Clone the repo
- Install dependencies:
pnpm install --ignore-scripts pnpm huskyonce: the install skipsprepare, so the commit-msg hook (commitlint) is not there otherwise- Fill
iblai.envand.env.local(see Quick Start), thenpnpm dev
- Create a branch from
main:git checkout -b feat/my-feature - Make your changes;
pnpm fmtthe files you touched - Run
pnpm check,pnpm testandpnpm build - Commit with a conventional subject (
feat:,fix:,docs:,chore:… — commitlint checks it; it decides the next version and the changelog) and push your branch - Open a pull request against
main
- Use ibl.ai SDK components first — do not build custom components when an SDK equivalent exists; shadcn/ui (
pnpm dlx shadcn@latest add <component> -y) for everything else - Host SDK panels the way the OS does — full width, no card, the component's own background; read
AGENTS.mdbefore wrapping one - Do not override SDK styles — SDK components ship with their own styling
- Never set
output: 'export'— the paywall needs the server routes IBLAI_API_KEYandPAYWALL_*are server-only — neverNEXT_PUBLIC_, never read outside route handlers; secrets never go into platform metadata- A new env key lands in
.env.exampleand this README in the same change; a new route lands with a test in__tests__/ - Use
pnpmas the package manager
Use the iblai/vibe skills with Claude Code:
/iblai-vibe-auth # SSO authentication (already wired)
/iblai-vibe-agent-chat # Agent chat (already wired)
/iblai-vibe-profile # Profile page (already wired)
/iblai-vibe-account # Account/org settings (already wired)
/iblai-vibe-analytics # Analytics dashboard (already wired)
/iblai-vibe-notification # Notification centre (already wired)
/iblai-vibe-invite # Invite dialogs
/iblai-vibe-ops-deploy # Deploy to ibl.ai hosting
/iblai-vibe-ops-build # Desktop/mobile builds
/iblai-vibe-ops-upgrade # Upgrade SDK and skills
See AGENTS.md for the full list and the component priority rules.
- ibl.ai Documentation
- Vibe — developer toolkit for building with ibl.ai
- @iblai/mcp — MCP server for AI-assisted development
- iblai-app-cli — CLI for scaffolding ibl.ai apps
- ibl.ai/os — the open-source agent platform this app mirrors
- HQ — sister app for org / profile / analytics management
Built with ibl.ai Vibe


