@nebulr-group/bridge-cli 0.6.0-beta.6 → 0.6.0-beta.7

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.
Files changed (34) hide show
  1. package/README.md +6 -0
  2. package/dist/commands/guide.command.d.ts +5 -0
  3. package/dist/commands/guide.command.d.ts.map +1 -1
  4. package/dist/commands/guide.command.js +28 -4
  5. package/dist/commands/guide.command.js.map +1 -1
  6. package/dist/commands/journeys.d.ts.map +1 -1
  7. package/dist/commands/journeys.js +4 -2
  8. package/dist/commands/journeys.js.map +1 -1
  9. package/dist/commands/plan.command.d.ts +28 -0
  10. package/dist/commands/plan.command.d.ts.map +1 -1
  11. package/dist/commands/plan.command.js +157 -8
  12. package/dist/commands/plan.command.js.map +1 -1
  13. package/dist/commands/role.command.d.ts.map +1 -1
  14. package/dist/commands/role.command.js +19 -1
  15. package/dist/commands/role.command.js.map +1 -1
  16. package/dist/commands/user.command.d.ts.map +1 -1
  17. package/dist/commands/user.command.js +2 -1
  18. package/dist/commands/user.command.js.map +1 -1
  19. package/dist/prompts/billing/master.md +12 -5
  20. package/dist/prompts/decisions/feature-control.md +41 -20
  21. package/dist/prompts/decisions/going-live.md +1 -1
  22. package/dist/prompts/decisions/login.md +1 -1
  23. package/dist/prompts/decisions/look-and-feel.md +1 -1
  24. package/dist/prompts/decisions/payments.md +13 -10
  25. package/dist/prompts/decisions/roles.md +22 -16
  26. package/dist/prompts/decisions/teams.md +3 -3
  27. package/dist/prompts/fit-together.md +79 -0
  28. package/dist/prompts/flags/master.md +3 -1
  29. package/dist/prompts/guide-coverage.json +6 -1
  30. package/dist/prompts/mcp-tools.json +6 -2
  31. package/dist/prompts/mechanisms.md +22 -12
  32. package/dist/prompts/orientation.md +5 -3
  33. package/dist/prompts/product-decisions.json +2 -2
  34. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  # Roles and permissions — decision guide
2
2
 
3
- Who may do what inside a customer's workspace. A **role** (Owner, Admin, Editor…) is a named bundle of **privileges** (`USER_WRITE`, `INVOICE_APPROVE`…). Every signed-in person has one role per workspace, and their token carries its role key and privileges, so the backend can check them on every request.
3
+ Who may do what inside a customer's workspace. A **role** (Owner, Admin, Editor…) is a named bundle of **privileges** (`USER_WRITE`, `INVOICE_APPROVE`…). Every signed-in person has one role per workspace, and their token carries its role key and privileges, so a flag rule can target them in the browser and on the backend alike.
4
4
 
