@nebulr-group/bridge-cli 0.6.5 → 1.0.0-alpha.0

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 (293) hide show
  1. package/README.md +93 -460
  2. package/dist/api/accounts.d.ts +69 -0
  3. package/dist/api/accounts.d.ts.map +1 -0
  4. package/dist/api/accounts.js +42 -0
  5. package/dist/api/accounts.js.map +1 -0
  6. package/dist/api/apps.d.ts +50 -0
  7. package/dist/api/apps.d.ts.map +1 -0
  8. package/dist/api/apps.js +62 -0
  9. package/dist/api/apps.js.map +1 -0
  10. package/dist/api/billing.d.ts +131 -0
  11. package/dist/api/billing.d.ts.map +1 -0
  12. package/dist/api/billing.js +42 -0
  13. package/dist/api/billing.js.map +1 -0
  14. package/dist/api/flags.d.ts +73 -0
  15. package/dist/api/flags.d.ts.map +1 -0
  16. package/dist/api/flags.js +108 -0
  17. package/dist/api/flags.js.map +1 -0
  18. package/dist/api/keys.d.ts +48 -0
  19. package/dist/api/keys.d.ts.map +1 -0
  20. package/dist/api/keys.js +87 -0
  21. package/dist/api/keys.js.map +1 -0
  22. package/dist/auth/browser-open.d.ts +21 -4
  23. package/dist/auth/browser-open.d.ts.map +1 -1
  24. package/dist/auth/browser-open.js +45 -65
  25. package/dist/auth/browser-open.js.map +1 -1
  26. package/dist/auth/lock.d.ts +8 -0
  27. package/dist/auth/lock.d.ts.map +1 -0
  28. package/dist/auth/lock.js +43 -0
  29. package/dist/auth/lock.js.map +1 -0
  30. package/dist/auth/loopback.d.ts +8 -12
  31. package/dist/auth/loopback.d.ts.map +1 -1
  32. package/dist/auth/loopback.js +40 -45
  33. package/dist/auth/loopback.js.map +1 -1
  34. package/dist/auth/session.d.ts +61 -0
  35. package/dist/auth/session.d.ts.map +1 -0
  36. package/dist/auth/session.js +142 -0
  37. package/dist/auth/session.js.map +1 -0
  38. package/dist/auth/sign-in.d.ts +67 -0
  39. package/dist/auth/sign-in.d.ts.map +1 -0
  40. package/dist/auth/sign-in.js +113 -0
  41. package/dist/auth/sign-in.js.map +1 -0
  42. package/dist/auth/store.d.ts +89 -0
  43. package/dist/auth/store.d.ts.map +1 -0
  44. package/dist/auth/store.js +214 -0
  45. package/dist/auth/store.js.map +1 -0
  46. package/dist/bin.js +0 -2
  47. package/dist/bin.js.map +1 -1
  48. package/dist/cli.d.ts +1 -2
  49. package/dist/cli.d.ts.map +1 -1
  50. package/dist/cli.js +13 -17
  51. package/dist/cli.js.map +1 -1
  52. package/dist/client.d.ts +31 -0
  53. package/dist/client.d.ts.map +1 -0
  54. package/dist/client.js +67 -0
  55. package/dist/client.js.map +1 -0
  56. package/dist/commands/accounts.command.d.ts +4 -0
  57. package/dist/commands/accounts.command.d.ts.map +1 -0
  58. package/dist/commands/accounts.command.js +116 -0
  59. package/dist/commands/accounts.command.js.map +1 -0
  60. package/dist/commands/app.command.d.ts +3 -2
  61. package/dist/commands/app.command.d.ts.map +1 -1
  62. package/dist/commands/app.command.js +132 -233
  63. package/dist/commands/app.command.js.map +1 -1
  64. package/dist/commands/auth.command.d.ts +2 -12
  65. package/dist/commands/auth.command.d.ts.map +1 -1
  66. package/dist/commands/auth.command.js +147 -20
  67. package/dist/commands/auth.command.js.map +1 -1
  68. package/dist/commands/billing.command.d.ts +4 -0
  69. package/dist/commands/billing.command.d.ts.map +1 -0
  70. package/dist/commands/billing.command.js +185 -0
  71. package/dist/commands/billing.command.js.map +1 -0
  72. package/dist/commands/check.command.d.ts +2 -1
  73. package/dist/commands/check.command.d.ts.map +1 -1
  74. package/dist/commands/check.command.js +11 -14
  75. package/dist/commands/check.command.js.map +1 -1
  76. package/dist/commands/context.d.ts +58 -0
  77. package/dist/commands/context.d.ts.map +1 -0
  78. package/dist/commands/context.js +78 -0
  79. package/dist/commands/context.js.map +1 -0
  80. package/dist/commands/flag.command.d.ts +3 -62
  81. package/dist/commands/flag.command.d.ts.map +1 -1
  82. package/dist/commands/flag.command.js +181 -864
  83. package/dist/commands/flag.command.js.map +1 -1
  84. package/dist/commands/key.command.d.ts +4 -0
  85. package/dist/commands/key.command.d.ts.map +1 -0
  86. package/dist/commands/key.command.js +44 -0
  87. package/dist/commands/key.command.js.map +1 -0
  88. package/dist/commands/plan.command.d.ts +3 -203
  89. package/dist/commands/plan.command.d.ts.map +1 -1
  90. package/dist/commands/plan.command.js +200 -759
  91. package/dist/commands/plan.command.js.map +1 -1
  92. package/dist/config.d.ts +19 -101
  93. package/dist/config.d.ts.map +1 -1
  94. package/dist/config.js +51 -364
  95. package/dist/config.js.map +1 -1
  96. package/dist/core/discovery.d.ts +27 -0
  97. package/dist/core/discovery.d.ts.map +1 -0
  98. package/dist/core/discovery.js +97 -0
  99. package/dist/core/discovery.js.map +1 -0
  100. package/dist/core/errors.d.ts +26 -0
  101. package/dist/core/errors.d.ts.map +1 -0
  102. package/dist/core/errors.js +33 -0
  103. package/dist/core/errors.js.map +1 -0
  104. package/dist/core/http.d.ts +23 -0
  105. package/dist/core/http.d.ts.map +1 -0
  106. package/dist/core/http.js +153 -0
  107. package/dist/core/http.js.map +1 -0
  108. package/dist/core/private-files.d.ts +6 -0
  109. package/dist/core/private-files.d.ts.map +1 -0
  110. package/dist/core/private-files.js +96 -0
  111. package/dist/core/private-files.js.map +1 -0
  112. package/dist/core/routes.d.ts +94 -0
  113. package/dist/core/routes.d.ts.map +1 -0
  114. package/dist/core/routes.js +91 -0
  115. package/dist/core/routes.js.map +1 -0
  116. package/dist/core/urls.d.ts +15 -0
  117. package/dist/core/urls.d.ts.map +1 -0
  118. package/dist/core/urls.js +55 -0
  119. package/dist/core/urls.js.map +1 -0
  120. package/dist/gate-rules.d.ts +1 -2
  121. package/dist/gate-rules.d.ts.map +1 -1
  122. package/dist/gate-rules.js +4 -5
  123. package/dist/gate-rules.js.map +1 -1
  124. package/dist/index.d.ts +23 -3
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +18 -3
  127. package/dist/index.js.map +1 -1
  128. package/dist/io.d.ts +21 -0
  129. package/dist/io.d.ts.map +1 -0
  130. package/dist/io.js +59 -0
  131. package/dist/io.js.map +1 -0
  132. package/dist/money.d.ts +42 -0
  133. package/dist/money.d.ts.map +1 -0
  134. package/dist/money.js +112 -0
  135. package/dist/money.js.map +1 -0
  136. package/dist/program.d.ts +5 -24
  137. package/dist/program.d.ts.map +1 -1
  138. package/dist/program.js +25 -113
  139. package/dist/program.js.map +1 -1
  140. package/package.json +7 -8
  141. package/dist/auth/api-client.d.ts +0 -50
  142. package/dist/auth/api-client.d.ts.map +0 -1
  143. package/dist/auth/api-client.js +0 -109
  144. package/dist/auth/api-client.js.map +0 -1
  145. package/dist/auth/relative-time.d.ts +0 -2
  146. package/dist/auth/relative-time.d.ts.map +0 -1
  147. package/dist/auth/relative-time.js +0 -27
  148. package/dist/auth/relative-time.js.map +0 -1
  149. package/dist/auth/scopes.d.ts +0 -30
  150. package/dist/auth/scopes.d.ts.map +0 -1
  151. package/dist/auth/scopes.js +0 -39
  152. package/dist/auth/scopes.js.map +0 -1
  153. package/dist/auth/workspace-client.d.ts +0 -70
  154. package/dist/auth/workspace-client.d.ts.map +0 -1
  155. package/dist/auth/workspace-client.js +0 -105
  156. package/dist/auth/workspace-client.js.map +0 -1
  157. package/dist/commands/auth/config.command.d.ts +0 -8
  158. package/dist/commands/auth/config.command.d.ts.map +0 -1
  159. package/dist/commands/auth/config.command.js +0 -60
  160. package/dist/commands/auth/config.command.js.map +0 -1
  161. package/dist/commands/auth/login.command.d.ts +0 -46
  162. package/dist/commands/auth/login.command.d.ts.map +0 -1
  163. package/dist/commands/auth/login.command.js +0 -166
  164. package/dist/commands/auth/login.command.js.map +0 -1
  165. package/dist/commands/auth/logout.command.d.ts +0 -21
  166. package/dist/commands/auth/logout.command.d.ts.map +0 -1
  167. package/dist/commands/auth/logout.command.js +0 -97
  168. package/dist/commands/auth/logout.command.js.map +0 -1
  169. package/dist/commands/auth/methods.command.d.ts +0 -68
  170. package/dist/commands/auth/methods.command.d.ts.map +0 -1
  171. package/dist/commands/auth/methods.command.js +0 -91
  172. package/dist/commands/auth/methods.command.js.map +0 -1
  173. package/dist/commands/auth/status.command.d.ts +0 -18
  174. package/dist/commands/auth/status.command.d.ts.map +0 -1
  175. package/dist/commands/auth/status.command.js +0 -104
  176. package/dist/commands/auth/status.command.js.map +0 -1
  177. package/dist/commands/auth/use.command.d.ts +0 -17
  178. package/dist/commands/auth/use.command.d.ts.map +0 -1
  179. package/dist/commands/auth/use.command.js +0 -47
  180. package/dist/commands/auth/use.command.js.map +0 -1
  181. package/dist/commands/branding.command.d.ts +0 -3
  182. package/dist/commands/branding.command.d.ts.map +0 -1
  183. package/dist/commands/branding.command.js +0 -42
  184. package/dist/commands/branding.command.js.map +0 -1
  185. package/dist/commands/event.command.d.ts +0 -3
  186. package/dist/commands/event.command.d.ts.map +0 -1
  187. package/dist/commands/event.command.js +0 -27
  188. package/dist/commands/event.command.js.map +0 -1
  189. package/dist/commands/flag-init.command.d.ts +0 -13
  190. package/dist/commands/flag-init.command.d.ts.map +0 -1
  191. package/dist/commands/flag-init.command.js +0 -463
  192. package/dist/commands/flag-init.command.js.map +0 -1
  193. package/dist/commands/guide.command.d.ts +0 -61
  194. package/dist/commands/guide.command.d.ts.map +0 -1
  195. package/dist/commands/guide.command.js +0 -655
  196. package/dist/commands/guide.command.js.map +0 -1
  197. package/dist/commands/info.command.d.ts +0 -3
  198. package/dist/commands/info.command.d.ts.map +0 -1
  199. package/dist/commands/info.command.js +0 -70
  200. package/dist/commands/info.command.js.map +0 -1
  201. package/dist/commands/integration.command.d.ts +0 -35
  202. package/dist/commands/integration.command.d.ts.map +0 -1
  203. package/dist/commands/integration.command.js +0 -290
  204. package/dist/commands/integration.command.js.map +0 -1
  205. package/dist/commands/journeys.d.ts +0 -43
  206. package/dist/commands/journeys.d.ts.map +0 -1
  207. package/dist/commands/journeys.js +0 -221
  208. package/dist/commands/journeys.js.map +0 -1
  209. package/dist/commands/ops.command.d.ts +0 -17
  210. package/dist/commands/ops.command.d.ts.map +0 -1
  211. package/dist/commands/ops.command.js +0 -129
  212. package/dist/commands/ops.command.js.map +0 -1
  213. package/dist/commands/privilege.command.d.ts +0 -16
  214. package/dist/commands/privilege.command.d.ts.map +0 -1
  215. package/dist/commands/privilege.command.js +0 -86
  216. package/dist/commands/privilege.command.js.map +0 -1
  217. package/dist/commands/role.command.d.ts +0 -3
  218. package/dist/commands/role.command.d.ts.map +0 -1
  219. package/dist/commands/role.command.js +0 -101
  220. package/dist/commands/role.command.js.map +0 -1
  221. package/dist/commands/runtime-dir.d.ts +0 -16
  222. package/dist/commands/runtime-dir.d.ts.map +0 -1
  223. package/dist/commands/runtime-dir.js +0 -18
  224. package/dist/commands/runtime-dir.js.map +0 -1
  225. package/dist/commands/setup.command.d.ts +0 -63
  226. package/dist/commands/setup.command.d.ts.map +0 -1
  227. package/dist/commands/setup.command.js +0 -148
  228. package/dist/commands/setup.command.js.map +0 -1
  229. package/dist/commands/stripe.command.d.ts +0 -29
  230. package/dist/commands/stripe.command.d.ts.map +0 -1
  231. package/dist/commands/stripe.command.js +0 -99
  232. package/dist/commands/stripe.command.js.map +0 -1
  233. package/dist/commands/tenant.command.d.ts +0 -3
  234. package/dist/commands/tenant.command.d.ts.map +0 -1
  235. package/dist/commands/tenant.command.js +0 -135
  236. package/dist/commands/tenant.command.js.map +0 -1
  237. package/dist/commands/token.command.d.ts +0 -3
  238. package/dist/commands/token.command.d.ts.map +0 -1
  239. package/dist/commands/token.command.js +0 -54
  240. package/dist/commands/token.command.js.map +0 -1
  241. package/dist/commands/user.command.d.ts +0 -3
  242. package/dist/commands/user.command.d.ts.map +0 -1
  243. package/dist/commands/user.command.js +0 -117
  244. package/dist/commands/user.command.js.map +0 -1
  245. package/dist/credentials.d.ts +0 -141
  246. package/dist/credentials.d.ts.map +0 -1
  247. package/dist/credentials.js +0 -348
  248. package/dist/credentials.js.map +0 -1
  249. package/dist/flag-coherence.d.ts +0 -42
  250. package/dist/flag-coherence.d.ts.map +0 -1
  251. package/dist/flag-coherence.js +0 -103
  252. package/dist/flag-coherence.js.map +0 -1
  253. package/dist/output.d.ts +0 -17
  254. package/dist/output.d.ts.map +0 -1
  255. package/dist/output.js +0 -107
  256. package/dist/output.js.map +0 -1
  257. package/dist/preview.d.ts +0 -9
  258. package/dist/preview.d.ts.map +0 -1
  259. package/dist/preview.js +0 -63
  260. package/dist/preview.js.map +0 -1
  261. package/dist/product-decisions.d.ts +0 -40
  262. package/dist/product-decisions.d.ts.map +0 -1
  263. package/dist/product-decisions.js +0 -52
  264. package/dist/product-decisions.js.map +0 -1
  265. package/dist/prompt-urls.d.ts +0 -31
  266. package/dist/prompt-urls.d.ts.map +0 -1
  267. package/dist/prompt-urls.js +0 -35
  268. package/dist/prompt-urls.js.map +0 -1
  269. package/dist/prompts/auth-master-integration-prompt.md +0 -255
  270. package/dist/prompts/billing/master.md +0 -332
  271. package/dist/prompts/decisions/feature-control.md +0 -131
  272. package/dist/prompts/decisions/going-live.md +0 -60
  273. package/dist/prompts/decisions/login.md +0 -71
  274. package/dist/prompts/decisions/look-and-feel.md +0 -68
  275. package/dist/prompts/decisions/payments.md +0 -97
  276. package/dist/prompts/decisions/roles.md +0 -78
  277. package/dist/prompts/decisions/teams.md +0 -61
  278. package/dist/prompts/fit-together.md +0 -97
  279. package/dist/prompts/flags/master.md +0 -220
  280. package/dist/prompts/guide-coverage.json +0 -17
  281. package/dist/prompts/integration-success.md +0 -106
  282. package/dist/prompts/mcp-tools.json +0 -90
  283. package/dist/prompts/mechanisms.md +0 -298
  284. package/dist/prompts/orientation.md +0 -42
  285. package/dist/prompts/product-decisions.json +0 -338
  286. package/dist/resolve.d.ts +0 -110
  287. package/dist/resolve.d.ts.map +0 -1
  288. package/dist/resolve.js +0 -260
  289. package/dist/resolve.js.map +0 -1
  290. package/dist/shape.d.ts +0 -25
  291. package/dist/shape.d.ts.map +0 -1
  292. package/dist/shape.js +0 -74
  293. package/dist/shape.js.map +0 -1
