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
64 changes: 43 additions & 21 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 an Angular application. This adds authentication, tenant context, role and privilege access control, feature flags and billing.

## Decide first — which login surface?

This decision shapes everything else. Make it before writing code; getting it wrong means rewriting the auth pages.

| You want | Use | What you build |
|---|---|---|
| The fastest path; Bridge owns the login UI | **Hosted auth** (default) | Nothing — no login page |
| Login inside your app | **SDK auth** | Nothing either — `...bridgeAuthRoutes()` serves every sign-in page |

**Setting `loginRoute` in `BridgeConfig` is the entire switch.** Without it you get hosted; with it you get in-app. 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.
Sign-in happens inside the app: the login, signup and password pages render in the app's own layout, and `...bridgeAuthRoutes()` serves every one of them. For the sign-in pages in detail, matching them to the app's design and customising them, read `sdk-auth-prompt.md` (`get_integration_guide` topic `auth`, or `bridge guide angular sdk-auth`).

## Prerequisites

Expand Down Expand Up @@ -44,8 +33,9 @@ import { routes } from './app.routes';

const bridgeConfig: BridgeConfig = {
appId: environment.bridgeAppId,
// Where a signed-out visitor is sent: the app's own login page.
loginRoute: '/auth/login',
// Defaults to production. Required for a stage / local / self-hosted app.
// The hosted sign-in pages follow it on Bridge's domains.
apiBaseUrl: environment.bridgeApiBaseUrl,
};

Expand All @@ -59,6 +49,8 @@ export const appConfig: ApplicationConfig = {
};
```

(If the developer explicitly asks for Bridge's hosted sign-in page, or the app already uses it, respect that: leave out `loginRoute`, sign in with `AuthService.login()` instead of a link to `/auth/login`, and log out with `AuthService.logout()`; a local or self-hosted Bridge also sets `hostedUrl`. Everything else stays the same.)

Configuration is code (Angular has no env-var convention): each option resolves as *explicit option > default*, and an empty string is unset. **An empty `appId` throws at startup, naming the option.** If `provideBridge()` itself is missing, nothing errors loudly: flags return their defaults and auth never resolves — check that first when something seems inert.

## Step 2b — Let Bridge serve its pages
Expand All @@ -80,7 +72,7 @@ export const routes: Routes = [
];
```

**Do not write an OAuth callback, a set-password page, or subscription pages.** These spreads own them, including the address in every signup verification email. The sign-in pages are public by construction; to replace one, `bridgeAuthRoutes({ overrides: { login: { component: MyLogin } } })`.
**Do not write a login page, a signup page, an OAuth callback, a set-password page, or subscription pages.** These spreads own them, including the address in every signup verification email. The sign-in pages are public by construction; to replace one, `bridgeAuthRoutes({ overrides: { login: { component: MyLogin } } })`.

## Step 3 — Guard routes declaratively

Expand Down Expand Up @@ -123,34 +115,64 @@ export const routes: Routes = [

> **Gating a whole page? Use a rule, not a component-level `if`.** A rule redirects before the component ever renders; an `if` inside the component means the page mounts, fetches, and only then hides itself — which leaks both the route's existence and whatever the page loaded on the way.

## Step 4 — Read the user
## Step 4 — Read the user, sign in and out

```ts
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { AuthService } from '@nebulr-group/bridge-angular';

@Component({ selector: 'app-header', standalone: true, template: `…` })
@Component({
selector: 'app-header',
standalone: true,
imports: [RouterLink],
template: `
@if (auth.isAuthenticated()) {
<span>{{ auth.profile()?.fullName ?? auth.profile()?.email }}</span>
<button (click)="logout()">Log out</button>
} @else {
<a routerLink="/auth/login">Log in</a>
}
`,
})
export class HeaderComponent {
private auth = inject(AuthService);
protected auth = inject(AuthService);

logout(): Promise<void> {
return this.auth.getBridgeAuth().logout({ redirectTo: '/auth/login' });
}
}
```

Always pass `redirectTo` to `logout()` so the user lands on the app's own login page. A signed-out visitor on a protected route is sent to `/auth/login` and brought back to that route afterwards; the link is only for pages that are public.

`ProfileService` exposes profile fields; `realtimeStatus` reports the live-channel state.

## Checklist

- [ ] `@nebulr-group/bridge-angular` installed
- [ ] `provideBridge(bridgeConfig)` in `appConfig.providers`, with a non-empty `appId`, `loginRoute: '/auth/login'`, and `apiBaseUrl` for a stage or local app
- [ ] `...bridgeAuthRoutes()` in the routes; no hand-written login, signup, callback or set-password page
- [ ] The sign-in pages match the app's design (`sdk-auth-prompt.md`)
- [ ] Protected routes guarded with `bridgeAuthGuard()` and a `RouteGuardConfig`
- [ ] `logout()` passes `redirectTo: '/auth/login'`

## Verify it works

Do not stop at a clean build. Run it and confirm:

1. A protected route while signed out starts the login flow.
2. After login you land **inside the app**.
1. A protected route while signed out redirects to `/auth/login` **inside the app**.
2. After login you land back on that route, inside the app.
3. A reload keeps you signed in.
4. A flag-gated route redirects when the flag is off, and is reachable when on.
4. Log out: you land on the app's `/auth/login`.
5. A flag-gated route redirects when the flag is off, and is reachable when on.
6. The developer does steps 1, 2 and 4 themselves in a browser, and signs up once at `/auth/signup`; do not create test users. If a step fails, `bridge event auth-attempts --user <their email>` names the reason.

## Where to go next

| Goal | Guide |
|---|---|
| Login/signup inside your app | `sdk-auth-prompt.md` |
| The sign-in pages, matching the app's design, 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
52 changes: 34 additions & 18 deletions mcp/sdk-auth-prompt.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
# Bridge Angular — Authentication

You are wiring authentication into an Angular 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) | Nothing — no login page | No `loginRoute` |
| Login inside your app | **SDK auth** | Nothing — `...bridgeAuthRoutes()` serves the pages | Set `loginRoute: '/auth/login'` |

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

