@nebulr-group/bridge-cli 0.6.0-beta.2 → 0.6.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/commands/flag.command.d.ts.map +1 -1
- package/dist/commands/flag.command.js +19 -6
- package/dist/commands/flag.command.js.map +1 -1
- package/dist/commands/guide.command.d.ts +6 -0
- package/dist/commands/guide.command.d.ts.map +1 -1
- package/dist/commands/guide.command.js +101 -1
- package/dist/commands/guide.command.js.map +1 -1
- package/dist/commands/journeys.d.ts +2 -1
- package/dist/commands/journeys.d.ts.map +1 -1
- package/dist/commands/journeys.js +15 -1
- package/dist/commands/journeys.js.map +1 -1
- package/dist/commands/plan.command.d.ts +39 -0
- package/dist/commands/plan.command.d.ts.map +1 -1
- package/dist/commands/plan.command.js +134 -10
- package/dist/commands/plan.command.js.map +1 -1
- package/dist/commands/role.command.d.ts.map +1 -1
- package/dist/commands/role.command.js +2 -1
- package/dist/commands/role.command.js.map +1 -1
- package/dist/commands/tenant.command.d.ts.map +1 -1
- package/dist/commands/tenant.command.js +12 -5
- package/dist/commands/tenant.command.js.map +1 -1
- package/dist/commands/token.command.d.ts.map +1 -1
- package/dist/commands/token.command.js +6 -0
- package/dist/commands/token.command.js.map +1 -1
- package/dist/commands/user.command.d.ts.map +1 -1
- package/dist/commands/user.command.js +6 -0
- package/dist/commands/user.command.js.map +1 -1
- package/dist/flag-coherence.d.ts +42 -0
- package/dist/flag-coherence.d.ts.map +1 -0
- package/dist/flag-coherence.js +103 -0
- package/dist/flag-coherence.js.map +1 -0
- package/dist/preview.d.ts +9 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +63 -0
- package/dist/preview.js.map +1 -0
- package/dist/prompts/auth-master-integration-prompt.md +7 -5
- package/dist/prompts/billing/master.md +35 -18
- package/dist/prompts/decisions/feature-control.md +105 -0
- package/dist/prompts/decisions/going-live.md +59 -0
- package/dist/prompts/decisions/login.md +68 -0
- package/dist/prompts/decisions/look-and-feel.md +62 -0
- package/dist/prompts/decisions/payments.md +81 -0
- package/dist/prompts/decisions/roles.md +72 -0
- package/dist/prompts/decisions/teams.md +56 -0
- package/dist/prompts/flags/master.md +8 -8
- package/dist/prompts/integration-success.md +4 -5
- package/dist/prompts/mcp-tools.json +77 -0
- package/dist/prompts/mechanisms.md +284 -0
- package/dist/prompts/orientation.md +40 -0
- package/dist/shape.d.ts +25 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +74 -0
- package/dist/shape.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# How Bridge works — the mechanisms and the levels
|
|
2
|
+
|
|
3
|
+
One page for the rules every Bridge guide builds on. Read it before you write code against a limit, an upgrade prompt or a restyled sign-in page. The per-framework guides (`bridge guide svelte`, `bridge guide nestjs`, …) say *which file*; this page says *why it works* and *which level to pick*.
|
|
4
|
+
|
|
5
|
+
The examples use the SvelteKit frontend (`@nebulr-group/bridge-svelte`) and the NestJS backend (`@nebulr-group/bridge-nestjs`), the two plugins with this surface today.
|
|
6
|
+
|
|
7
|
+
## The whole integration
|
|
8
|
+
|
|
9
|
+
What a developer's repo holds once Bridge is in. Nothing else is required; every other page Bridge needs, it serves.
|
|
10
|
+
|
|
11
|
+
**Frontend (SvelteKit) — 15 lines in four files, plus one line of `.env`:**
|
|
12
|
+
|
|
13
|
+
```env
|
|
14
|
+
# .env
|
|
15
|
+
VITE_BRIDGE_APP_ID=your-app-id
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// src/routes/+layout.ts
|
|
20
|
+
import { bridgeBootstrap } from '@nebulr-group/bridge-svelte';
|
|
21
|
+
export const ssr = false;
|
|
22
|
+
export const load = bridgeBootstrap({ rules: [{ match: new RegExp('^/auth($|/)'), public: true }] });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```svelte
|
|
26
|
+
<!-- src/routes/+layout.svelte -->
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import { BridgeBootstrap } from '@nebulr-group/bridge-svelte';
|
|
29
|
+
import '@nebulr-group/bridge-svelte/styles';
|
|
30
|
+
let { children } = $props();
|
|
31
|
+
</script>
|
|
32
|
+
<BridgeBootstrap>
|
|
33
|
+
{@render children()}
|
|
34
|
+
</BridgeBootstrap>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```svelte
|
|
38
|
+
<!-- src/routes/auth/[...bridge]/+page.svelte — every sign-in page -->
|
|
39
|
+
<script lang="ts">
|
|
40
|
+
import { BridgeAuthRoutes } from '@nebulr-group/bridge-svelte';
|
|
41
|
+
</script>
|
|
42
|
+
<BridgeAuthRoutes />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
With plans, one more four-line file serves the subscription page, the paywall and both checkout return pages:
|
|
46
|
+
|
|
47
|
+
```svelte
|
|
48
|
+
<!-- src/routes/subscription/[...bridge]/+page.svelte -->
|
|
49
|
+
<script lang="ts">
|
|
50
|
+
import { BridgeBillingRoutes } from '@nebulr-group/bridge-svelte';
|
|
51
|
+
</script>
|
|
52
|
+
<BridgeBillingRoutes />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Backend (NestJS) — 4 lines, plus one line of environment.** The lines marked `// +` are Bridge's; the rest is what `nest new` already gave you:
|
|
56
|
+
|
|
57
|
+
```env
|
|
58
|
+
# .env
|
|
59
|
+
BRIDGE_APP_ID=your-app-id
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// src/app.module.ts
|
|
64
|
+
import { Module } from '@nestjs/common';
|
|
65
|
+
import { BridgeModule } from '@nebulr-group/bridge-nestjs'; // +
|
|
66
|
+
import { ExportsController } from './exports.controller';
|
|
67
|
+
|
|
68
|
+
@Module({
|
|
69
|
+
imports: [BridgeModule.forRoot({ guard: { global: true } })], // + every route needs a signed-in user
|
|
70
|
+
controllers: [ExportsController],
|
|
71
|
+
})
|
|
72
|
+
export class AppModule {}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// src/exports.controller.ts
|
|
77
|
+
import { Controller, Post } from '@nestjs/common';
|
|
78
|
+
import { RequireQuota } from '@nebulr-group/bridge-nestjs'; // +
|
|
79
|
+
|
|
80
|
+
@Controller('exports')
|
|
81
|
+
export class ExportsController {
|
|
82
|
+
@Post()
|
|
83
|
+
@RequireQuota('exports') // + the plan limit
|
|
84
|
+
create() {
|
|
85
|
+
return { exported: true };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`BRIDGE_APP_ID` must be in the process environment. NestJS does not read `.env` by itself: start with `node --env-file=.env dist/main.js`, or set it in your container or process manager.
|
|
91
|
+
|
|
92
|
+
**Settings are read for you.** Each field resolves as *explicit option > environment > default*; an empty value counts as unset.
|
|
93
|
+
|
|
94
|
+
| Frontend | Backend | When to set it |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `VITE_BRIDGE_APP_ID` | `BRIDGE_APP_ID` | Always. Missing, Bridge refuses to start and names the variable |
|
|
97
|
+
| `VITE_BRIDGE_API_BASE_URL` | `BRIDGE_API_BASE_URL` | Only for a non-production app (stage, local, self-hosted). Unset means production |
|
|
98
|
+
| `VITE_BRIDGE_HOSTED_URL` | — | Only for a local or self-hosted Bridge. On Bridge's own domains it follows the API address (`api-stage` → `auth-stage`) |
|
|
99
|
+
| `VITE_BRIDGE_DEBUG` | `BRIDGE_DEBUG` | `true` for console logging |
|
|
100
|
+
|
|
101
|
+
## 1. The server decides; the client decorates
|
|
102
|
+
|
|
103
|
+
Anyone can call your API with curl, so a limit or a paid feature is enforced on the backend handler, never in the browser. Everything the frontend shows about limits — the upgrade dialog, a disabled button, a hidden panel — explains or anticipates a decision the server already makes. Remove every frontend check and the product is still correct; remove the backend decorator and it is not.
|
|
104
|
+
|
|
105
|
+
## 2. A POST increments the limit
|
|
106
|
+
|
|
107
|
+
The backend decorator **is** the increment. There is nothing else to wire: no "report usage" call after the insert, no counter in your database, no endpoint for the frontend.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
@Post()
|
|
111
|
+
@RequireQuota('exports')
|
|
112
|
+
create() { /* your code */ }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- **Before the handler:** at the plan's limit the request is refused with `402` and a body the frontend understands (`{ code: 'QUOTA_EXCEEDED', metric, used, limit, fix }`). Your handler never runs.
|
|
116
|
+
- **After the handler:** only when it answered **2xx**, Bridge records the use — exactly one write. A handler that throws or answers 4xx/5xx records nothing.
|
|
117
|
+
- **Retries:** a request that sends an `Idempotency-Key` header is counted once per key, however often the client retries.
|
|
118
|
+
- **Fail-closed:** if Bridge cannot answer, the request is refused with `503`, never let through unchecked.
|
|
119
|
+
- A `metered` quota never refuses: past its allowance it bills per unit.
|
|
120
|
+
|
|
121
|
+
## 3. Counter or gauge
|
|
122
|
+
|
|
123
|
+
**If deleting it frees room, it's a gauge and your app counts it; if it happened, it's a counter and Bridge counts it.**
|
|
124
|
+
|
|
125
|
+
| | Counter | Gauge |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| Examples | exports, API calls, AI completions, emails sent | tickets, projects, stored files, seats |
|
|
128
|
+
| Who counts | Bridge, one event per successful request | Your app — you pass your own count |
|
|
129
|
+
| Resets | every billing period ("40 of 100 this month") | never ("8 of 10 projects") |
|
|
130
|
+
| Configure | `bridge plan quota set <plan> --metric exports --limit 100 --policy hard` (counter is the default) | `bridge plan quota set <plan> --metric tickets --limit 10 --policy hard --kind gauge` |
|
|
131
|
+
| Backend | `@RequireQuota('exports')` | `@RequireQuota('tickets', { current })` on create, `@SyncQuota('tickets', { current })` on delete |
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
@Post()
|
|
135
|
+
@RequireQuota('tickets', { current: (t, self: TicketsController) => self.tickets.countFor(t.id) })
|
|
136
|
+
create() { /* … */ }
|
|
137
|
+
|
|
138
|
+
@Delete(':id')
|
|
139
|
+
@SyncQuota('tickets', { current: (t, self: TicketsController) => self.tickets.countFor(t.id) })
|
|
140
|
+
remove() { /* … */ }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`current` returns your own count, and that count is what the limit is compared against, so Bridge's copy heals itself if it ever missed an update. There is no decrement and no reservation: every create and delete sends the whole count.
|
|
144
|
+
|
|
145
|
+
**Seats** (`users`) are a gauge Bridge keeps itself from workspace membership. `@RequireQuota('users')` on your invite handler checks the seat limit and writes nothing.
|
|
146
|
+
|
|
147
|
+
`bridge plan quota list` shows every metric the app's plans limit, with its kind.
|
|
148
|
+
|
|
149
|
+
**Every hard quota is also an entitlement** with the same name, true while there is room. So never pair `@RequireEntitlement('exports')` with `@RequireQuota('exports')`: at the cap the entitlement answers `403` before the quota can answer the `402` the frontend knows how to upsell. Use `@RequireEntitlement` for a plan *feature* (`analytics`, `sso`), `@RequireQuota` for a *limit*.
|
|
150
|
+
|
|
151
|
+
**A plan feature is a hard quota nothing counts.** There is no separate entitlement setting: `bridge plan quota set pro --metric analytics --limit 1 --policy hard` makes `analytics` true on `pro`, and a plan without that quota answers false. Gate it with `@RequireEntitlement('analytics')` on the backend and `<Entitled to="analytics">` in the UI. `app_active` is always there: true while the workspace's subscription is active, trialing, past due or cancelling at period end.
|
|
152
|
+
|
|
153
|
+
## 4. Three ways to handle a limit in the UI
|
|
154
|
+
|
|
155
|
+
Pick the lowest level that does the job. Each is optional; level 0 is on without code.
|
|
156
|
+
|
|
157
|
+
| Level | What the page writes | What the user sees |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| **0 — nothing** | a plain button calling your API | Your backend refuses at the cap (`402`), and `<BridgeBootstrap>` opens an **upgrade dialog** naming the metric, linking to the subscription page. A workspace member who cannot manage billing is told to ask the owner instead |
|
|
160
|
+
| **1 — one component** | `<QuotaGate metric="tickets">…</QuotaGate>` around the button; `<Entitled to="analytics">…</Entitled>` around a paid feature | The button is disabled at a known hard cap with an upgrade line beside it; the paid feature shows only on a plan that grants it |
|
|
161
|
+
| **2 — your own UI** | `useQuota('tickets')` and `$entitlements.can('analytics')` | Whatever you build from the live numbers |
|
|
162
|
+
|
|
163
|
+
```svelte
|
|
164
|
+
<script lang="ts">
|
|
165
|
+
import { QuotaGate, Entitled, useQuota } from '@nebulr-group/bridge-svelte';
|
|
166
|
+
const tickets = useQuota('tickets');
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<!-- level 1 -->
|
|
170
|
+
<QuotaGate metric="tickets">
|
|
171
|
+
<button onclick={createTicket}>New ticket</button>
|
|
172
|
+
</QuotaGate>
|
|
173
|
+
|
|
174
|
+
<Entitled to="analytics">
|
|
175
|
+
<a href="/analytics">Analytics</a>
|
|
176
|
+
{#snippet fallback()}<a href="/subscription">Upgrade for analytics</a>{/snippet}
|
|
177
|
+
</Entitled>
|
|
178
|
+
|
|
179
|
+
<!-- level 2 -->
|
|
180
|
+
{#if tickets.loading}
|
|
181
|
+
Loading…
|
|
182
|
+
{:else if tickets.unlimited}
|
|
183
|
+
Unlimited tickets
|
|
184
|
+
{:else}
|
|
185
|
+
{tickets.used} of {tickets.limit} tickets
|
|
186
|
+
{/if}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- "Not loaded yet" is never "zero" and never "not allowed": `useQuota` numbers stay `null` while `loading`, `<QuotaGate>` stays enabled while loading, and `<Entitled>` renders nothing (or its `loading` snippet) until `$entitlements.ready`.
|
|
190
|
+
- The dialog catches a `402 QUOTA_EXCEEDED` from the page's own origin, from Bridge, and from any call made with `bridgeFetch()`; a backend on another origin is listed in `billing.apiOrigins`. `billing: { upgradeDialog: false }` turns it off, `billing: { upgradeDialog: MyDialog }` replaces it.
|
|
191
|
+
- Do not write a quota `if`, a "limit reached" toast or a `/quota` endpoint of your own: level 0 already covers the refusal, and the frontend reads quota directly from Bridge.
|
|
192
|
+
|
|
193
|
+
## 5. Usage reported from the browser trusts the client
|
|
194
|
+
|
|
195
|
+
An app with no backend that sees the action — local-first, mobile, data on the device — can report usage from the frontend:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { bridge } from '@nebulr-group/bridge-svelte';
|
|
199
|
+
|
|
200
|
+
bridge.usage.report('exports'); // a counter: it happened
|
|
201
|
+
await bridge.usage.set('projects', projects.length); // a gauge: how many exist now
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This is **self-reported, trusted-client usage**. Anything in a browser can send any number, so **a frontend alone cannot enforce a limit**: Bridge shows and bills what the client reports, and the client can lie. When the app has a backend, report and enforce there (section 2). Use the browser path only when there is no server to do it.
|
|
205
|
+
|
|
206
|
+
## 6. Four levels of customising Bridge's pages
|
|
207
|
+
|
|
208
|
+
The same ladder holds for the sign-in pages (`<BridgeAuthRoutes>`) and the subscription pages (`<BridgeBillingRoutes>`). Climb only as far as you need.
|
|
209
|
+
|
|
210
|
+
| Rung | What you do | What you own |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| **0 — nothing** | Bridge's pages render inside your own `+layout.svelte` | Your navigation, header and shell already surround them |
|
|
213
|
+
| **1 — tokens** | Set `--bridge-*` CSS variables in your CSS | Colours, radius, spacing |
|
|
214
|
+
| **2 — frame and heading** | Pass the `frame(page, children)` and `heading(page)` snippets | Everything around the form on every page, and each page's heading |
|
|
215
|
+
| **3 — take over one page** | Create that page's own route file, e.g. `src/routes/auth/login/+page.svelte` | That one page; SvelteKit prefers it over `[...bridge]`, every other page keeps working |
|
|
216
|
+
| **4 — headless** | Build your own UI on `getBridgeAuth()` | Everything |
|
|
217
|
+
|
|
218
|
+
```svelte
|
|
219
|
+
<!-- rung 2: src/routes/auth/[...bridge]/+page.svelte -->
|
|
220
|
+
<script lang="ts">
|
|
221
|
+
import { BridgeAuthRoutes } from '@nebulr-group/bridge-svelte';
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<BridgeAuthRoutes>
|
|
225
|
+
{#snippet frame(page, children)}
|
|
226
|
+
<main class="auth-card">{@render children()}</main>
|
|
227
|
+
{/snippet}
|
|
228
|
+
{#snippet heading(page)}
|
|
229
|
+
<h1>{page === 'signup' ? 'Create your account' : 'Welcome back'}</h1>
|
|
230
|
+
{/snippet}
|
|
231
|
+
</BridgeAuthRoutes>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`heading` replaces only each page's main step (the login credentials step, the signup form, the set-password form); sub-steps such as "Reset your password" keep their own, so two headings never stack. There are no per-page snippets: to change more than frame and heading, take over the page (rung 3). A sign-in page you own navigates itself after sign-in: `<LoginForm onLogin={() => goto(readReturnTo($page.url) ?? '/')} />`.
|
|
235
|
+
|
|
236
|
+
**The token contract (rung 1).** These `--bridge-*` variables are the supported way to restyle the components; each has a built-in default. Set them on `:root` or on any wrapper.
|
|
237
|
+
|
|
238
|
+
| Token | Default | Styles |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| `--bridge-primary` | `#4f46e5` | Primary buttons, selected plan interval, active workspace border |
|
|
241
|
+
| `--bridge-primary-hover` | `#4338ca` | Primary button hover |
|
|
242
|
+
| `--bridge-primary-fg` | `#ffffff` | Text on primary buttons |
|
|
243
|
+
| `--bridge-primary-foreground` | `#ffffff` | Workspace avatar text; plan picker fallback when `--bridge-primary-fg` is unset |
|
|
244
|
+
| `--bridge-primary-light` | `#eff6ff` | Active workspace background |
|
|
245
|
+
| `--bridge-border` | `#d1d5db` | Input and secondary button borders |
|
|
246
|
+
| `--bridge-border-radius` | `6px` | Corners of inputs, buttons, alerts, cards |
|
|
247
|
+
| `--bridge-input-focus` | `#4f46e5` | Input focus ring |
|
|
248
|
+
| `--bridge-foreground` | `#374151` | Password-toggle hover, workspace names |
|
|
249
|
+
| `--bridge-muted` | `#6b7280` | Secondary text, hints, table headings |
|
|
250
|
+
| `--bridge-bg` | `#ffffff` | Plan-change confirmation panel |
|
|
251
|
+
| `--bridge-bg-muted` | `#f5f5f5` | MFA backup code |
|
|
252
|
+
| `--bridge-muted-bg` | `#f3f4f6` | Workspace list hover, plan interval tabs (which fall back to `--bridge-bg-muted`) |
|
|
253
|
+
| `--bridge-alert-error-bg` | `#fef2f2` | Error alert background |
|
|
254
|
+
| `--bridge-alert-error-fg` | `#991b1b` | Error alert text |
|
|
255
|
+
| `--bridge-alert-error-border` | `#fca5a5` | Error alert border |
|
|
256
|
+
| `--bridge-alert-success-bg` | `#f0fdf4` | Success alert background |
|
|
257
|
+
| `--bridge-alert-success-fg` | `#166534` | Success alert text |
|
|
258
|
+
| `--bridge-alert-success-border` | `#86efac` | Success alert border |
|
|
259
|
+
| `--bridge-auth-page-padding` | `3rem 1rem` | Padding of the default sign-in page container |
|
|
260
|
+
| `--bridge-billing-page-width` | `60rem` | Width of the subscription pages and `<BridgePaywallPage>` |
|
|
261
|
+
| `--bridge-billing-page-padding` | `3rem 1rem` | Padding of the subscription pages and `<BridgePaywallPage>` |
|
|
262
|
+
| `--bridge-paywall-bg` | `rgba(15, 23, 42, 0.72)` | Backdrop of the `<BridgePaywall>` overlay |
|
|
263
|
+
| `--bridge-paywall-panel-bg` | `#ffffff` | Panel of the `<BridgePaywall>` overlay |
|
|
264
|
+
|
|
265
|
+
All default rules use `:where()`, so any selector of yours wins without `!important`. Leaving out `import '@nebulr-group/bridge-svelte/styles'` makes the components headless: plain HTML for you to style.
|
|
266
|
+
|
|
267
|
+
## 7. Pages Bridge serves, and the one it only offers
|
|
268
|
+
|
|
269
|
+
- **Sign-in:** `src/routes/auth/[...bridge]/+page.svelte` serves login, signup, the OAuth callback, set password (where signup verification and password-reset emails land), forgot password, magic link, passkey setup and workspace selection. Which methods appear comes from the app's settings at runtime, so turning magic links on needs no code. Hosted and in-app sign-in use the same file; `loginRoute: '/auth/login'` in `bridgeBootstrap()` is the whole switch to in-app.
|
|
270
|
+
- **Subscription:** `src/routes/subscription/[...bridge]/+page.svelte` serves `/subscription`, the paywall `/subscription/plan`, and the checkout return pages `/subscription/success` and `/subscription/error`. Bridge's redirects and upgrade links point there by default.
|
|
271
|
+
- **The paywall** redirects a signed-in workspace with no plan to `/subscription/plan` before any page renders — only when the app has plans and its `paymentsAutoRedirect` setting is on (the default). `bridge app update --payments-auto-redirect false` turns it off.
|
|
272
|
+
- **A `/welcome` onboarding page is offered, never created unasked.** It is a product decision: ask the developer. Only if they say yes, add a one-line page rendering `<BridgePaywallPage />` and `billing: { paywallRoute: '/welcome' }` in `bridgeBootstrap()`.
|
|
273
|
+
- Any other address under a catch-all gets the app's own 404.
|
|
274
|
+
|
|
275
|
+
## Where the details live
|
|
276
|
+
|
|
277
|
+
| Topic | Command |
|
|
278
|
+
|---|---|
|
|
279
|
+
| Sign-in, hosted or in-app | `bridge guide svelte` · `bridge guide svelte sdk-auth` |
|
|
280
|
+
| Plans, the subscription pages, the UI levels | `bridge guide svelte billing` |
|
|
281
|
+
| Backend limits and entitlements | `bridge guide nestjs billing` |
|
|
282
|
+
| Backend sign-in and route protection | `bridge guide nestjs` |
|
|
283
|
+
| Feature flags | `bridge guide flags` |
|
|
284
|
+
| Proving sign-in works | `bridge test-user create`, then `bridge test-user verify` |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# What Bridge does
|
|
2
|
+
|
|
3
|
+
Relay this to the developer as it stands, then add one sentence about what their app has today (read it first: `get_app` and `get_setup_status`, or `bridge app get` and `bridge setup status`).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Bridge runs everything between signup and invoice for your app, so you don't build it yourself.
|
|
8
|
+
|
|
9
|
+
- **Login.** People sign up and sign in with a password, a magic link, a passkey, Google, GitHub and other providers, or their company's SSO, with optional two-factor. Bridge serves the pages; your app adds about fifteen lines. *Want to choose how people sign in?*
|
|
10
|
+
- **Teams and workspaces.** Each customer gets a workspace and can invite teammates into it. You decide whether people create their own workspace or join by invitation. *Is your app for single users or for teams?*
|
|
11
|
+
- **Roles and permissions.** Inside a workspace, roles decide who may do what, and your backend checks them on every request. *Who are the different kinds of people in a customer's workspace?*
|
|
12
|
+
- **Payments and plans.** Plans with prices, trials and limits ("3 projects on Free, 50 on Pro"), checkout through your own Stripe account, and an upgrade prompt when someone hits a limit. *Want to set up a free tier and a paid plan?*
|
|
13
|
+
- **Feature control.** Turn any feature on or off for everyone, a percentage of users, one plan or one customer, without a deploy. *Is there a feature you want to roll out carefully or sell on a higher plan?*
|
|
14
|
+
- **Look and feel.** Sign-in and subscription pages in your colours and fonts, inside your own layout, and emails sent in your app's name. *Want the sign-in pages to look like the rest of your app?*
|
|
15
|
+
- **Going live.** A readiness checklist that says exactly what still points at localhost or test keys, and a test sign-in that proves login works end to end. *Ready to check what stands between you and real users?*
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Where to go next
|
|
20
|
+
|
|
21
|
+
Each area has a decision guide: the questions to ask the developer, and the defaults to apply without asking.
|
|
22
|
+
|
|
23
|
+
| Area | Decision guide | Journey |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Login | `bridge guide decision login` | `bridge guide add-login` |
|
|
26
|
+
| Teams and workspaces | `bridge guide decision teams` | `bridge guide add-teams` |
|
|
27
|
+
| Roles and permissions | `bridge guide decision roles` | — |
|
|
28
|
+
| Payments and plans | `bridge guide decision payments` | `bridge guide add-paid-plan` |
|
|
29
|
+
| Feature control | `bridge guide decision feature-control` | `bridge guide add-feature-flag` |
|
|
30
|
+
| Look and feel | `bridge guide decision look-and-feel` | — |
|
|
31
|
+
| Going live | `bridge guide decision going-live` | `bridge guide go-live` |
|
|
32
|
+
|
|
33
|
+
In an MCP client the same guides are the resources `bridge://guides/decisions/<area>` (areas: `login`, `teams`, `roles`, `payments`, `feature-control`, `look-and-feel`, `going-live`), and the journeys are the server's prompts of the same names.
|
|
34
|
+
|
|
35
|
+
How limits, upgrade prompts and page customisation actually work, for every area: `bridge guide mechanisms`.
|
|
36
|
+
|
|
37
|
+
Two rules hold in every guide:
|
|
38
|
+
|
|
39
|
+
1. **Product questions go to the developer.** What to charge, who may do what, what a plan includes, what users see: ask, and wait for the answer. Never guess them.
|
|
40
|
+
2. **Mechanics you decide.** Where a guide gives a default, apply it and say why in one line. The developer can overrule it.
|
package/dist/shape.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant metadata keys the platform writes for its own conversion-pixel
|
|
3
|
+
* bookkeeping (tracking.service.ts, tenant.service.ts). Never app data.
|
|
4
|
+
*/
|
|
5
|
+
export declare const INTERNAL_TENANT_METADATA_KEYS: string[];
|
|
6
|
+
/** A tenant without the platform's pixel bookkeeping in `metadata`. */
|
|
7
|
+
export declare function shapeTenant<T>(tenant: T): T;
|
|
8
|
+
/**
|
|
9
|
+
* A plan without the payment provider's ids: each quota's `providerRefs`
|
|
10
|
+
* (Stripe price and meter ids) is dropped. Prices and quotas otherwise stay
|
|
11
|
+
* whole — `create_plan` and friends still return the full plan.
|
|
12
|
+
*/
|
|
13
|
+
export declare function shapePlan<T>(plan: T): T;
|
|
14
|
+
/**
|
|
15
|
+
* Roles with each privilege named once. Every role used to carry the full
|
|
16
|
+
* privilege object (id, key, description) for every privilege it grants, so a
|
|
17
|
+
* five-role app repeated the same descriptions five times. Now each role lists
|
|
18
|
+
* privilege KEYS — what code and create_role/update_role take — and the
|
|
19
|
+
* descriptions appear once, in `privileges`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function shapeRoleList(roles: unknown[]): {
|
|
22
|
+
roles: Array<Record<string, unknown>>;
|
|
23
|
+
privileges: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape.d.ts","sourceRoot":"","sources":["../src/shape.ts"],"names":[],"mappings":"AAUA;;;GAGG;AACH,eAAO,MAAM,6BAA6B,UAA+C,CAAC;AAE1F,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAU3C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAYvC;AAOD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG;IAC/C,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC,CAkBA"}
|
package/dist/shape.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* TBP-709 (D) — what `bridge … list` shows, minus the platform's internals.
|
|
3
|
+
*
|
|
4
|
+
* The REST DTOs these come from also serve the dashboards and the SDKs, so
|
|
5
|
+
* they are left alone; the trimming happens at the presentation layer.
|
|
6
|
+
* Port of bridge-api's MCP `shape.ts` (same functions, same tests) so the
|
|
7
|
+
* CLI lists and the MCP lists cannot drift. Every function is pure and leaves
|
|
8
|
+
* its input untouched.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Tenant metadata keys the platform writes for its own conversion-pixel
|
|
12
|
+
* bookkeeping (tracking.service.ts, tenant.service.ts). Never app data.
|
|
13
|
+
*/
|
|
14
|
+
export const INTERNAL_TENANT_METADATA_KEYS = ['pendingPixelEvents', 'conversionTracking'];
|
|
15
|
+
/** A tenant without the platform's pixel bookkeeping in `metadata`. */
|
|
16
|
+
export function shapeTenant(tenant) {
|
|
17
|
+
if (!tenant || typeof tenant !== 'object')
|
|
18
|
+
return tenant;
|
|
19
|
+
const metadata = tenant.metadata;
|
|
20
|
+
if (!metadata || typeof metadata !== 'object')
|
|
21
|
+
return tenant;
|
|
22
|
+
const kept = Object.fromEntries(Object.entries(metadata).filter(([k]) => !INTERNAL_TENANT_METADATA_KEYS.includes(k)));
|
|
23
|
+
return { ...tenant, metadata: kept };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A plan without the payment provider's ids: each quota's `providerRefs`
|
|
27
|
+
* (Stripe price and meter ids) is dropped. Prices and quotas otherwise stay
|
|
28
|
+
* whole — `create_plan` and friends still return the full plan.
|
|
29
|
+
*/
|
|
30
|
+
export function shapePlan(plan) {
|
|
31
|
+
if (!plan || typeof plan !== 'object')
|
|
32
|
+
return plan;
|
|
33
|
+
const quotas = plan.quotas;
|
|
34
|
+
if (!Array.isArray(quotas))
|
|
35
|
+
return plan;
|
|
36
|
+
return {
|
|
37
|
+
...plan,
|
|
38
|
+
quotas: quotas.map((q) => {
|
|
39
|
+
if (!q || typeof q !== 'object')
|
|
40
|
+
return q;
|
|
41
|
+
const { providerRefs: _drop, ...rest } = q;
|
|
42
|
+
return rest;
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Roles with each privilege named once. Every role used to carry the full
|
|
48
|
+
* privilege object (id, key, description) for every privilege it grants, so a
|
|
49
|
+
* five-role app repeated the same descriptions five times. Now each role lists
|
|
50
|
+
* privilege KEYS — what code and create_role/update_role take — and the
|
|
51
|
+
* descriptions appear once, in `privileges`.
|
|
52
|
+
*/
|
|
53
|
+
export function shapeRoleList(roles) {
|
|
54
|
+
const privileges = {};
|
|
55
|
+
const shaped = (roles ?? []).map((role) => {
|
|
56
|
+
const r = (role ?? {});
|
|
57
|
+
const granted = Array.isArray(r.privileges) ? r.privileges : [];
|
|
58
|
+
const keys = [];
|
|
59
|
+
for (const p of granted) {
|
|
60
|
+
if (typeof p === 'string') {
|
|
61
|
+
keys.push(p);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!p || typeof p.key !== 'string')
|
|
65
|
+
continue;
|
|
66
|
+
keys.push(p.key);
|
|
67
|
+
if (!(p.key in privileges))
|
|
68
|
+
privileges[p.key] = p.description ?? '';
|
|
69
|
+
}
|
|
70
|
+
return { ...r, privileges: keys };
|
|
71
|
+
});
|
|
72
|
+
return { roles: shaped, privileges };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape.js","sourceRoot":"","sources":["../src/shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,oBAAoB,EAAE,oBAAoB,CAAC,CAAC;AAE1F,uEAAuE;AACvE,MAAM,UAAU,WAAW,CAAI,MAAS;IACtC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IACzD,MAAM,QAAQ,GAAI,MAAiC,CAAC,QAAQ,CAAC;IAC7D,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAC7B,MAAM,CAAC,OAAO,CAAC,QAAmC,CAAC,CAAC,MAAM,CACxD,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,6BAA6B,CAAC,QAAQ,CAAC,CAAC,CAAC,CACpD,CACF,CAAC;IACF,OAAO,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAO,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAI,IAAO;IAClC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnD,MAAM,MAAM,GAAI,IAA6B,CAAC,MAAM,CAAC;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO;QACL,GAAG,IAAI;QACP,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACvB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE,OAAO,CAAC,CAAC;YAC1C,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,CAA4B,CAAC;YACtE,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;KACE,CAAC;AACT,CAAC;AAOD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,KAAgB;IAI5C,MAAM,UAAU,GAA2B,EAAE,CAAC;IAC9C,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxC,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAA4B,CAAC;QAClD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,UAA4C,CAAC,CAAC,CAAC,EAAE,CAAC;QACnG,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;gBAC1B,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACb,SAAS;YACX,CAAC;YACD,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ;gBAAE,SAAS;YAC9C,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACjB,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC;gBAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC;QACtE,CAAC;QACD,OAAO,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACpC,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AACvC,CAAC"}
|