Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions mcp/integration-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@

You are integrating The Bridge into a React application. This adds authentication, tenant context, role and privilege access control, feature flags and billing to a React SPA.

## Decide first — which login surface?

This is the only decision that shapes the rest of the integration, and getting it wrong means rewriting the auth pages. Make it before you write anything.

| You want | Use | What you build |
|---|---|---|
| The fastest path; Bridge owns the login UI | **Hosted auth** (default) | Nothing beyond the one `/auth/*` route below |
| Login inside your own app, your own styling | **SDK auth** | The same one `/auth/*` route, plus `loginRoute: '/auth/login'` in the config |

**Hosted is the default and needs no `loginRoute`.** Adding `loginRoute: '/auth/login'` is the whole switch to in-app mode. Either way `<BridgeAuthRoutes>` serves every sign-in page (login, signup, the OAuth callback, set-password, forgot-password, magic-link, setup-passkey, workspaces) from one route: **never hand-write those pages.**

If the user has not said, ask. Do not guess: a wrong guess here is the most expensive rework in this guide.
Sign-in happens inside the app. One route, `/auth/*`, renders `<BridgeAuthRoutes>`, which serves every sign-in page (login, signup, the OAuth callback, set-password, forgot-password, magic-link, setup-passkey, workspaces): **never hand-write those pages.** For the sign-in pages, matching them to the app's design, and customising them, see `sdk-auth-prompt.md` (`get_integration_guide` topic `auth`, or `bridge guide react sdk-auth`).

## Prerequisites

Expand All @@ -39,7 +28,7 @@ VITE_BRIDGE_APP_ID=your-app-id-here

`<BridgeProvider>` reads `VITE_BRIDGE_*` (Vite) and `REACT_APP_BRIDGE_*` (Create React App) itself. Each field resolves as **explicit option > environment > default**, so do not copy variables into a config object by hand. With no app id anywhere, the provider refuses to start and logs which variable to set.

**For a non-production app, set `VITE_BRIDGE_API_BASE_URL` too** (e.g. `https://api-stage.thebridge.dev`). Unset means production, and a stage or local app id against the production API does not exist there; in a development build the provider says so once in the console. The hosted login address follows the API address on Bridge's own domains, so `VITE_BRIDGE_HOSTED_URL` is only for a local or self-hosted Bridge.
**For a non-production app, set `VITE_BRIDGE_API_BASE_URL` too** (e.g. `https://api-stage.thebridge.dev`). Unset means production, and a stage or local app id against the production API does not exist there; in a development build the provider says so once in the console. Bridge's page address (`VITE_BRIDGE_HOSTED_URL`, used for pages such as plan selection) follows the API address on Bridge's own domains; set it only for a local or self-hosted Bridge.

## Step 3 — Mount the provider

Expand All @@ -56,8 +45,7 @@ import App from './App';

