# Iblai Vibe Auth

> Add ibl.ai SSO authentication to a vanilla Next.js app

- Skill: `iblai/iblai-vibe-auth` (Agent Skill, multi-file: 12 files)
- Install (CLI): `npx skillmds@latest add iblai/iblai-vibe-auth`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iblai/iblai-vibe-auth/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: iblai (https://skillmd.com/u/iblai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iblai/iblai-vibe-auth

---


# /iblai-vibe-auth

Add ibl.ai SSO authentication to a vanilla Next.js app. After completion,
unauthenticated users are redirected to login.iblai.app and returned with
a session -- no API tokens to manage.

> **Templates:** the auth files this skill creates (providers, store, SSO
> callback, `lib/iblai/*`) are bundled here as Jinja2 assets —
> [`assets/`](assets/). You render them into the project in Step 3. See
> [`/iblai-vibe-scaffold`](../../start/iblai-vibe-scaffold/SKILL.md) for the `{{ }}` variable
> contract and the shared patch mechanics.

If `.env.local` already has `NEXT_PUBLIC_MAIN_TENANT_KEY` set to a real
value (not a placeholder like `your-main-platform`, `your-tenant`, `your-platform`,
`your-tenant-key`, `test-tenant`, `main`, or empty), keep that value.

> **Common setup (brand, conventions, env files, verification):** see [docs/skill-setup.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/docs/skill-setup.md).

## Which sign-in architecture? (read first)

Sign-in is the same hosted round trip for every ibl.ai front end
(`login.<domain>/login?app=…&redirect-to=<origin>&tenant=<org>` → back to
`/sso-login-complete?data=…`); what differs is **where the org comes from**.
`/iblai-vibe-start` records the answer in `iblai.env` as `ARCHITECTURE=`;
if it is missing, ask one question before writing files:

| `ARCHITECTURE` | The app is for | Org key | This skill does |
|---|---|---|---|
| `single-org` *(default)* | members of one organization — yours, or the customer you deploy to | pinned: `NEXT_PUBLIC_MAIN_TENANT_KEY` (never `main`) | everything below as written |
| `multi-org` | users who belong to many organizations (the os.ibl.ai model) | from the URL (`/platform/[org]/…`) plus a switcher; `NEXT_PUBLIC_MAIN_TENANT_KEY=main` is the community org | everything below, then **Going multi-org** at the end |
| `headless` | nobody signs in — scripts, CI, a backend | `IBLAI_ORG` per call | not this skill: `/iblai-api-login` and the `iblai-api-*` family |

The parameters, tokens, anonymous access, organization switching, native shells,
and the OS's implementation are in
[docs/auth-model.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/docs/auth-model.md).

## Step 0: Start from vibe-starter? (new projects)

Before running this skill, ask the user:

> Are you starting a new project from scratch? If so, vibe-starter
> (https://github.com/iblai/vibe/tree/main/skills/start/iblai-vibe-ops-init/assets/vibe-starter) is a pre-wired Next.js 16 +
> Tailwind v4 + shadcn/ui template with ibl.ai SSO auth, a responsive navbar,
> and profile/account/notifications pages already in place. Want to use that
> instead?

If yes, copy the bundled starter template from the installed
`iblai-vibe-ops-init` skill's `assets/vibe-starter/` directory (a sibling of
this skill's directory), or fetch it from the vibe repo if those assets are
not installed -- tell the user which path you took:

    cp -a <skills-dir>/iblai-vibe-ops-init/assets/vibe-starter/. .
    # or, without local assets:
    git clone --depth 1 https://github.com/iblai/vibe.git vibe-tmp && cp -a vibe-tmp/skills/start/iblai-vibe-ops-init/assets/vibe-starter/. . && rm -rf vibe-tmp

    pnpm install --ignore-scripts

> Run with `--ignore-scripts` to skip package lifecycle (postinstall) scripts.

If they prefer to wire auth into an existing app, continue below.

## Prerequisites

 **Want a complete app from scratch?** Use **vibe-starter** (Step 0 above) —
 it ships with auth, a navbar, and profile/account/notifications already
 wired, so you can skip this skill. For the underlying whole-app scaffold and
 its templates, see [`/iblai-vibe-scaffold`](../../start/iblai-vibe-scaffold/SKILL.md).

 **This skill** adds auth to a vanilla Next.js app or an existing project by
 creating the auth files directly (Step 3).

- Next.js 14+ with App Router (`app/` directory)
- Node.js 18+

## Step 1: Check Environment

Before proceeding, check for a `iblai.env`
in the project root. Look for `PLATFORM`, `DOMAIN`, and `TOKEN` variables.
If the file does not exist or is missing these variables, tell the user:
"You need an `iblai.env` with your platform configuration. Copy the
bundled template and fill in your values:
`cp iblai.env.example iblai.env` (vibe-starter ships the example) — or,
when the project has no `iblai.env.example`:
`curl -o iblai.env https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/iblai.env`"

If `PLATFORM` is set to a real value (not `your-platform` or `your-main-platform`), use it
as the platform key in Step 3. Otherwise use the placeholder and tell the user to replace it.

## Step 2: Customize Auth Interface

STOP here. You MUST ask the user these questions before
proceeding to Step 3. Do NOT skip this step.

### How

Fetch the org's `platform_name` (`GET https://api.{domain}/dm/api/core/orgs/{platform}/metadata/`), ask the user what the app does and whether they want the navbar, generate a headline and tagline, save the `AUTH_*` values to `iblai.env`, STOP for the user to review, then upload the logo (`POST …/platforms/{platform}/public-image-assets/`, three categories) and PUT `auth_web_skillsai` + `auth_web_mentorai` into the org metadata. The exact questions, defaults, upload table, and JSON payload are in [`references/auth-interface.md`](references/auth-interface.md) — follow it verbatim. Never drop the org's other metadata keys (GET, merge, PUT — see `/iblai-vibe-org-metadata`).

## Step 3: Create the auth files

Render the bundled [`assets/`](assets/) Jinja2 templates into the project —
strip the `.j2` suffix and substitute `{{ platform_key }}` with the platform
key. For a `src/` layout, place files under `src/lib`, `src/store`,
`src/providers` accordingly.

| Template (`assets/…`) | Destination |
|---|---|
| `config.ts.j2` | `lib/iblai/config.ts` |
| `auth-utils.ts.j2` | `lib/iblai/auth-utils.ts` |
| `storage-service.ts.j2` | `lib/iblai/storage-service.ts` |
| `tenant.ts.j2` | `lib/iblai/tenant.ts` |
| `iblai-store.ts.j2` | `store/iblai-store.ts` |
| `iblai-providers.tsx.j2` | `providers/iblai-providers.tsx` |
| `iblai-styles.css.j2` | `app/iblai-styles.css` |
| `sso-login-complete-page.tsx.j2` | `app/sso-login-complete/page.tsx` |

Then apply the project patches (each is idempotent — check for a marker
before editing; full mechanics in
[`/iblai-vibe-scaffold` → add-command](../../start/iblai-vibe-scaffold/references/add-command.md)):

- **`next.config`** — add the `@reduxjs/toolkit` webpack `resolve.alias` that
  deduplicates RTK. Without it the SDK binds a different `ReactReduxContext`
  and RTK Query hooks silently return `undefined`.
- **`globals.css`** — add `@import "./iblai-styles.css";`.
- **`.env.local`** — set `NEXT_PUBLIC_MAIN_TENANT_KEY=<platform key>` (←
  `PLATFORM`) and, for server-side platform calls, `IBLAI_API_KEY` (←
  `TOKEN`). Nothing else needs deriving — the URL vars default in the
  rendered `lib/iblai/config.ts`.

The SSO callback at `app/sso-login-complete/page.tsx` must stay **outside**
the providers (Step 5 / Troubleshooting), or it deadlocks on login.

## Step 4: Install Dependencies and Add Test Script

Add the SDK dependencies and install:

```bash
pnpm add @iblai/iblai-js @reduxjs/toolkit react-redux sonner lucide-react
pnpm install --ignore-scripts
```

> Run with `--ignore-scripts` to skip package lifecycle (postinstall) scripts.

These are:

- `@iblai/iblai-js` -- SDK (auth, data layer, UI components)
- `@reduxjs/toolkit` + `react-redux` -- state management (SDK uses RTK Query)
- `sonner` -- toast notifications
- `lucide-react` -- icons

If `package.json` does not already have a `"test"` script (or it still has the
default `create-next-app` placeholder), add vitest:

```bash
pnpm add -D vitest
```

Then set the test script in `package.json`:

```json
"scripts": {
  "test": "vitest run"
}
```

## Step 5: Wire Providers into Layout

Open `app/layout.tsx` and wrap `{children}` with the generated `IblaiProviders`.
Add `viewport-fit=cover` to the metadata so mobile builds (iOS/Android) respect
safe area insets and don't overlap with the status bar.

**If you have no existing providers:**

```tsx
import type { Metadata, Viewport } from "next";
import { IblaiProviders } from "@/providers/iblai-providers";

export const metadata: Metadata = {
  title: "My App",
};

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <IblaiProviders>{children}</IblaiProviders>
      </body>
    </html>
  );
}
```

**If you have existing providers** (e.g., custom contexts):

```tsx
import type { Metadata, Viewport } from "next";
import { IblaiProviders } from "@/providers/iblai-providers";
import { MyProvider } from "./my-provider";

export const metadata: Metadata = {
  title: "My App",
};

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <IblaiProviders>
          <MyProvider>
            {children}
          </MyProvider>
        </IblaiProviders>
      </body>
    </html>
  );
}
```

`IblaiProviders` must be the **outermost** provider -- it contains Redux, Auth,
and Organization providers that other components depend on. Place your own providers
inside it.

## Step 6: Configure Environment

Step 3 created `.env.local` with the platform key. Verify it has the
right value (edit the line directly if not):

```bash
NEXT_PUBLIC_MAIN_TENANT_KEY=your-main-platform
```

The API URLs default to hosted `iblai.app` in the rendered
`lib/iblai/config.ts`; override them in `.env.local` only when
self-hosting.

## Step 7: Import SDK Styles

Verify `app/globals.css` has the SDK imports (Step 3 adds these):

```css
@import '@iblai/iblai-js/web-containers/styles';
@source "../node_modules/@iblai/iblai-js/dist/web-containers/source";
```

If these lines are missing, add them near the top of `globals.css`.

## Step 8: Replace Default Home Page

After adding auth, check if `app/page.tsx` still has the default Next.js
content (look for `next/image`, `vercel.svg`, `Vercel`, or
`Get started by editing`). If it does, replace it with the ibl.ai home page —
render [`assets/home-page.tsx.j2`](assets/home-page.tsx.j2) into
`app/(app)/page.tsx` (or `app/page.tsx`), substituting `{{ }}` placeholders.

If the user has already customized their home page, skip this step.

## Step 9: Navbar

If the user said yes to the navbar question in Step 2, run `/iblai-vibe-navbar`
now before starting the dev server.

## Step 10: Start Dev Server

Start the dev server so the user can see the result:

```bash
pnpm dev
```

1. Open http://localhost:3000
2. You should be redirected to https://login.iblai.app
3. Log in (or create a free account)
4. You'll be returned to your app with a fully authenticated session
5. Check browser localStorage -- you should see `axd_token`, `userData`, `tenants`

## What Was Generated

| File | Purpose |
|------|---------|
| `app/sso-login-complete/page.tsx` | SSO callback -- stores tokens from URL into localStorage |
| `lib/iblai/config.ts` | Environment variable accessors (API URLs, platform key, auth URL) |
| `lib/iblai/storage-service.ts` | localStorage wrapper implementing the SDK's StorageService interface |
| `lib/iblai/auth-utils.ts` | `redirectToAuthSpa()`, `hasNonExpiredAuthToken()`, `handleLogout()` |
| `store/iblai-store.ts` | Redux store with `coreApiSlice`, `mentorReducer`, `mentorMiddleware`, and the slice keys the SDK `<Chat>` hard-codes (`chat`, `chatInput`, `chatSliceShared`, `files`, `rbac`, `subscription`, `topBanner`) |
| `providers/iblai-providers.tsx` | Provider chain: ReduxProvider > AuthProvider > TenantProvider |

## What Was Patched

- **`next.config.ts`** -- webpack `resolve.alias` to deduplicate `@reduxjs/toolkit`,
  `turbopack: {}` for Next.js 16+. Without the dedup,
  SDK components use a different `ReactReduxContext` and RTK Query hooks silently
  return `undefined` with zero HTTP requests.
- **`globals.css`** -- SDK base styles import.
- **`.env.local`** -- platform key (`NEXT_PUBLIC_MAIN_TENANT_KEY`) and, for
  server-side platform calls, `IBLAI_API_KEY`.

## Advanced: Route Groups

For production apps, consider moving the SSO callback outside the auth
providers using Next.js route groups:

```
app/
├── (auth)/
│   └── sso-login-complete/page.tsx   ← Outside providers (no AuthProvider wrapper)
└── (app)/
    ├── layout.tsx                     ← IblaiProviders wraps only this group
    └── page.tsx
```

This prevents the SSO callback deadlock where `AuthProvider` blocks rendering
before tokens are stored. Step 3 places the callback at
`app/sso-login-complete/` (flat, no route group) which works for simple apps.

For the route group pattern, see the **vibe-starter** reference app:
  https://github.com/iblai/vibe/tree/main/skills/start/iblai-vibe-ops-init/assets/vibe-starter

## Troubleshooting

### Sign-in redirects to login.iblai.app and never comes back

Every origin the app runs on — `http://localhost:3000`, the deployed URL, a
Tauri custom scheme — must be in the organization's **allowed redirect
origins**. Nothing in the app can fix this; ask your ibl.ai operator to add
the origin. Also check the org key is not `main` (the shared org) or a
placeholder — the starter renders an alert for those instead of looping.

### "Unknown server error" with custom-domains on localhost

The SDK calls `/api/custom-domains?domain=localhost` as part of platform detection.
This fails on localhost but is **harmless** -- the platform is resolved from
`NEXT_PUBLIC_MAIN_TENANT_KEY` in `.env.local` instead. You can safely ignore
this console error during local development.

### SDK components show undefined / no API requests

`@reduxjs/toolkit` must be deduplicated in `next.config.ts`. Without the
webpack alias, the SDK's components use a different `ReactReduxContext` than
your app's `StoreProvider`, so RTK Query hooks silently return `undefined`.

Verify your next.config has:
```javascript
config.resolve.alias['@reduxjs/toolkit'] = rtkDir;
```

### Auth redirect loops

The SSO callback page (`app/sso-login-complete/page.tsx`) must NOT be wrapped
by `AuthProvider`. If it is, `AuthProvider` detects "no tokens" and redirects
to login before the callback can store the tokens -- creating an infinite loop.

If this happens, use the route group pattern described above to separate the
SSO callback from the authenticated routes.

### Blank screen after login

Check that `.env.local` has `NEXT_PUBLIC_MAIN_TENANT_KEY` set. Without it,
the platform resolution falls back to custom-domain detection which fails on
localhost, leaving the app in a broken state.

## Next Steps


After auth is set up, add more features using skills -- ask your AI assistant to use:

| Skill | What It Adds |
|-------|-------------|
| `/iblai-vibe-agent-chat` | In-process agent chat surface |
| `/iblai-vibe-profile` | User profile dropdown + settings page |
| `/iblai-vibe-account` | Account/organization settings page |
| `/iblai-vibe-analytics` | Analytics dashboard page |
| `/iblai-vibe-notification` | Notification bell + center page |

For a complete reference app with auth, navbar, and pages already wired:
  https://github.com/iblai/vibe/tree/main/skills/start/iblai-vibe-ops-init/assets/vibe-starter

**Brand guidelines**: [BRAND.md](https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/BRAND.md)

## Going multi-org (`ARCHITECTURE=multi-org`)

Five moves, all with SDK pieces vibe-starter already imports — the full
version is `providers/index.tsx` in [iblai/os](https://github.com/iblai/os):

1. **Org in the URL** — `app/platform/[tenantKey]/…`; read it with
   `useParams()`; keep `resolveAppTenant()` for the default org.
2. **Providers** — `TenantProvider currentTenant={stored} requestedTenant={fromRoute}`
   with `saveUserTokens`, `saveUserTenants`, `saveCurrentTenant`, `saveTenant`
   implemented (the starter's `iblai-providers.tsx` already has them).
3. **Switching** — `UserProfileDropdown showTenantSwitcher` (or `TenantSwitcher`)
   → `handleTenantSwitch(org, { authUrl })` from `@iblai/iblai-js/web-utils`:
   clears storage, re-enters the SPA with `tenant=`, broadcasts to other tabs.
4. **Anonymous per agent** — in `AuthProvider`'s `middleware`, for
   `^/platform/([^/]+)/([^/]+)` fetch the agent's public settings
   (`useLazyGetMentorPublicSettingsQuery`) and return `false` when
   `allow_anonymous` or `mentor_visibility === "viewable_by_anyone"`.
5. **Landing path guard** — after SSO, reset to `/` when the stored path
   names an org other than the one just authenticated
   (`resolveRedirectPath` on `SsoLogin`).

Register every deployed origin as an allowed redirect origin, exactly as for
a single-org app.