5
- > Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
5
+ > Read first: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`), how roles, plans, limits and flags fit together. Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
6
6
 
7
7
  ## Which tool answers which question
8
8
 
@@ -10,11 +10,12 @@ Three different questions, three different tools. Keep them apart.
10
10
 
11
11
  | The question | Answered by | Guide |
12
12
  |---|---|---|
13
- | Who, inside a workspace, may do this? | a **role** and its privileges | this one |
14
- | Is this feature on for them at all? | a **flag** | feature-control |
13
+ | Which privileges does each kind of person have? | a **role**, a named bundle of privileges | this one |
14
+ | Is this route, endpoint or feature on for them? | a **flag**, whose rule targets a privilege (preferred) or a role | feature-control |
15
15
  | How much do they get (projects, seats, exports)? | the **plan** | payments |
16
+ | May they change this one record? | the app's own code | — |
16
17
 
17
- A role never stands in for a plan ("Pro users are Admins") and a plan never stands in for a role.
18
+ Access by who someone is goes through a flag rule, never a role check written into the app's code: `privileges contains "REPORTS_VIEW"` keeps working when roles are renamed or reshuffled, and `user.role eq "ADMIN"` is for when the developer means the role itself. A role never stands in for a plan ("Pro users are Admins") and a plan never stands in for a role.
18
19
 
19
20
  ## Start from what is there
20
21
 
@@ -24,7 +25,7 @@ A role never stands in for a plan ("Pro users are Admins") and a plan never stan
24
25
  | One role in full | `get_role` | — |
25
26
  | Privileges that exist | `list_roles` (the `privileges` map) | `bridge privilege list` |
26
27
 
27
- A new app has two roles: **Owner** (`OWNER`, everything, including permanent deletes) and **Admin** (`ADMIN`, manage people, read workspace settings). **Owner is the default role**, which means anyone invited without a role becomes an Owner.
28
+ Read the app's real roles with `list_roles` first: the developer may have renamed, re-scoped or replaced any of them, so never assume what a role can do from its name. In the default setup a new app starts with three roles, a suggestion to reshape rather than a fixed set: **Owner** (`OWNER`, everything, including permanent deletes), **Admin** (`ADMIN`, manage people, read workspace settings) and **Member** (`MEMBER`, signed in and nothing more: no workspace settings, billing or managing people). In the default setup **Member is the default role**. Whoever creates a workspace is always its Owner; anyone added after them without a role named gets the default. Apps created before Member existed keep their own roles, and on many of them Owner is still the default.
28
29
 
29
30
  ## Ask the developer
30
31
 
@@ -32,18 +33,19 @@ Product questions. Ask, wait, never guess.
32
33
 
33
34
  1. **What kinds of people use one customer's workspace?** For example: the person who pays, people who manage the team, people who do the work, people who only look.
34
35
  2. **For each kind, what may they do that others may not?** Ask in the app's own verbs: approve an invoice, delete a project, invite a colleague, change billing.
35
- 3. **Which role should a newly invited person get?** Owner is rarely the right answer.
36
+ 3. **Which role should a newly invited person get?** The current default (see `list_roles`; Member on a new app in the default setup) is a starting point, not an answer.
36
37
  4. **Who may manage the subscription and billing?**
37
38
 
38
39
  ## Decide yourself
39
40
 
40
41
  - **Name privileges after actions, in capitals: `PROJECT_DELETE`, `INVOICE_APPROVE`.** Reason: they read like the existing ones (`USER_READ`, `TENANT_WRITE`) and say exactly what they allow.
41
42
  - **Role keys in capitals too: `EDITOR`, `VIEWER`.** Reason: the token carries the key exactly as written, and a flag rule on `user.role` compares it case for case.
42
- - **In code, check a privilege, not a role name.** Reason: roles get reshaped as the product grows; the privilege on a handler stays true.
43
- - **Keep Owner as the one role with everything.** Never strip it: it is the only role that can delete, and every workspace needs someone who can.
44
- - **Make the lowest sensible role the default** by creating it with the default setting on. Reason: a forgotten role on an invitation then gives too little, not too much.
43
+ - **Control access with a flag rule on a privilege, never a role check in code.** Reason: roles get reshaped as the product grows; a privilege rule stays true, and the rule changes without a release.
44
+ - **Name the full privilege key in a rule** (`privileges contains "REPORTS_VIEW"`). Reason: a rule matches whole keys in the person's list, so `REPORTS_VIEW` does not match `REPORTS_VIEW_ALL`, and a partial key matches nothing.
45
+ - **Keep Owner as the one role with everything.** Never strip it: in the default setup it is the only role that can delete, and every workspace needs someone who can.
46
+ - **Make the lowest sensible role the default**, by creating it with the default setting on or with `set_default_role` / `bridge role set-default <key>` for a role that exists. Reason: a forgotten role on an invitation then gives too little, not too much.
45
47
  - **When changing a role's privileges, send the full list.** Reason: the update replaces the list, and anything left out is revoked.
46
- - **Enforce on the backend; hide buttons in the frontend only as a courtesy.** Reason: anyone can call the API directly.
48
+ - **When the action calls the backend, put the same flag on the endpoint** (NestJS `@RequireFlag`). Reason: the backend reads the same rule, and anyone can call the API directly.
47
49
 
48
50
  ## Do it
49
51
 
@@ -51,22 +53,26 @@ Product questions. Ask, wait, never guess.
51
53
  |---|---|---|
52
54
  | Add a privilege | — | `bridge privilege create --key PROJECT_DELETE --description …` |
53
55
  | Create a role | `create_role` | `bridge role create --key EDITOR --name Editor --privileges PROJECT_READ,PROJECT_WRITE` |
54
- | Create it as the new default | `create_role` (`isDefault: true`) | `bridge role create --key MEMBER --name Member --privileges … --is-default` |
56
+ | Create it as the new default | `create_role` (`isDefault: true`) | `bridge role create --key VIEWER --name Viewer --privileges … --is-default` |
57
+ | Make an existing role the default | `set_default_role` (`key`), or `update_role` (`isDefault: true`) | `bridge role set-default EDITOR`, or `bridge role update --key EDITOR --is-default` |
55
58
  | Change a role's privileges | `update_role` (full list) | `bridge role update --key EDITOR --privileges …` (full list) |
56
59
  | Give a person a role | `update_user` | `bridge user update --email … --role EDITOR --tenant-id …` |
57
- | Code for the framework | `get_integration_guide` (topic `team`) | `bridge guide nestjs` (backend checks), `bridge guide svelte team` |
60
+ | Gate a route, endpoint or feature on a privilege | `create_feature_flag` (rule on `privileges`) | `bridge flag create --key reports --state on-with-rule --rule '<json>'` |
61
+ | Code for the framework | `get_integration_guide` (topic `feature-flags` or `team`) | `bridge guide flags --framework nestjs`, `bridge guide svelte team` |
58
62
 
59
- On the NestJS backend the check is a decorator on the handler: `@RequirePrivilege('PROJECT_DELETE')`, or `@RequireRole('ADMIN')` when the developer really means a role.
63
+ On the NestJS backend the flag goes on the handler: `@RequireFlag('reports')`, whose rule is `privileges contains "REPORTS_VIEW"`. A person without it gets `403 FEATURE_NOT_PERMITTED`. `@RequirePrivilege` still exists for a check that must never change at runtime; it is the exception, and a hard-coded role check is never the answer.
60
64
 
61
65
  Setting the default on one role clears it from the previous default.
62
66
 
63
67
  ## Prove it
64
68
 
65
69
  1. `list_roles` / `bridge role list` shows each role with exactly the privileges agreed, and the default where the developer wanted it.
66
- 2. Call a protected backend route as a user whose role lacks the privilege: it must answer 403. Then with one who has it: it must succeed.
70
+ 2. `evaluate_feature_flag` / `bridge flag eval reports --identity u1 --attribute privileges='["REPORTS_VIEW"]'`, then without the privilege: the answers differ.
71
+ 3. Call the flag-gated backend route as a user whose role lacks the privilege: it must answer 403. Then with one who has it: it must succeed.
67
72
 
68
73
  ## Where this connects
69
74
 
70
75
  - **Teams:** the role an invited person gets.
71
- - **Feature control:** a flag can target `user.role`, but "may this person do it" is still a privilege check on the backend.
76
+ - **Fit together:** roles, plans, limits and flags side by side (`bridge guide fit-together`).
77
+ - **Feature control:** the flag rule that targets a privilege (preferred) or `user.role`.
72
78
  - **Payments:** who may change the plan.
@@ -2,7 +2,7 @@
2
2
 
3
3
  How customers are grouped in the developer's app. In Bridge every signed-in person belongs to a **workspace** (the API and the CLI call it a tenant). A workspace is one customer: it holds the people, their roles, and the plan the customer pays for.
4
4
 
5
- > Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
5
+ > Read first: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`), how roles, plans, limits and flags fit together. Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
6
6
 