createRoot(document.getElementById('root')!).render(
<StrictMode>
{/* SDK auth: config={{ loginRoute: '/auth/login' }}. Hosted auth: no props. */}
<BridgeProvider>
<BridgeProvider config={{ loginRoute: '/auth/login' }}>
<BrowserRouter>
<App />
</BrowserRouter>
Expand All @@ -66,13 +54,17 @@ createRoot(document.getElementById('root')!).render(
);
```

`loginRoute` is where `<ProtectedRoute>` sends a signed-out visitor: the login page `<BridgeAuthRoutes>` serves on `/auth/*`.

(If the developer explicitly asks for Bridge's hosted sign-in page, or the app already uses it, respect that: leave out `loginRoute`, and send the "Log in" button, and the user after logging out, through `login()` from `useAuth()` instead of to `/auth/login`. Everything else stays the same, including the `/auth/*` route, which still handles the OAuth callback.)

Common `BridgeConfig` fields (all optional when the environment is set):

| Field | Default | Description |
|---|---|---|
| `appId` | `VITE_BRIDGE_APP_ID` | Your Bridge app ID |
| `apiBaseUrl` | `VITE_BRIDGE_API_BASE_URL`, else production | Only for stage / local / self-hosted |
| `loginRoute` | — | **Setting this switches to SDK (in-app) auth** |
| `loginRoute` | — | The in-app login page, `'/auth/login'` |
| `defaultRedirectRoute` | `'/'` | Where to land after login |
| `billing` | see `billing-prompt.md` | Subscription pages, paywall, upgrade dialog |
| `debug` | `false` | Debug logging |
Expand Down Expand Up @@ -101,31 +93,41 @@ function App() {

The adapter components also register the router as Bridge's navigation, so the guard, the OAuth callback and the paywall redirect stay in-app. An app that renders none of them calls `useBridgeRouter()` from the same entry once inside the router — skipping that makes post-login redirects full page reloads.

`<ProtectedRoute>` shows a loading state until auth resolves, then either renders its children or starts the login flow — hosted portal or your `loginRoute`, remembering the page the visitor asked for.
`<ProtectedRoute>` shows a loading state until auth resolves, then either renders its children or sends the visitor to `loginRoute`, remembering the page the visitor asked for.

To change how the sign-in pages look: `--bridge-*` CSS tokens first; then `frame(page, children)` / `heading(page)` render props on `<BridgeAuthRoutes>`; then take one page over by element, `pages={{ login: <MyLoginPage /> }}`. See `learning/mechanisms.md`.

## Step 6 — Read the user
## Step 5 — Read the user

```tsx
import { useAuth } from '@nebulr-group/bridge-react';
import { Link, useNavigate } from 'react-router-dom';

function Header() {
const { isAuthenticated, login, logout } = useAuth();
const { isAuthenticated, logout } = useAuth();
const navigate = useNavigate();
const signOut = async () => {
await logout();
navigate('/auth/login');
};
return isAuthenticated
? <button onClick={logout}>Log out</button>
: <button onClick={() => login()}>Log in</button>;
? <button onClick={signOut}>Log out</button>
: <Link to="/auth/login">Log in</Link>;
}
```

`logout()` takes no options: it clears the session in the browser and does not navigate. Send the visitor to the app's own login page yourself, as above.

## Verify it works

Do not declare this done on a clean type-check. Run the app and confirm:

1. Visiting a protected route while signed out starts the login flow.
2. Completing login lands you back **inside the app**, not on the callback URL.
1. Visiting a protected route while signed out shows the app's own login page at `/auth/login`.
2. Completing login lands you back **inside the app**, on the page you asked for, not on the callback URL.
3. A reload keeps you signed in.
4. Logout returns you to a public route.
4. Logout returns you to `/auth/login`.

Ask the developer to sign up and sign in themselves in the running app. If sign-in fails, `bridge event auth-attempts --user <their email>` explains why.

Step 2 is the one that catches a missing router registration, and it is invisible in any test that does not use a real browser.

Expand All @@ -134,7 +136,7 @@ Step 2 is the one that catches a missing router registration, and it is invisibl
| Goal | Guide |
|---|---|
| The rules everything builds on (limits, levels, customising) | `learning/mechanisms.md` |
| Build login/signup inside your app | `sdk-auth-prompt.md` |
| The sign-in pages, matching the app's design, and customising them | `sdk-auth-prompt.md` |
| Gate features behind a flag | `feature-flags-prompt.md` |
| Plans, checkout, quotas | `billing-prompt.md` |
| Team / workspace management UI | `team-prompt.md` |
Expand Down
56 changes: 37 additions & 19 deletions mcp/sdk-auth-prompt.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
# Bridge React — Authentication

You are wiring authentication into a React application that uses The Bridge.

## Decide first — hosted or in-app?

| You want | Mode | What you build | Config |
|---|---|---|---|
| Bridge owns the login UI | **Hosted** (default) | One `/auth/*` route rendering `<BridgeAuthRoutes />` | No `loginRoute` |
| Login inside your app, your styling | **SDK auth** | The same one `/auth/*` route | `loginRoute: '/auth/login'` |

**One config field is the entire switch.** Adding `loginRoute` to `BridgeConfig` turns hosted mode off. If you are being redirected to a route you never built, that field is why.

If the user has not said which they want, ask. This is not a detail to infer — a wrong guess means rewriting the auth pages.

The rest of this guide covers **SDK auth**. For hosted, there is nothing to build beyond the `/auth/*` route in `integration-prompt.md`.
You are wiring authentication into a React application that uses The Bridge. Sign-in happens inside the app: one `/auth/*` route renders `<BridgeAuthRoutes />`, and its pages are styled to match the app. Tell the developer that in one line.

## The components — reach for these, do not hand-roll

Expand Down Expand Up @@ -61,7 +48,9 @@ import '@nebulr-group/bridge-react/styles';
</BridgeProvider>
```

The app id comes from `VITE_BRIDGE_APP_ID`; an option you pass explicitly wins over the environment.
The app id comes from `VITE_BRIDGE_APP_ID`; an option you pass explicitly wins over the environment. `loginRoute` is where `<ProtectedRoute>` sends a signed-out visitor. If you are being redirected to a route you never built, this field is why.

(If the developer explicitly asks for Bridge's hosted sign-in page, or the app already uses it, respect that: leave out `loginRoute`, and send the "Log in" button, and the user after logging out, through `login()` from `useAuth()` instead of to `/auth/login`. Everything else stays the same, including the `/auth/*` route, which still handles the OAuth callback.)

## Step 3 — One route serves every auth page

Expand All @@ -87,7 +76,34 @@ Without a router: `<BridgeAuthRoutes base="/auth" />` from the main entry.

**Do not hand-write `/auth/login`, `/auth/signup`, `/auth/set-password/:token` or any other auth page.** The set-password page is where every signup verification email lands; a forgotten one sends every new signup to a 404. `<BridgeAuthRoutes>` owns the list.

Customise in rungs, climbing only as far as needed:
## Match the app's design — always

The sign-in pages render inside the app, so they must look like the rest of it. Bridge's built-in look (indigo buttons, grey borders) is a fallback for an app with no design, never the finished result. Do this without asking:

1. **Read the app's design.** The global stylesheet (`src/index.css`, `src/App.css` or what `main.tsx` imports) and its CSS variables, `tailwind.config.*` or the Tailwind `@theme` block, a component library's theme (shadcn/ui, MUI, Chakra, Mantine, …), the font the app loads, and one existing primary button and text input.
2. **Set the `--bridge-*` tokens from it** in that global stylesheet, pointing at the app's own variables rather than copying values, so dark mode and later theme changes carry over:

```css
:root {
--bridge-primary: var(--color-primary); /* the app's primary button background */
--bridge-primary-hover: var(--color-primary-hover);
--bridge-primary-fg: var(--color-primary-foreground);
--bridge-border: var(--color-border);
--bridge-input-focus: var(--color-ring);
--bridge-border-radius: var(--radius);
--bridge-foreground: var(--color-foreground);
--bridge-muted: var(--color-muted-foreground);
--bridge-bg: var(--color-background);
--bridge-muted-bg: var(--color-muted);
}
```

The names on the right are examples: use the variables the app actually defines, or its literal values when it has none. Map its error and success colours to the `--bridge-alert-*` tokens. The components inherit the page font; set nothing for it unless the app puts its font on something other than `body`. The full token list: `bridge guide mechanisms`.
3. **Give the pages the app's frame** when the app's own pages sit in a card, a panel or a split layout: pass `frame={(page, children) => …}` to `<BridgeAuthRoutes>` (rung 2 below), built from the app's own classes or components, so sign-in is not the one page that looks different.

Ask the developer for colours only when the project has no styles of its own.

Customise further in rungs, climbing only as far as needed:

1. `--bridge-*` CSS tokens (colours, radius, spacing).
2. `frame={(page, children) => …}` replaces everything around the form on every page; `heading={(page) => …}` replaces each page's main heading.
Expand All @@ -96,16 +112,18 @@ Customise in rungs, climbing only as far as needed:

## Step 4 — Guard routes

Wrap routes that need a signed-in user in `<ProtectedRoute>`. It shows a loading state until auth resolves, then renders its children or sends the visitor to `loginRoute` (or the hosted login), remembering where they were going. It is the same component in both modes.
Wrap routes that need a signed-in user in `<ProtectedRoute>`. It shows a loading state until auth resolves, then renders its children or sends the visitor to `loginRoute`, remembering where they were going.

bridge-react has no declarative `routeConfig`: you decide which routes are public in your router. For a guard of your own, use `useAuth()` and keep the `isLoading` branch — returning a redirect while auth resolves bounces a signed-in user to login on every refresh.

## Reading the user

```tsx
const { isAuthenticated, isLoading, login, logout } = useAuth();
const { isAuthenticated, isLoading, logout } = useAuth();
```

Link "Log in" to `/auth/login`. `logout()` takes no options: it clears the session in the browser and does not navigate, so send the visitor to the app's login page yourself (`await logout(); navigate('/auth/login')`).

For profile fields use `useProfile()`. For the raw token (to call your own backend) use `useBridgeToken()` — send it as `Authorization: Bearer <token>` and verify it server-side with `@nebulr-group/bridge-nestjs` or `@nebulr-group/bridge-express`.

> **Never make an authorization decision on the client alone.** Hiding a button is UX; the check that matters happens on the server against the verified token. A client-side role check is a hint, not a boundary.
Expand All @@ -116,7 +134,7 @@ For profile fields use `useProfile()`. For the raw token (to call your own backe
- **Hand-rolling a password form** instead of `<LoginForm>` — loses magic link, passkeys, MFA and workspace selection, all of which are configured server-side and cannot be replicated from the client.
- **Forgetting `import '@nebulr-group/bridge-react/styles'`** — components look broken.
- **Omitting the `isLoading` branch** in a guard — signed-in users get bounced to login on refresh.
- **Setting `loginRoute` while expecting hosted login**, or the reverse.
- **Calling `login()` from `useAuth()` for the "Log in" button.** It leaves the app; link to `/auth/login` instead.
- **Trusting `useAuth()` for authorization.** It tells you whether a session exists, not what it may do.

## Configuring auth methods
Expand Down
Loading