@@ -1,131 +0,0 @@
1
- # Feature control — decision guide
2
-
3
- Controlling who gets a route, an API endpoint, a feature or a single piece of code, without a deploy. The tool is a **flag**: code asks for it by key, and the flag's rule decides the answer.
4
-
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
-
7
- ## The rule that decides everything else
8
-
9
- 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, and point the flag's rule at the plan's list.
10
-
11
- - **Is it on for them? A flag.** The flag is the stable place in code; the *reasons* live in its rule and change as the business does: 10% of users today, "the plan includes it" tomorrow, one named customer as an exception next month. Each of those is a rule edit, not new code.
12
- - **Does a plan sell it? The plan's features list, and the flag's rule reads it:** `bridge:billing.entitlement.<feature> eq true`. The rule never names plans, so changing what Pro includes is one edit on the plan.
13
- - **Who is this person? A flag rule on a privilege** (preferred) **or a role.** Never a role check written into the app's code.
14
- - **How much do they get? A plan limit.** Numbers are not flags: see the **payments** guide.
15
- - **May they change this one record? The app's own code.** Only the app knows who owns what.
16
-
17
- **Every gate in app code is a flag.** App code never reads a role, a privilege list, the plan or a plan feature to decide what someone may see or do; before calling the work done, `npx @nebulr-group/bridge-cli check gates` lists any that are left.
18
-
19
- ## Start from what is there
20
-
21
- | Read | MCP | CLI |
22
- |---|---|---|
23
- | Every flag with its rule, and warnings on flags that cannot mean what they look like | `list_feature_flags` | `bridge flag list` |
24
- | One flag | `get_flag` | `bridge flag get <key>` |
25
- | What each plan sells, for plan-feature rules | `list_plans` | `bridge plan list`, `bridge plan feature list <plan>` |
26
- | The app's real roles and privileges, for who-someone-is rules | `list_roles` | `bridge role list` |
27
-
28
- Read the roles before writing a rule on them: the developer may have renamed or reshaped any of them, so never assume what a role grants from its name.
29
-
30
- ## Ask the developer
31
-
32
- Product questions. Ask, wait, never guess.
33
-
34
- 1. **Which feature, in their words, and who should get it?** Everyone, nobody yet, a percentage of users, the plans that sell it, people allowed to do something, named customers, or a mix.
35
- 2. **If a plan sells it: which plans?** The answer goes on the plans' features lists, not into the rule.
36
- 3. **If it depends on who someone is: what may they do that others may not?** Ask in the app's verbs ("manage billing", "see reports"). That names a privilege; the roles guide covers creating one.
37
- 4. **What do people without it see?** Nothing at all, the old version, or, when the plan is the reason, an upgrade prompt they can click.
38
- 5. **Should it switch on or off by itself at a set time?**
39
-
40
- ## Decide yourself
41
-
42
- - **Flag keys in kebab-case, named after the feature: `new-editor`, `analytics`.** Reason: the key is what code reads; it should still make sense after the rollout is over.
43
- - **Boolean unless the developer needs variants.** Reason: on/off covers almost every case and reads plainly in code.
44
- - **A feature a plan sells: add it to the plan (`add_plan_feature`), then rule on `bridge:billing.entitlement.<feature> eq true`.** Reason: the pricing table, the upgrade dialog and the flag all read one list, and no rule names a plan.
45
- - **Who someone is: rule on a privilege (`privileges contains "REPORTS_VIEW"`) before a role (`user.role eq "ADMIN"`).** Reason: a privilege rule survives renamed or reshuffled roles. Use the full privilege key: `contains` on the privileges list matches a whole key, never part of one.
46
- - **An admin-only page is a flag route rule, not a check on the page.** Reason: the rule is changed in one place and the backend reads the same one.
47
- - **Always give the rule an `otherwiseValue`, and make it the safe answer (usually `false`).** Reason: anyone the rule does not match gets it.
48
- - **Test the rule with the exact attributes the app sends before relying on it** (`evaluate_feature_flag` / `bridge flag eval`). Reason: a rule on an attribute the app never sends silently answers `otherwiseValue` for everyone.
49
- - **Read flags through the SDK the per-framework guide names, never with a hand-written request.** Reason: a wrong address fails into "off", which looks exactly like "not bought".
50
- - **Treat a warning from `list_feature_flags` as a bug to fix now.**
51
-
52
- ## The attributes a rule can test
53
-
54
- These are the exact names and values. A rule on any other name matches nothing unless the app sends that attribute itself.
55
-
56
- **Sent by the browser SDKs** (SvelteKit, React, Next.js, Angular) for a signed-in person, read from their token:
57
-
58
- | Attribute | Value | Compared |
59
- |---|---|---|
60
- | `user.id` | the person's id | exactly |
61
- | `user.email` | their email | exactly |
62
- | `user.role` | the role **key** as created, e.g. `OWNER`, `ADMIN` | exactly: `admin` does not match `ADMIN` |
63
- | `tenant.id` | the workspace id | exactly |
64
- | `tenant.plan` | the plan **key**, e.g. `pro` (not the name) | ignoring case: `Pro` matches `pro` |
65
- | `privileges` | the person's privilege keys, as a list | `contains`: the key appears in the list. The preferred way to target who someone is |
66
-
67
- **Also sent by the browser SDKs, from the workspace's subscription:**
68
-
69
- | Attribute | Value |
70
- |---|---|
71
- | `bridge:billing.plan` | the plan key, compared exactly |
72
- | `bridge:billing.subscription.status` | `trial`, `active`, `past_due`, `cancel_at_period_end` or `canceled` |
73
- | `bridge:billing.trial` | `true` or `false` |
74
- | `bridge:billing.quota.<metric>.used` / `.limit` / `.remaining` | numbers |
75
- | `bridge:billing.quota.<metric>.percent_used` | a fraction: `0.8` is 80% |
76
- | `bridge:billing.entitlement.<name>` | `true` or `false`: a feature in the plan's features list is `true` when the workspace's plan includes it, so "the plan includes analytics" is the rule `bridge:billing.entitlement.analytics eq true`. Also `app_active` (the subscription is active, trialing, past due or cancelling) |
77
-
78
- **On the NestJS backend** the same attributes are filled in with no wiring: `user.role`, `privileges`, `user.id`, `user.email`, `tenant.id` and `tenant.plan` from the verified sign-in, and `bridge:billing.plan`, `.subscription.status`, `.trial` and `.entitlement.<name>` from Bridge's own billing records. A rule gives the same answer there as in the browser. Only the plan-limit numbers (`bridge:billing.quota.*`) are browser-only. Values the client sends are never used for these.
79
-
80
- **Why it is off.** A flag that is off says why: `plan` (the plan does not include it, so an upgrade would turn it on), `permission` (role or privileges), or `off` (switched off). No upgrade dialog opens by itself: it opens when someone opens a gated route or clicks something gated (SvelteKit: `<FeatureFlag key="analytics" upgrade>`). A flag-gated backend endpoint answers `402 FEATURE_NOT_IN_PLAN`, `403 FEATURE_NOT_PERMITTED` or `403 FEATURE_OFF`.
81
-
82
- **Percentage rollouts** bucket on the identity: the person's id when signed in, a stored anonymous id in the browser otherwise. A backend call with no signed-in person gets the default.
83
-
84
- Operators: `eq`, `neq`, `in`, `not_in`, `contains`, `not_contains`, `gt`, `lt`, `between`, `regex`, `exists`, `not_exists`.
85
-
86
- A condition's values are always a list, even for a single value.
87
-
88
- ## Do it
89
-
90
- | Step | MCP | CLI |
91
- |---|---|---|
92
- | Create a flag with its rule | `create_feature_flag` | `bridge flag create --key analytics --state on-with-rule --rule '<json>'` |
93
- | Change a rule or values | `update_feature_flag` | `bridge flag update --key analytics --rule '<json>'` |
94
- | Switch it on or off for everyone now (the rule is kept, unused while on) | `toggle_feature_flag` | `bridge flag toggle --key analytics --enabled true` |
95
- | Add the feature to the plans that sell it | `add_plan_feature` | `bridge plan feature add pro analytics --name "Analytics"` |
96
- | Check who gets what | `evaluate_feature_flag` | `bridge flag eval analytics --identity u1 --attribute bridge:billing.entitlement.analytics=true` |
97
- | Switch at a set time | `set_flag_schedule` | `bridge flag schedule set analytics --at … --state on` |
98
- | Copy flags between apps | `export_feature_flags`, `import_feature_flags` | `bridge flag export`, `bridge flag import <file>` |
99
- | Code for the framework | `get_integration_guide` (topic `feature-flags`) | `bridge guide flags --framework svelte` |
100
-
101
- A rule for "the plans that sell analytics":
102
-
103
- ```json
104
- { "branches": [{ "conditions": [{ "attribute": "bridge:billing.entitlement.analytics", "operator": "eq", "values": [true] }], "returnValue": true }],
105
- "otherwiseValue": false, "rolloutPct": 100 }
106
- ```
107
-
108
- A rule for "people who may see reports":
109
-
110
- ```json
111
- { "branches": [{ "conditions": [{ "attribute": "privileges", "operator": "contains", "values": ["REPORTS_VIEW"] }], "returnValue": true }],
112
- "otherwiseValue": false, "rolloutPct": 100 }
113
- ```
114
-
115
- ## Prove it
116
-
117
- 1. `evaluate_feature_flag` / `bridge flag eval` with a context that should get the feature, then one that should not: the answers differ as the developer intended.
118
- 2. `list_feature_flags` / `bridge flag list` shows no warnings on the flag.
119
- 3. In the app, as a test user on each side of the rule.
120
- 4. `npx @nebulr-group/bridge-cli check gates` in the project: no direct role, privilege, plan or plan-feature check left.
121
-
122
- ## Exceptions
123
-
124
- Checking a plan feature without a flag (`@RequireEntitlement` on the backend, `<Entitled>` or `$entitlements.can` in the UI) exists for the rare case where the developer explicitly asks for no flag. It prints a one-time note in development; mark the line `// bridge-gate-exception: <reason>` so `check gates` leaves it.
125
-
126
- ## Where this connects
127
-
128
- - **Fit together:** the rule above, with roles, plans, limits and flags side by side (`bridge guide fit-together`).
129
- - **Payments:** the plan's features list the rule points at, and limits for anything counted.
130
- - **Roles:** the privileges and roles a who-someone-is rule targets; read them with `list_roles` first.
131
- - **Going live:** flags are per app; a production app needs its own (`export_feature_flags`, then `import_feature_flags`).
@@ -1,60 +0,0 @@
1
- # Going live — decision guide
2
-
3
- Everything between "it works on my machine" and real users signing up and paying. Bridge keeps a readiness checklist for each app; this guide is how to work through it with the developer.
4
-
5
- > How roles, plans, limits and flags fit together: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`). Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
6
-
7
- ## Start from the checklist
8
-
9
- `get_setup_status` / `bridge setup status` lists every item (redirect URIs, default callback, allowed origins, UI URL, sign-in methods and provider credentials, email sender, Stripe, plans, first real sign-in) as configured, missing or needing attention, each with its next step. Its `productionNeeds` is what must still change before real users arrive: localhost addresses, test Stripe keys, an unconfirmed sender. Work from that list, not from memory.
10
-
11
- `list_apps` shows which apps this connection can reach. `diagnose_integration` / `bridge diagnose` compares the project's env files with the app they point at.
12
-
13
- ## Ask the developer
14
-
15
- Product questions. Ask, wait, never guess.
16
-
17
- 1. **Is the app you have been building with already serving real users?** If yes, it is the production app. If no, see "a separate production app" below.
18
- 2. **What is the production address of the frontend** (and of the backend, if it calls Bridge)?
19
- 3. **For each social sign-in provider: the production client id and secret,** if they use a different provider app in production.
20
- 4. **The live Stripe keys** (`sk_live_…`, `pk_live_…`), from the Stripe account that should receive the money. Never invent, guess or reuse a key.
21
- 5. **Which address and name should emails come from** in production?
22
-
23
- **Only changed when you pass them.** No tool sets these on its own; left out, the current value stays: `update_app` (`uiUrl`, `defaultCallbackUri`) · `setup_communication` (`fromAddress`, `fromName`).
24
-
25
- ## Decide yourself
26
-
27
- - **A separate Bridge app for production** when the current one has no real users. Reason: test users, test Stripe keys and localhost addresses stay out of production, and nothing in development can touch a real customer. Create it with `create_app`; it does not become the default, so pass its id as `app` or call `use_app`. Copy flags across with `export_feature_flags` then `import_feature_flags`, and recreate each plan with `apply_plan`.
28
- - **Production env files carry the production app id and no base URL.** Reason: an unset base URL means Bridge's production API; a leftover stage or local base URL sends a production app id to the wrong place, and sign-in fails.
29
- - **Add production addresses; remove localhost ones only from the production app.** Reason: removing them from the development app breaks local work.
30
- - **Add redirect URIs one at a time and send allowed origins as a full list** (read first). Reason: the first keeps the list; the second replaces it.
31
- - **Register the production callback in every social provider's console** (`setup_sso` returns it). Reason: providers refuse callbacks they do not know.
32
- - **Never put a secret key in a browser variable** (`VITE_*`, `NEXT_PUBLIC_*`). Reason: everything in those ships to every visitor. `diagnose_integration` flags it.
33
-
34
- ## Do it
35
-
36
- | Step | MCP | CLI |
37
- |---|---|---|
38
- | Readiness checklist | `get_setup_status` | `bridge setup status` |
39
- | Check a project's env files | `diagnose_integration` | `bridge diagnose` |
40
- | Create a production app | `create_app`, then `use_app` | `bridge app create --name <name> --use` |
41
- | Production URL, default callback, allowed origins | `update_app` | `bridge app update --ui-url … --default-callback-uri … --allowed-origins …` |
42
- | Production callback URL | `add_redirect_uri` | `bridge app redirect-uris add <url>` |
43
- | Live Stripe keys | `setup_payments` | `bridge setup payments --stripe-key sk_live_… --stripe-public-key pk_live_…` |
44
- | Email sender | `setup_communication` | `bridge setup communication --from-address … --from-name …` |
45
- | Copy flags | `export_feature_flags`, `import_feature_flags` | `bridge flag export --out flags.json`, `bridge flag import flags.json` |
46
- | Copy a plan | `apply_plan` | `bridge plan apply --spec @plan.json` |
47
-
48
- ## Prove it
49
-
50
- 1. `get_setup_status` / `bridge setup status` again: show the developer what is left. Nothing in `productionNeeds` should be a surprise to them.
51
- 2. Ask the developer to sign up and sign in themselves on the production address: a real sign-in from the production origin. If it fails, `debug_auth_issue` / `bridge event auth-attempts --user <their email>` names the reason.
52
- 3. `get_stripe_status` / `bridge stripe status`: live keys, webhook working.
53
- 4. The first real sign-in appears on the checklist once someone signs up.
54
-
55
- ## Where this connects
56
-
57
- - **Login:** redirect URIs, origins and provider credentials.
58
- - **Payments:** live keys, and plans on the production app.
59
- - **Feature control:** flags are per app.
60
- - **Look and feel:** a confirmed sender address.
@@ -1,71 +0,0 @@
1
- # Login — decision guide
2
-
3
- How people sign up and sign in to the developer's app. This guide says what to ask, what to decide yourself, and which tool or command does each step. The code itself is in the per-framework guide.
4
-
5
- > How roles, plans, limits and flags fit together: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`). Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). How the pieces work underneath: `bridge guide mechanisms`.
6
-
7
- ## Start from what is there
8
-
9
- Read before you ask anything, so your questions are about changes, not about the current state.
10
-
11
- | Read | MCP | CLI |
12
- |---|---|---|
13
- | Sign-in methods and token lifetimes | `get_auth_config` | `bridge auth config` |
14
- | URLs, redirect URIs, allowed origins | `get_app` | `bridge app get` |
15
- | The exact env variables for this project | `get_environment_info` | — |
16
-
17
- A new app has password, magic link and passkeys on, two-factor off, and no social providers.
18
-
19
- ## Ask the developer
20
-
21
- Product questions. Ask, wait, never guess.
22
-
23
- 1. **Which ways should people be able to sign in?** Password is always there. Offer magic link, passkeys, Google, GitHub, Microsoft (Azure AD), LinkedIn, Facebook. Company SSO (SAML or OIDC) is set up in the Bridge dashboard, not from here.
24
- 2. **Should two-factor sign-in be available?**
25
- 3. **Can anyone sign up, or only people who are invited?** This is a teams question: see the **teams** guide before changing it.
26
- 4. **For each social provider they pick: the client id and client secret** from that provider's console. Never invent, guess or reuse a credential.
27
-
28
- **Only changed when you pass them.** No tool sets these on its own; left out, the current value stays: `update_auth_methods` (`mfaEnabled`, `passkeysEnabled`, `magicLinkEnabled`, `googleSsoEnabled`, `linkedinSsoEnabled`, `azureAdSsoEnabled`, `appleSsoEnabled`, `githubSsoEnabled`, `facebookSsoEnabled`).
29
-
30
- ## Decide yourself
31
-
32
- Apply these without asking. Say the reason in one line.
33
-
34
- - **Sign-in happens inside the app (SDK mode).** The pages render on the app's own address, inside its layout, styled to match the app (the **look and feel** guide says how). There is nothing to choose; tell the developer in one line. Reason: users never leave the app, and the pages look like the rest of it. (If the developer explicitly asks for Bridge's hosted sign-in page, or the app already uses it, respect that.)
35
- - **Callback URL is `<frontend-url>/auth/oauth-callback`,** registered as a redirect URI and as the default callback. Reason: the sign-in route file serves that address, and Bridge refuses any callback it has not seen.
36
- - **Add redirect URIs one at a time** with `add_redirect_uri` / `bridge app redirect-uris add`. Reason: they keep the rest of the list; nothing is lost.
37
- - **Before changing allowed origins, read the current list and pass it back with the new origin added.** Reason: `update_app` and `bridge app update --allowed-origins` replace the whole list.
38
- - **Set the UI URL to the frontend's address.** Reason: links in invitation and password-reset emails are built from it; unset, people land on a Bridge page instead of the app.
39
- - **Leave token lifetimes at their defaults** unless the developer raises it. Reason: the defaults are safe. There are no password-complexity rules to set; if asked, say so.
40
- - **Write no sign-in pages by hand, and no token fetch helper.** Reason: the plugin serves every sign-in page (login, signup, callback, set password, forgot password, magic link, passkey setup, workspace selection) from one file.
41
- - **Protect every route by default; make only `/auth/*` public.** Reason: a new page is private until someone decides otherwise.
42
-
43
- ## Do it
44
-
45
- | Step | MCP | CLI |
46
- |---|---|---|
47
- | See every login method and whether it is on | `get_auth_config` | `bridge auth methods list` |
48
- | Turn magic link, passkeys, two-factor or a social provider on or off | `update_auth_methods` | `bridge auth methods enable magicLink` / `bridge auth methods disable google` |
49
- | Add a social provider with its credentials | `setup_sso` | `bridge setup sso --provider google --client-id … --client-secret …` |
50
- | Register the callback URL | `add_redirect_uri` | `bridge app redirect-uris add <url>` |
51
- | Set UI URL, default callback, allowed origins | `update_app` | `bridge app update --ui-url … --default-callback-uri … --allowed-origins …` |
52
- | Change token lifetimes (seconds) | `update_password_policy` | `bridge auth password-policy --access-token-ttl 900` |
53
- | Code for the framework | `get_integration_guide` (topic `integration`, then `auth`) | `bridge guide svelte`, then `bridge guide svelte sdk-auth` |
54
-
55
- `setup_sso` returns a callback URL. The developer must paste it into the provider's console, or that provider's sign-in fails with a redirect mismatch. Always show it to them with that instruction. `update_auth_methods` / `bridge auth methods` never save credentials: Bridge refuses to switch a social provider on until they are saved, and switching one off keeps them. Bridge also refuses to switch off the last remaining sign-in method.
56
-
57
- ## Prove it
58
-
59
- Do not say "it should work". Show it working.
60
-
61
- 1. `diagnose_integration` / `bridge diagnose`: compares the project's env files with the real app and names every difference with its fix.
62
- 2. Ask the developer to sign up and sign in themselves in the running app: sign up, open the verification email, set a password, sign in, sign out. Do not create test users; a real signup is the only check that covers the app's own pages and the emails.
63
- 3. If a step fails, `debug_auth_issue` / `bridge event auth-attempts --user <their email>` names the reason and the fix.
64
- 4. When a real person cannot sign in: `debug_auth_issue` / `bridge event auth-attempts --user <email>` gives each failed attempt with its reason and fix.
65
-
66
- ## Where this connects
67
-
68
- - **Teams:** who can sign up, and whether they create a workspace or join one.
69
- - **Roles:** what a person can do once signed in.
70
- - **Look and feel:** how the sign-in pages take on the app's design.
71
- - **Going live:** production URLs and redirect URIs replace the localhost ones.
@@ -1,68 +0,0 @@
1
- # Look and feel — decision guide
2
-
3
- How Bridge's pages and emails look to the developer's users: the sign-in pages, the subscription pages, and the emails Bridge sends in the app's name.
4
-
5
- > How roles, plans, limits and flags fit together: `bridge guide fit-together` (MCP: resource `bridge://guides/fit-together`). Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). The customisation levels in full, with every design token: `bridge guide mechanisms`.
6
-
7
- ## Where the pages live
8
-
9
- The sign-in and subscription pages render inside the developer's own layout, on their address. They are styled with CSS design tokens (`--bridge-*`) in the app's own stylesheet, taken from the app's existing design. (If the developer explicitly asks for Bridge's hosted sign-in page, or the app already uses it, respect that: that page is styled with the app's branding stored in Bridge, `update_branding` / `bridge branding update`. Give it the app's colours.)
10
-
11
- **The pages Bridge adds must look like the developer's app, not like Bridge.** Bridge's built-in look (indigo buttons, grey borders, system font) is a fallback for an app with no design of its own, never the finished result.
12
-
13
- ## Start from what is there
14
-
15
- Read the app's own design first: it is in the project, not in Bridge.
16
-
17
- | Read | Where |
18
- |---|---|
19
- | Colours, radius, spacing | The global stylesheet and its CSS variables (`:root { --primary: … }`), `tailwind.config.*` / the Tailwind `@theme` block, or the component library's theme (shadcn, MUI, Angular Material, Chakra, …) |
20
- | Font | The font the app loads (`<link>` in the HTML shell, `@font-face`, `next/font`, the `font-family` on `body`) |
21
- | Buttons and inputs | An existing primary button and text input: background, text colour, border, radius, focus ring, height |
22
- | Page shell | How the app frames a page: a centred card, a max width, a header and footer, a background colour, dark mode |
23
-
24
- | Read | MCP | CLI |
25
- |---|---|---|
26
- | App name, and the email sender | `get_app` | `bridge app get` |
27
-
28
- ## Ask the developer
29
-
30
- Product questions. Ask, wait, never guess.
31
-
32
- 1. **Only when the project has no styles of its own:** what are the brand colours and font? Ask for exact values; do not pick colours for them. When the app has a design, take it from the code and do not ask.
33
- 2. **What name should users see** on sign-in pages and in emails?
34
- 3. **Which address and sender name should emails come from?** (Invitations, password resets, magic links.)
35
- 4. **Beyond matching the app, should sign-in pages say anything of their own?** A different heading, or a page they design themselves.
36
-
37
- **Only changed when you pass them.** No tool sets these on its own; left out, the current value stays: `update_branding` (`bgColor`, `textColor`, `linkColor`, `primaryButtonBgColor`, `primaryButtonTextColor`, `fontFamily`, `borderRadius`).
38
-
39
- ## Decide yourself
40
-
41
- Match the app's design without asking, and say which level you stopped at.
42
-
43
- - **Level 0, layout:** the pages already sit inside the app's layout, so its header and navigation surround them. Never the stopping point on its own: the form inside still wears Bridge's colours.
44
- - **Level 1, tokens — always:** map the app's design onto the `--bridge-*` tokens in the app's own stylesheet: its primary colour and hover to `--bridge-primary` / `--bridge-primary-hover` / `--bridge-primary-fg`, its border colour to `--bridge-border`, its corner radius to `--bridge-border-radius`, its focus ring to `--bridge-input-focus`, its text and muted text to `--bridge-foreground` / `--bridge-muted`, its surfaces to `--bridge-bg` / `--bridge-muted-bg`, its error and success colours to the alert tokens. Reference the app's own variables (`--bridge-primary: var(--primary)`) rather than copying values, so a later theme change carries over, dark mode included. The components inherit the page font, so set nothing for it unless the app sets its font on something other than `body`. Reason: tokens are the supported contract; ordinary CSS already wins over Bridge's defaults, so there is never a need for `!important`.
45
- - **Level 2, frame and heading — when the app has a page shell:** if the app's own pages sit in a card, a panel or a two-column layout, pass the `frame` snippet so sign-in sits in the same one, using the app's own classes or components. Reason: it changes every page at once and keeps each page's own sub-steps.
46
- - **Level 3, take over one page:** create that page's own route file (e.g. `src/routes/auth/login/+page.svelte`); every other page keeps working.
47
- - **Level 4, headless:** build everything on `getBridgeAuth()`. Only when the developer asks for it.
48
- - **Never copy Bridge's components into the app to restyle them.** Reason: the copy stops receiving fixes.
49
- - **A new sender address must be confirmed.** Bridge emails that address a link; until it is clicked, mail keeps coming from Bridge's default sender. Tell the developer to look for it.
50
-
51
- ## Do it
52
-
53
- | Step | MCP | CLI |
54
- |---|---|---|
55
- | Name users see | `update_app` (`name`) | `bridge app update --name "Acme"` |
56
- | Email sender | `setup_communication` | `bridge setup communication --from-address no-reply@acme.com --from-name Acme` |
57
- | Sign-in pages, tokens, snippets | `get_integration_guide` (topic `branding`, then `auth`) | `bridge guide svelte branding`, `bridge guide mechanisms` |
58
-
59
- ## Prove it
60
-
61
- 1. Open the sign-in page and the subscription page in the running app next to one of the app's own pages: buttons, inputs, font, corners and the page frame match, in light and dark mode if the app has both. No Bridge indigo is left.
62
- 2. Ask the developer to trigger a password reset for their own account: the email arrives from the agreed sender, in the app's name, and its link opens the app, not a Bridge page.
63
-
64
- ## Where this connects
65
-
66
- - **Login:** the sign-in pages this guide styles.
67
- - **Payments:** the subscription and plan-choice pages use the same tokens.
68
- - **Going live:** a verified sender address before real users arrive.
@@ -1,97 +0,0 @@
1
- # Payments and plans — decision guide
2
-
3
- What customers pay, what each plan includes, and what happens when they reach a limit. A **plan** belongs to a workspace, not to a person. It carries the price, the trial, the limits ("3 projects on Free, 50 on Pro") and the list of features it sells ("Analytics on Pro"). The features themselves are switched by flags whose rules read that list; the plan never gets checked by name in code. Payments run through the developer's own Stripe account.
4
-
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>`). The full model behind limits and upgrade prompts: `bridge guide mechanisms`.
6
-
7
- ## Start from what is there
8
-
9
- | Read | MCP | CLI |
10
- |---|---|---|
11
- | Plans with prices and limits | `list_plans` | `bridge plan list` |
12
- | Every metric the plans already limit, with its kind | `list_plan_quotas` | `bridge plan quota list` |
13
- | Whether Stripe is connected and its webhook works | `get_stripe_status` | `bridge stripe status` |
14
- | Whether new customers must pick a plan first (`paymentsAutoRedirect`) | `get_app` | `bridge app get` |
15
-
16
- ## Ask the developer
17
-
18
- Product questions. Ask, wait, never guess. Then show the answer back as tables (plans, prices, limits) and create nothing until the developer confirms them.
19
-
20
- 1. **Which plans, and what are they called?** Is there a free one?
21
- 2. **What does each paid plan cost, in which currency, and monthly, yearly or both?**
22
- 3. **Is there a free trial, and how many days?**
23
- 4. **What does each plan limit, and how much?** In their words: "2 tickets on Free, 100 on Pro", "10 seats", "1,000 exports a month".
24
- 5. **For each limit: at the limit, block, or keep going and charge per extra unit?** If they charge, how much per unit?
25
- 6. **For each limit: does the action call your server?** ("Does clicking Export send a request to your backend?") The answer decides where it is counted; see below.
26
- For a seat limit, ask instead: **do invites go through Bridge's built-in team page, or through your own invite handler?** The answer decides where the seat limit is checked; see below.
27
- 7. **Which features does each plan sell?** ("Analytics on Pro only.") They go on the plans' features lists.
28
- 8. **Do they want a welcome page for new customers** where they choose a plan? Offer it; create it only if they say yes.
29
-
30
- **The tools will not guess these.** Called without one, a tool changes nothing and answers `DECISION_NEEDED` with the question to ask; the matching bridge command stops the same way: `create_plan` (`amount`, `interval`, `currency`, `trial`, `trialDays`) · `apply_plan` (`name`, `prices.amount`, `prices.interval`, `prices.currency`, `trial`, `trialDays`) · `set_plan_price` (`currency`). A free plan (price 0) needs no currency or trial answer, and a price added to a plan that charges in one currency reuses it.
31
-
32
- **Only changed when you pass them.** No tool sets these on its own; left out, the current value stays: `update_app` (`paymentsAutoRedirect`).
33
-
34
- ## Decide yourself
35
-
36
- - **Plan keys short and lower-case: `free`, `pro`, `team`.** Reason: the key is permanent and is what code, flag rules and tokens use; the name can change any time.
37
- - **Monthly and yearly prices of one tier are one plan with two prices,** never `pro-monthly` and `pro-yearly`. Reason: the subscription page then shows one plan with a monthly/yearly switch.
38
- - **A free plan gets a price of 0.** Reason: a plan with no price cannot be assigned to a workspace.
39
- - **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. Exports, AI completions and emails are counters (reset each billing period). Tickets, projects and stored files are gauges (never reset). Decide from that sentence and say which you chose.
40
- - **Seats are a plan limit the app names, counted from membership:** metric `seats`, kind `gauge`, source `membership` (`set_plan_quota` / `bridge plan quota set pro --metric seats --limit 10 --policy hard --kind gauge --source membership`). Reason: Bridge then counts the workspace's active members itself, pending invites included, so the app never reports a seat. No metric name is built in.
41
- - **Reuse metric names the app already has** (`list_plan_quotas`) before inventing new ones. Reason: two names for one thing split the count.
42
- - **Connect Stripe with test keys first; live keys only when going live.** Reason: nobody is charged while you build. Use `setup_payments` / `bridge setup payments`: it stores the keys and switches payments on in one step.
43
- - **Connect Stripe before adding paid prices.** Reason: each price is created in Stripe as it is saved.
44
- - **Leave "customers must pick a plan" on** (it is on by default) unless the developer says otherwise. Reason: a new workspace is then asked to choose a plan before it uses the app.
45
- - **A workspace with no plan is normal.** It has none of the plan's features, and the plan-choice page or the app's own upgrade prompt shows it the plans. Never report it as a fault.
46
- - **One plan, one call:** `apply_plan` / `bridge plan apply` sets name, trial, prices and limits together. Reason: a plan is never left half set up.
47
-
48
- ## How a hard limit is enforced, end to end
49
-
50
- Tell the developer this plainly; it is where integrations go wrong.
51
-
52
- 1. **The plan holds the number.** A limit is a quota on the plan: metric, limit, `hard`, and `counter` or `gauge`.
53
- 2. **It is counted once, where the action happens.** Ask whether the action calls the developer's server, then follow the answer. When it does, the backend counts and enforces it: in NestJS one decorator on the handler that creates the thing, `@RequireQuota('exports')`; in Express the same as middleware, `bridge.requireQuota('exports')`. Before the handler runs, a workspace at its limit gets `402` with `{ code: 'QUOTA_EXCEEDED', metric, used, limit, fix }` and the handler never runs. After a `2xx` answer, Bridge records one use. Nothing else to wire: no usage call, no counter table. If Bridge cannot answer, the request is refused with `503`, never let through.
54
- 3. **Gauges send the app's own count.** `@RequireQuota('tickets', { current })` on create and `@SyncQuota('tickets', { current })` on delete (Express: `bridge.requireQuota` / `bridge.syncQuota` with the same `current`), where `current` returns the app's count from its own database. There is no decrement.
55
- 4. **Seats need no count at all; they are checked where invites happen.** Bridge counts the workspace's active members, pending invites included, and the count drops when someone is removed or suspended. Invites through the built-in team page: give it the seat limit's name, `<TeamManagementPanel seatsMetric="seats" />`, and Invite stops at the limit with the upgrade dialog. Invites through the app's own handler: `@RequireQuota('seats')` on that handler. Bridge's invite API does not refuse at the limit by itself, so one of the two is always needed. Never gate seats with a flag or an entitlement.
56
- 5. **With a backend, the frontend only shows the count and explains the refusal.** It does not report the same metric. With no code at all, the upgrade dialog opens on a `402`, names the limit, and links to the subscription page. A member who cannot manage billing is told to ask the owner. Disabling a button before the click (`<QuotaGate>`) or showing "8 of 10" (`useQuota`) is optional.
57
- 6. **When the action happens in the browser, the browser counts it.** For an app whose action never reaches a server of its own (local-first, data on the device), `bridge.usage.report('exports')` records something that happened and `bridge.usage.set('projects', n)` how many exist, and `<QuotaGate metric="exports">` around the button stops it at the limit. This is a complete, first-class way to run limits. Never count one metric in both places.
58
- 7. **`metered` never blocks.** Past the included amount it bills per unit through Stripe.
59
- 8. **A feature a plan sells goes in the plan's features list, and a flag controls it.** `analytics` on `pro` makes `bridge:billing.entitlement.analytics` true on Pro and false elsewhere; the flag that controls the feature has the rule `bridge:billing.entitlement.analytics eq true`, so the rule never names a plan; checking the plan feature directly is the exception at the end of this guide. No upgrade dialog opens by itself: it opens when someone opens a gated route, clicks something gated, or the backend answers `402`. A hard quota with limit 1 still works the same way; it is the older way. Never put `@RequireEntitlement` and `@RequireQuota` on the same name: at the cap the entitlement answers `403` first and the upgrade dialog never opens.
60
- 9. **The plan-choice page** (`/subscription/plan`) appears only when the app has plans and "customers must pick a plan" is on. Turn it off with `update_app` (`paymentsAutoRedirect: false`) / `bridge app update --payments-auto-redirect false`.
61
-
62
- The flag and its rule are set up in the **feature-control** guide.
63
-
64
- ## Do it
65
-
66
- | Step | MCP | CLI |
67
- |---|---|---|
68
- | Connect Stripe (keys from the developer) | `setup_payments` | `bridge setup payments --stripe-key sk_test_… --stripe-public-key pk_test_…` |
69
- | Set up or reshape one plan in one call | `apply_plan` | `bridge plan apply --spec @plan.json` |
70
- | Or step by step: create a plan with its first price | `create_plan` | `bridge plan create --key pro --name Pro --amount 29 --interval month --currency usd --no-trial` |
71
- | Add another price | `set_plan_price` | `bridge plan price set pro --amount 290 --interval year` |
72
- | Add a limit | `set_plan_quota` | `bridge plan quota set pro --metric tickets --limit 100 --policy hard --kind gauge` |
73
- | Remove a plan no workspace is on | `delete_plan` | `bridge plan delete --key pro` |
74
- | Stop selling a plan workspaces are on (they keep it) | `archive_plan` | `bridge plan archive --key pro` |
75
- | Move a workspace to another plan | `set_tenant_plan` | `bridge tenant set-plan --name Acme --plan free` |
76
- | Add an on/off feature the plan sells | `add_plan_feature` | `bridge plan feature add pro analytics --name "Analytics"` |
77
- | Turn off the forced plan choice | `update_app` | `bridge app update --payments-auto-redirect false` |
78
- | Code for the framework | `get_integration_guide` (topic `billing`) | `bridge guide billing --framework svelte`, `bridge guide billing --framework nestjs` |
79
-
80
- ## Prove it
81
-
82
- 1. `list_plans` / `bridge plan list` matches the tables the developer confirmed; a plan with monthly and yearly pricing is one plan.
83
- 2. `get_stripe_status` / `bridge stripe status`: connected, webhook working.
84
- 3. At the limit, send the decorated request with curl: it answers `402 QUOTA_EXCEEDED`. In the app, the upgrade dialog opens and names the limit.
85
- 4. A paid plan opens Stripe Checkout and returns to the subscription page with the new plan.
86
-
87
- ## Exceptions
88
-
89
- Checking a plan feature without a flag (`@RequireEntitlement` on the backend, `<Entitled>` or `$entitlements.can` in the UI) exists for the rare case where the developer explicitly asks for no flag. It prints a one-time note in development; mark the line `// bridge-gate-exception: <reason>` so `check gates` leaves it.
90
-
91
- ## Where this connects
92
-
93
- - **Fit together:** plans, limits, roles and flags side by side (`bridge guide fit-together`).
94
- - **Feature control:** the flag whose rule reads the plan's features list.
95
- - **Teams:** seats, and the plan belonging to the workspace.
96
- - **Roles:** who may change the plan.
97
- - **Going live:** live Stripe keys.
@@ -1,78 +0,0 @@
1
- # Roles and permissions — decision guide
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 a flag rule can target them in the browser and on the backend alike.
4
-
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
-
7
- ## Which tool answers which question
8
-
9
- Three different questions, three different tools. Keep them apart.
10
-
11
- | The question | Answered by | Guide |
12
- |---|---|---|
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
- | How much do they get (projects, seats, exports)? | the **plan** | payments |
16
- | May they change this one record? | the app's own code | — |
17
-
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.
19
-
20
- ## Start from what is there
21
-
22
- | Read | MCP | CLI |
23
- |---|---|---|
24
- | Roles, their privileges, and which is the default | `list_roles` | `bridge role list` |
25
- | One role in full | `get_role` | — |
26
- | Privileges that exist | `list_roles` (the `privileges` map) | `bridge privilege list` |
27
-
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.
29
-
30
- ## Ask the developer
31
-
32
- Product questions. Ask, wait, never guess.
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.
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.
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.
37
- 4. **Who may manage the subscription and billing?**
38
-
39
- ## Decide yourself
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.
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.
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.
47
- - **When changing a role's privileges, send the full list.** Reason: the update replaces the list, and anything left out is revoked.
48
- - **When the action calls the backend, put the same flag on the endpoint** (NestJS `@RequireFeatureFlag`). Reason: the backend reads the same rule, and anyone can call the API directly.
49
-
50
- ## Do it
51
-
52
- | Step | MCP | CLI |
53
- |---|---|---|
54
- | Add a privilege | — | `bridge privilege create --key PROJECT_DELETE --description …` |
55
- | Create a role | `create_role` | `bridge role create --key EDITOR --name Editor --privileges PROJECT_READ,PROJECT_WRITE` |
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` |
58
- | Change a role's privileges | `update_role` (full list) | `bridge role update --key EDITOR --privileges …` (full list) |
59
- | Give a person a role | `update_user` | `bridge user update --email … --role EDITOR --tenant-id …` |
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` |
62
-
63
- On the NestJS backend the flag goes on the handler: `@RequireFeatureFlag('reports')`, whose rule is `privileges contains "REPORTS_VIEW"`. A person without it gets `403 FEATURE_NOT_PERMITTED`. `@RequirePrivilege` is for API tokens only (a machine calling with `x-api-key`), never for a person, and a hard-coded role check is never the answer.
64
-
65
- Setting the default on one role clears it from the previous default.
66
-
67
- ## Prove it
68
-
69
- 1. `list_roles` / `bridge role list` shows each role with exactly the privileges agreed, and the default where the developer wanted it.
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.
72
-
73
- ## Where this connects
74
-
75
- - **Teams:** the role an invited person gets.
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`.
78
- - **Payments:** who may change the plan.
@@ -1,61 +0,0 @@
1
- # Teams and workspaces — decision guide
2
-
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
-
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
-
7
- ## Start from what is there
8
-
9
- | Read | MCP | CLI |
10
- |---|---|---|
11
- | Whether people can create their own workspace (`tenantSelfSignup`) | `get_app` | `bridge app get` |
12
- | Existing workspaces | `list_tenants` | `bridge tenant list` |
13
- | The people in one workspace | `list_users` | `bridge user list --tenant-id <id>` |
14
- | Roles an invited person can get | `list_roles` | `bridge role list` |
15
-
16
- ## Ask the developer
17
-
18
- Product questions. Ask, wait, never guess.
19
-
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
- 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?** 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
- 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. Then ask: **do invites go through Bridge's built-in team page, or through your own invite handler?**
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
-
26
- **The tools will not guess these.** Called without one, a tool changes nothing and answers `DECISION_NEEDED` with the question to ask; the matching bridge command stops the same way: `invite_user` (`role`). It offers the app's role keys and names the default one.
27
-
28
- **Only changed when you pass them.** No tool sets these on its own; left out, the current value stays: `update_app` (`tenantSelfSignup`).
29
-
30
- ## Decide yourself
31
-
32
- - **Self-signup on for a product people try on their own; off for invitation-only products.** Take it from the answer to question 2 and set `tenantSelfSignup` to match. Reason: with it off, a stranger who signs up has nowhere to land.
33
- - **Use Bridge's team panel for inviting and managing people** (`TeamManagementPanel` in SvelteKit) rather than a page of your own. Reason: it already handles invitations, role changes and removal against the right permissions.
34
- - **Gate the team link and the team page with a flag ruled on a privilege**, for example a flag `team-admin` with `privileges contains "USER_WRITE"` after reading `list_roles`, used as `<FeatureFlag key="team-admin">` around the link and a route rule with `featureFlag: 'team-admin'` on the page. Never a list of role names in code. Reason: every gate in app code is a flag, and a privilege rule survives renamed roles.
35
- - **Seats are a plan limit you name (`seats`), a gauge counted from membership.** Reason: Bridge counts the workspace's active members itself, pending invites included, so there is nothing for the app to count or report. Check it where invites happen: `<TeamManagementPanel seatsMetric="seats" />` on the built-in team page, or `@RequireQuota('seats')` on the app's own invite handler. Bridge's invite API does not refuse at the limit by itself. Never a flag or an entitlement.
36
- - **Never create workspaces from the app's own code for sign-ups.** Reason: Bridge creates one at signup when self-signup is on; creating them elsewhere makes duplicates.
37
- - **Create workspaces by hand only for setup and support** (an existing customer you are migrating, or a demo).
38
-
39
- ## Do it
40
-
41
- | Step | MCP | CLI |
42
- |---|---|---|
43
- | Allow or stop self-signup | `update_app` (`tenantSelfSignup`) | `bridge app update --tenant-self-signup true` |
44
- | Create a workspace by hand | `create_tenant` | `bridge tenant create --name … --owner-email …` |
45
- | Invite someone into a workspace | `invite_user` | `bridge user invite --email … --role … --tenant-id …` |
46
- | Change someone's role | `update_user` | `bridge user update --email … --role … --tenant-id …` |
47
- | Limit seats per plan | `set_plan_quota` (metric `seats`, kind `gauge`, source `membership`) | `bridge plan quota set pro --metric seats --limit 10 --policy hard --kind gauge --source membership` |
48
- | Code for the framework | `get_integration_guide` (topic `team`) | `bridge guide svelte team` |
49
-
50
- Pass the role key the developer chose on every invitation you make on their behalf; without one, `invite_user` and `bridge user invite` invite nobody and ask which role.
51
-
52
- ## Prove it
53
-
54
- 1. Ask the developer to sign up themselves in the running app; their workspace then appears in `list_tenants` / `bridge tenant list`.
55
- 2. Invite a second address into that workspace and check it appears in `list_users` / `bridge user list` with the role you meant.
56
-
57
- ## Where this connects
58
-
59
- - **Roles:** the privileges each kind of person has, which flag rules then target.
60
- - **Payments:** a plan belongs to the workspace, not to a person; seats are a plan limit.
61
- - **Login:** whether sign-up is open at all.