7
7
  ## Start from what is there
8
8
 
@@ -19,7 +19,7 @@ Product questions. Ask, wait, never guess.
19
19
 
20
20
  1. **Is the app for single people or for teams?** With teams, a customer invites colleagues into a shared workspace. For single people, each person simply has a workspace of their own and never sees the word.
21
21
  2. **How does someone get in the first time?** Either they sign up and get a new workspace of their own, or they can only join by invitation from someone already inside.
22
- 3. **When someone is invited, which role should they get?** See the **roles** guide: on a new app the default role is Owner, so an invitee without a role becomes an Owner.
22
+ 3. **When someone is invited, which role should they get?** Read the app's real roles with `list_roles` first and offer those. See the **roles** guide: in the default setup a new app's default role is Member, while older apps often still default to Owner.
23
23
  4. **Does the number of people per workspace depend on the plan?** ("1 user on Free, 10 on Pro.") If yes, that is a seat limit: see the **payments** guide.
24
24
  5. **Can one person belong to several workspaces?** Bridge supports it and shows a workspace picker at sign-in; the question is whether the developer wants to offer it in their product.
25
25
 
@@ -55,6 +55,6 @@ Pass the role key the developer chose on every invitation you make on their beha
55
55
 
56
56
  ## Where this connects
57
57
 