If the user has not said which they want, ask. A wrong guess here means rewriting the auth pages.

The rest of this guide covers **SDK auth**.
You are wiring authentication into an Angular application that uses The Bridge. Sign-in happens inside the app: the login, signup and password pages render in the app's own layout, and users never leave it. `...bridgeAuthRoutes()` serves every one of those pages; you write no login, signup, callback or set-password page.

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

Expand All @@ -28,7 +15,6 @@ The rest of this guide covers **SDK auth**.
| MFA challenge / setup | `<bridge-mfa-challenge>`, `<bridge-mfa-setup>` |
| Workspace ("tenant") selection | `<bridge-workspace-selector>`, `<bridge-tenant-selector>` |
| SSO button | `<bridge-sso-button>` |
| Hosted-login entry point | `<bridge-login>` |

> **`<bridge-login-form>` is not just an email and password box.** It drives forgot-password, magic link, passkeys, MFA and workspace selection as inline steps, and it decides which methods to show from the app's own configuration — which the client cannot see. Rebuilding any of it means reimplementing a flow that exists and then keeping it in sync with settings you have no visibility of.
>
Expand All @@ -53,6 +39,8 @@ const bridgeConfig: BridgeConfig = {
};
```

(If the developer explicitly asks for Bridge's hosted sign-in page, or the app already uses it, respect that: leave out `loginRoute`, sign in with `AuthService.login()` instead of a link to `/auth/login`, and log out with `AuthService.logout()`; a local or self-hosted Bridge also sets `hostedUrl`. Everything else stays the same.)

## Step 2 — Spread the auth routes

```ts
Expand All @@ -67,23 +55,51 @@ export const routes: Routes = [

That serves login, signup, the OAuth callback, set password (the address in every signup verification and password-reset email), forgot password, magic link, passkey setup and workspace selection. They are public by construction — no route rule needed, and no redirect loop.

Customise only as far as needed: `--bridge-*` CSS tokens; `bridgeAuthRoutes({ frame: AuthLayoutComponent, heading: (page) => … })`; replace one page with `overrides: { login: { component: MyLoginComponent } }` (a page you own renders `<bridge-login-form (login)="…">` and navigates itself).
## 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/styles.css` / `styles.scss`, as listed under `styles` in `angular.json`) and its CSS variables, `tailwind.config.*` or the Tailwind `@theme` block, a component library's theme (Angular Material, PrimeNG, Taiga, …), 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 `bridgeAuthRoutes({ frame: AuthLayoutComponent })` with a component that wraps `<ng-content>` in the app's own card or shell, 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 only as far as needed: `--bridge-*` CSS tokens (above); `bridgeAuthRoutes({ frame: AuthLayoutComponent, heading: (page) => … })`; replace one page with `overrides: { login: { component: MyLoginComponent } }` (a page you own renders `<bridge-login-form (login)="…">` and navigates itself).

## Step 3 — Guard the rest

Use the declarative `RouteGuardConfig` + `bridgeAuthGuard` from `integration-prompt.md` rather than checking auth inside each component. Rules run before the component renders; a component-level check renders, fetches, and only then hides.

## Reading the user

Inject `AuthService` for auth state and `ProfileService` for profile fields. For the raw token — to call your own backend — take it from the auth service, send it as `Authorization: Bearer <token>`, and verify it server-side with `@nebulr-group/bridge-nestjs` or `@nebulr-group/bridge-express`.
Inject `AuthService` for auth state and `ProfileService` for profile fields. To log out, call `getBridgeAuth().logout({ redirectTo: '/auth/login' })` on `AuthService` so the user lands on the app's own login page. For the raw token — to call your own backend — take it from the auth service, 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 control is UX. The check that matters happens on the server against the verified token.

## Common mistakes

- **Hand-rolling a password form** instead of `<bridge-login-form>` — loses magic link, passkeys, MFA and workspace selection, all configured server-side.
- **Writing login/signup/callback pages by hand** — `bridgeAuthRoutes()` already serves them; a hand-written set usually forgets `set-password/:token`, which every signup email links to.
- **Setting `loginRoute` while expecting hosted login**, or the reverse.
- **Leaving out `loginRoute`.** The guard then sends signed-out visitors off the app instead of to `/auth/login`.
- **Logging out without `redirectTo`.** The user then leaves the app instead of landing on `/auth/login`.
- **Trusting auth state for authorization.** It says a session exists, not what it may do.
- **Expecting an NgModule.** These are standalone components; import them into `imports: []`.

Expand Down
Loading