diff --git a/mcp/integration-prompt.md b/mcp/integration-prompt.md index 6682a3d..8aedc3a 100644 --- a/mcp/integration-prompt.md +++ b/mcp/integration-prompt.md @@ -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 `` 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 ``, 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 @@ -39,7 +28,7 @@ VITE_BRIDGE_APP_ID=your-app-id-here `` 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 @@ -56,8 +45,7 @@ import App from './App'; createRoot(document.getElementById('root')!).render( - {/* SDK auth: config={{ loginRoute: '/auth/login' }}. Hosted auth: no props. */} - + @@ -66,13 +54,17 @@ createRoot(document.getElementById('root')!).render( ); ``` +`loginRoute` is where `` sends a signed-out visitor: the login page `` 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 | @@ -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. -`` 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. +`` 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 ``; then take one page over by element, `pages={{ login: }}`. 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 - ? - : ; + ? + : Log in; } ``` +`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 ` 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. @@ -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` | diff --git a/mcp/sdk-auth-prompt.md b/mcp/sdk-auth-prompt.md index 7c40cba..72a6a67 100644 --- a/mcp/sdk-auth-prompt.md +++ b/mcp/sdk-auth-prompt.md @@ -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 `` | 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 ``, 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 @@ -61,7 +48,9 @@ import '@nebulr-group/bridge-react/styles'; ``` -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 `` 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 @@ -87,7 +76,34 @@ Without a router: `` 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. `` 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 `` (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. @@ -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 ``. 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 ``. 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 ` 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. @@ -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 `` — 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