58
- - **Roles:** what each person in a workspace may do.
58
+ - **Roles:** the privileges each kind of person has, which flag rules then target.
59
59
  - **Payments:** a plan belongs to the workspace, not to a person; seats are a plan limit.
60
60
  - **Login:** whether sign-up is open at all.
@@ -0,0 +1,79 @@
1
+ # How roles, plans, limits and flags fit together
2
+
3
+ Read this before any decision guide that touches who gets what. Every other guide builds on it. MCP: resource `bridge://guides/fit-together`. CLI: `bridge guide fit-together`.
4
+
5
+ ## The rule
6
+
7
+ Features should be controlled with feature flags. A flag can switch a route, an API endpoint, a feature or a single piece of code; its rules change without a release and update live when someone's role or plan changes. If a plan sells the feature, include it on the plan as well. This is valuable because the plan is where the customer sees what they are buying: the pricing table, the upgrade dialog and the "not on your plan" reason all read the plan's list, and the flag's rule points at that same list, so changing what Pro includes is one edit in one place.
8
+
9
+ ## One question, one tool
10
+
11
+ | The question | The tool | How |
12
+ |---|---|---|
13
+ | Is this route, API endpoint, feature or piece of code on for them? | a **flag** | The code asks for the flag by key; the flag's rule decides |
14
+ | Does their plan sell it? | the plan's **features list**, read by the flag's rule | `bridge plan feature add pro analytics`, then the rule `bridge:billing.entitlement.analytics eq true` |
15
+ | How many do they get (projects, exports, seats)? | a **plan limit** | `bridge plan quota set pro --metric exports --limit 100 --policy hard` |
16
+ | Who is this person, and may they do this kind of thing? | a flag rule on a **privilege** (preferred) or a **role** | `privileges contains "REPORTS_VIEW"`, or `user.role eq "ADMIN"` |
17
+ | May this person change this one record? | the app's own code | "Only the author edits their post" |
18
+
19
+ The flag's rule never names plans. It points at `bridge:billing.entitlement.<feature>`, so when Pro gains or loses a feature, the plan changes and no rule has to follow.
20
+
21
+ ## Where a flag is not the tool
22
+
23
+ - **Numbers are plan limits.** "100 exports a month" or "10 projects" is a quota on the plan, not a flag. Bridge refuses at the limit and the upgrade dialog explains it.
24
+ - **Permission on one specific record stays in app code.** Whether this person may edit *this* invoice depends on data only the app has (who wrote it, which team owns it). The flag decides whether invoice editing exists for them at all; the app decides the record.
25
+ - **Checking a plan feature without a flag is the exception.** `@RequireEntitlement('analytics')` on the backend and `<Entitled to="analytics">` or `$entitlements.can('analytics')` in the UI read the plan's list directly. They work, and they are for the rare case where the developer asks for no flag. Use a flag by default: it can also carry a rollout, a trial group or a one-customer exception later, without a code change.
26
+
27
+ ## Access by who someone is
28
+
29
+ Control it with a flag rule, never with a role check written into the app's code.
30
+
31
+ - **Prefer a privilege rule** (`privileges contains "REPORTS_VIEW"`) over a role rule (`user.role eq "ADMIN"`). A privilege rule keeps working when roles are renamed or reshuffled. `contains` on `privileges` means the person holds exactly that key: `REPORTS_VIEW` does not match `REPORTS_VIEW_ALL`.
32
+ - **A role rule is fine when the developer means the role itself.** Use the role key exactly as `list_roles` shows it; `ADMIN` and `admin` are different.
33
+ - **An admin-only page is a flag route rule, not a check on the page.** In SvelteKit: `{ match: '/admin/*', featureFlag: 'admin-area', redirectTo: '/' }`, with the flag `admin-area` ruled on a privilege. On the NestJS backend: `@RequireFlag('admin-area')` on the handler. Both read the same rule.
34
+ - **Roles are the app's own.** What a role can do is only ever "in the default setup". Read the app's real roles and privileges with `list_roles` / `bridge role list` before writing any rule, and never assume what a role grants from its name. In the default setup a new app has Owner, Admin and Member, and Member is the default for everyone after the first person in a workspace.
35
+
36
+ ## The backend sees the same rule
37
+
38
+ A flag rule on a role, a privilege, the plan or a plan feature gives the same answer in the NestJS backend as in the browser, with no wiring: the plugin fills those in from the verified sign-in and from Bridge's own billing records. Nothing the client sends is used for them. Plan-limit numbers (`bridge:billing.quota.*`) are the one thing a server-side rule cannot see; limits are enforced by the plan-limit decorator anyway.
39
+
40
+ ## Limits: count once, where the action happens
41
+
42
+ Ask the developer first: **"Does this action call your server?"** Then:
43
+
44
+ - **Yes, it calls the backend:** the backend counts it. One decorator on the handler (`@RequireQuota('exports')` in NestJS) refuses at the limit with `402` and records the use after a successful request. The frontend only shows the count and the upgrade dialog; it does not report the same metric.
45
+ - **No, it happens in the browser** (local-first, data on the device, no server of the developer's own): the browser counts it. `bridge.usage.report('exports')` for something that happened, `bridge.usage.set('projects', n)` for how many exist now, and `<QuotaGate metric="exports">` around the button so it stops at the limit. This is a first-class way to run limits. It trusts the browser: someone who edits the page's code could report less than they use.
46
+
47
+ Never count one metric in both places: it is counted twice. Never describe browser counting as a lesser or temporary option; ask the question above and follow the answer.
48
+
49
+ Counter or gauge: 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.
50
+
51
+ ## What people see when something is off
52
+
53
+ A flag that is off says why, and the app shows each reason differently:
54
+
55
+ | Why it is off | In the browser | From the backend |
56
+ |---|---|---|
57
+ | Not on their plan | The upgrade dialog, naming the plans that include it | `402 FEATURE_NOT_IN_PLAN` |
58
+ | Their role or privileges | Hidden, or a line telling them to ask an admin | `403 FEATURE_NOT_PERMITTED` |
59
+ | Switched off | Hidden | `403 FEATURE_OFF` |
60
+
61
+ No upgrade dialog opens by itself. It opens when someone opens a gated route, clicks something gated (SvelteKit: `<FeatureFlag key="analytics" upgrade>` renders an "Upgrade to use this" button), or when the backend refuses with a `402`. A page that only renders a hidden feature opens nothing.
62
+
63
+ ## Worked example
64
+
65
+ "Analytics on Pro. Only people who manage the workspace see settings. 100 exports a month on Pro."
66
+
67
+ 1. `bridge plan feature add pro analytics --name "Analytics"`, and a flag `analytics` with the rule `bridge:billing.entitlement.analytics eq true`.
68
+ 2. `bridge role list` first. Then a flag `workspace-settings` with a privilege rule, e.g. `privileges contains "TENANT_WRITE"` if that is the privilege the app's roles use for it, and a route rule on `/settings/*`.
69
+ 3. Ask whether exporting calls the developer's server. If it does: `bridge plan quota set pro --metric exports --limit 100 --policy hard` and `@RequireQuota('exports')` on the export handler. If it does not: the same quota, `bridge.usage.report('exports')` in the browser and `<QuotaGate metric="exports">` around the button.
70
+
71
+ ## Where each piece is decided
72
+
73
+ | Area | Decision guide |
74
+ |---|---|
75
+ | Flags and their rules | `bridge guide decision feature-control` |
76
+ | Roles and privileges | `bridge guide decision roles` |
77
+ | Plans, features and limits | `bridge guide decision payments` |
78
+ | Who is in a workspace | `bridge guide decision teams` |
79
+ | How limits, the upgrade dialog and page customisation work | `bridge guide mechanisms` |
@@ -2,6 +2,8 @@
2
2
 
3
3
  You are integrating **Bridge Feature Flags** into a user's application — the flags-only path. No auth scaffolding, no billing components. If the user also wants auth or billing, run `bridge guide` (auth master) or `bridge guide billing` afterwards.
4
4
 
5
+ Flags are the standard way to control a route, an API endpoint, a feature or a piece of code. A feature a plan sells is listed on the plan and the flag's rule points at `bridge:billing.entitlement.<feature>`, never at plan names; access by who someone is is a rule on a privilege (preferred) or a role, never a role check in code. `bridge guide fit-together` has the whole rule; `bridge guide decision feature-control` has the questions to ask.
6
+
5
7
  This master prompt orchestrates discovery, confirmation, and verification. The **actual install commands, provider shapes, and runnable code snippets live in the per-framework prompts** that ship in each plugin repo's `mcp/` folder. This master does the planning; the per-framework prompts do the wiring.
6
8
 
7
9
  ## Where per-framework prompts live
@@ -212,4 +214,4 @@ After delivering the message, the flags integration is complete.
212
214
 
213
215
  **Frontend vs backend:** The frontend SDK evaluates flags from a local cache hydrated at boot. The backend SDK (`mode: 'backend'`) evaluates per-request and requires an explicit `identity` for any rule using `rolloutPct < 100` — no anonymous bucketing.
214
216
 
215
- **Flags without Bridge Auth:** Flags work standalone. Pass your own `identity` and `attributes` when `identitySource` is "your own user model". If Bridge Auth is already installed, `role` and `plan` merge into the eval context automatically via the auth attribute provider wired by the per-framework guide.
217
+ **Flags without Bridge Auth:** Flags work standalone. Pass your own `identity` and `attributes` when `identitySource` is "your own user model". If Bridge Auth is already installed, `user.role`, `privileges`, `tenant.plan` and the `bridge:billing.*` attributes (including `bridge:billing.entitlement.<feature>`) fill the eval context automatically, in the browser and on the NestJS backend alike.
@@ -1,6 +1,11 @@
1
1
  {
2
- "_comment": "Which per-framework guides exist, keyed by the feature names `bridge guide <framework> <feature>` takes (the file stems under mcp/ in each plugin repo). The one copy: `bridge guide list` reads it from the package, and the Bridge MCP server reads it from GitHub (bridge-cli/main/bridge-cli/prompts/guide-coverage.json). Update it when a plugin adds or removes an mcp/*-prompt.md.",
2
+ "_comment": "Which per-framework guides exist, keyed by the feature names `bridge guide <framework> <feature>` takes (the file stems under mcp/ in each plugin repo). The one copy: `bridge guide list` reads it from the package, and the Bridge MCP server reads it from GitHub (bridge-cli/main/bridge-cli/prompts/guide-coverage.json). Update it when a plugin adds or removes an mcp/*-prompt.md. `topics` lists the framework-agnostic guides bundled here (`bridge guide <topic>`).",
3
3
  "aliases": { "flags": "feature-flags" },
4
+ "topics": {
5
+ "orientation": "What Bridge does: a short map to relay to the developer (bridge guide orientation)",
6
+ "fit-together": "How roles, plans, limits and flags fit together: the rule every decision guide builds on (bridge guide fit-together)",
7
+ "mechanisms": "How limits, the upgrade dialog and page customisation work (bridge guide mechanisms)"
8
+ },
4
9
  "frameworks": {
5
10
  "svelte": ["integration", "sdk-auth", "feature-flags", "billing", "team", "branding"],
6
11
  "react": ["integration", "sdk-auth", "feature-flags", "billing", "team", "branding"],
@@ -3,10 +3,11 @@
3
3
  "The names of the Bridge MCP server's tools, and nothing else. The bundled guides are tested against this list: a guide that names a tool not in it fails src/__tests__/guide-decisions.test.ts.",
4
4
  "Source: the `bridgeTools` registry in bridge-api, microservices/account/nebulr-api/mcp/tools/register.ts.",
5
5
  "Generated, never edited by hand: in bridge-api run `bun scripts/generate-mcp-tools-json.ts > <bridge-cli>/bridge-cli/prompts/mcp-tools.json` (in the bridge-api container).",
6
- "The count must equal the number of entries in `bridgeTools` (67 at the source below). bridge-api's guide-currency.spec.ts fails when this file and the registry disagree."
6
+ "The count must equal the number of entries in `bridgeTools` (71 at the source below). bridge-api's guide-currency.spec.ts fails when this file and the registry disagree."
7
7
  ],
8
- "source": "bridge-api feature/mcp-journey 26792b4",
8
+ "source": "bridge-api feature/mcp-journey e9762f2",
9
9
  "tools": [
10
+ "add_plan_feature",
10
11
  "add_redirect_uri",
11
12
  "apply_plan",
12
13
  "clear_flag_schedule",
@@ -50,13 +51,16 @@
50
51
  "list_roles",
51
52
  "list_tenants",
52
53
  "list_users",
54
+ "remove_plan_feature",
53
55
  "remove_plan_price",
54
56
  "remove_plan_quota",
55
57
  "remove_redirect_uri",
56
58
  "remove_user",
57
59
  "revoke_api_token",
58
60
  "scaffold_flags_integration",
61
+ "set_default_role",
59
62
  "set_flag_schedule",
63
+ "set_plan_features",
60
64
  "set_plan_price",
61
65
  "set_plan_quota",
62
66
  "setup_communication",
@@ -2,6 +2,8 @@
2
2
 
3
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
4
 
5
+ Who gets which feature, and how plans, roles, limits and flags divide the work, is on its own page: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`).
6
+
5
7
  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
8
 
7
9
  ## The whole integration
@@ -98,9 +100,14 @@ export class ExportsController {
98
100
  | `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
101
  | `VITE_BRIDGE_DEBUG` | `BRIDGE_DEBUG` | `true` for console logging |
100
102
 
101
- ## 1. The server decides; the client decorates
103
+ ## 1. Decide once, where the action happens
104
+
105
+ Ask the developer first: **does this action call your server?**
102
106
 
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.
107
+ - **It calls your backend:** the server decides; the client decorates. Anyone can call your API with curl, so the limit or the flag is enforced on the backend handler. Everything the frontend shows about it — the upgrade dialog, a disabled button, a hidden panel — explains or anticipates a decision the server already makes, and does not count the same metric again.
108
+ - **It happens in the browser** and never reaches a server of yours (local-first, data on the device): the browser counts it and gates the button (section 5). That is a first-class setup.
109
+
110
+ Never both for one metric: it would be counted twice.
104
111
 
105
112
  ## 2. A POST increments the limit
106
113
 
@@ -146,9 +153,9 @@ remove() { /* … */ }
146
153
 
147
154
  `bridge plan quota list` shows every metric the app's plans limit, with its kind.
148
155
 
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*.
156
+ **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. `@RequireQuota` is for a *limit*; `@RequireEntitlement` checks a plan *feature* (`analytics`, `sso`) without a flag, which is the exception (next paragraph).
150
157
 
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.
158
+ **A plan feature goes in the plan's features list.** `bridge plan feature add pro analytics --name "Analytics"` makes `analytics` true on `pro`, and false on every other plan. The pricing table and the upgrade dialog name it from the same list. Control the feature with a flag whose rule is `bridge:billing.entitlement.analytics eq true`, so changing what Pro sells is one edit on the plan and no flag rule has to follow. Checking it directly, without a flag, is the exception: `@RequireEntitlement('analytics')` on the backend and `<Entitled to="analytics">` in the UI. A hard quota with limit 1 that nothing counts still works the same way; it is the older way to do this. `app_active` is always there: true while the workspace's subscription is active, trialing, past due or cancelling at period end.
152
159
 
153
160
  ## 4. Three ways to handle a limit in the UI
154
161
 
@@ -157,12 +164,12 @@ Pick the lowest level that does the job. Each is optional; level 0 is on without
157
164
  | Level | What the page writes | What the user sees |
158
165
  |---|---|---|
159
166
  | **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 |
167
+ | **1 — one component** | `<QuotaGate metric="tickets">…</QuotaGate>` around the button; `<FeatureFlag key="analytics" upgrade>…</FeatureFlag>` around a paid feature | The button is disabled at a known hard cap with an upgrade line beside it; the paid feature shows on a plan that includes it, and elsewhere an "Upgrade to use this" button that opens the dialog when clicked |
161
168
  | **2 — your own UI** | `useQuota('tickets')` and `$entitlements.can('analytics')` | Whatever you build from the live numbers |
162
169
 
163
170
  ```svelte
164
171
  <script lang="ts">
165
- import { QuotaGate, Entitled, useQuota } from '@nebulr-group/bridge-svelte';
172
+ import { QuotaGate, FeatureFlag, useQuota } from '@nebulr-group/bridge-svelte';
166
173
  const tickets = useQuota('tickets');
167
174
  </script>
168
175
 
@@ -171,10 +178,10 @@ Pick the lowest level that does the job. Each is optional; level 0 is on without
171
178
  <button onclick={createTicket}>New ticket</button>
172
179
  </QuotaGate>
173
180
 
174
- <Entitled to="analytics">
181
+ <!-- the flag's rule: bridge:billing.entitlement.analytics eq true -->
182
+ <FeatureFlag key="analytics" defaultValue={false} upgrade>
175
183
  <a href="/analytics">Analytics</a>
176
- {#snippet fallback()}<a href="/subscription">Upgrade for analytics</a>{/snippet}
177
- </Entitled>
184
+ </FeatureFlag>
178
185
 
179
186
  <!-- level 2 -->
180
187
  {#if tickets.loading}
@@ -186,13 +193,14 @@ Pick the lowest level that does the job. Each is optional; level 0 is on without
186
193
  {/if}
187
194
  ```
188
195
 
196
+ - No upgrade dialog opens by itself: it opens on a `402` from your backend, when someone opens a route whose flag is off because of the plan, or when they click an upgrade prompt. A page that only renders a hidden feature opens nothing.
189
197
  - "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
198
  - 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
199
  - 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
200
 
193
- ## 5. Usage reported from the browser trusts the client
201
+ ## 5. Counting in the browser
194
202
 
195
- An app with no backend that sees the action — local-first, mobile, data on the device — can report usage from the frontend:
203
+ When the action happens in the browser and never reaches a server of yours — local-first, mobile, data on the device — the frontend counts it:
196
204
 
197
205
  ```ts
198
206
  import { bridge } from '@nebulr-group/bridge-svelte';
@@ -201,7 +209,9 @@ bridge.usage.report('exports'); // a counter: it happened
201
209
  await bridge.usage.set('projects', projects.length); // a gauge: how many exist now
202
210
  ```
203
211
 
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.
212
+ Put `<QuotaGate metric="exports">` around the button (section 4, level 1) so it stops at the limit, and the upgrade line sells the next plan.
213
+
214
+ This is a first-class way to run limits. It is trusted-client usage: it trusts the browser, so someone who edits the page's code could report less than they use. When the action does call your backend, count it there instead (section 2), never in both places.
205
215
 
206
216
  ## 6. Four levels of customising Bridge's pages
207
217
 
@@ -8,9 +8,9 @@ Bridge runs everything between signup and invoice for your app, so you don't bui
8
8
 
9
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
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?*
11
+ - **Roles and permissions.** Inside a workspace, each person's role carries privileges, and feature flags use them to decide who gets which page, endpoint or feature, the same in the browser and on your backend. *Who are the different kinds of people in a customer's workspace?*
12
+ - **Payments and plans.** Plans with prices, trials, limits ("3 projects on Free, 50 on Pro") and the features each one sells, checkout through your own Stripe account, and an upgrade prompt when someone hits a limit or a feature their plan lacks. *Want to set up a free tier and a paid plan?*
13
+ - **Feature control.** Feature flags switch any page, API endpoint or feature for everyone, a percentage of users, the plans that sell it, the people allowed to use it, or one customer, without a deploy. *Is there a feature you want to roll out carefully or sell on a higher plan?*
14
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
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
16
 
@@ -32,6 +32,8 @@ Each area has a decision guide: the questions to ask the developer, and the defa
32
32
 
33
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
34
 
35
+ How roles, plans, limits and flags fit together, the rule every decision guide builds on: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`). Read it before setting up any of them.
36
+
35
37
  How limits, upgrade prompts and page customisation actually work, for every area: `bridge guide mechanisms`.
36
38
 
37
39
  Two rules hold in every guide:
@@ -5,7 +5,7 @@
5
5
  "Source: PRODUCT_DECISIONS in bridge-api, microservices/account/nebulr-api/mcp/tools/product-decisions.ts.",
6
6
  "Generated, never edited by hand: in bridge-api run `bun scripts/generate-product-decisions-json.ts > <bridge-cli>/bridge-cli/prompts/product-decisions.json` (in the bridge-api container). guide-currency.spec.ts fails when the two disagree."
7
7
  ],
8
- "source": "bridge-api feature/mcp-journey ecfa0a2",
8
+ "source": "bridge-api feature/mcp-journey e9762f2",
9
9
  "decisions": [
10
10
  {
11
11
  "tool": "create_plan",
@@ -142,7 +142,7 @@
142
142
  "field": "role",
143
143
  "guide": "teams",
144
144
  "enforcement": "ask",
145
- "question": "Which role should this person get? (a role key; the app default is often Owner, rarely the right answer)",
145
+ "question": "Which role should this person get? (a role key from list_roles; the default role is only a starting suggestion)",
146
146
  "type": "string"
147
147
  },
148
148
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulr-group/bridge-cli",
3
- "version": "0.6.0-beta.6",
3
+ "version": "0.6.0-beta.7",
4
4
  "description": "Bridge platform CLI for AI coding agents and developers",
5
5
  "author": "Nebulr Group",
6
6
  "license": "MIT",