@nebulr-group/bridge-cli 0.6.0-beta.6 → 0.6.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/commands/check.command.d.ts +10 -0
- package/dist/commands/check.command.d.ts.map +1 -0
- package/dist/commands/check.command.js +95 -0
- package/dist/commands/check.command.js.map +1 -0
- package/dist/commands/guide.command.d.ts +5 -0
- package/dist/commands/guide.command.d.ts.map +1 -1
- package/dist/commands/guide.command.js +28 -4
- package/dist/commands/guide.command.js.map +1 -1
- package/dist/commands/journeys.d.ts.map +1 -1
- package/dist/commands/journeys.js +4 -2
- package/dist/commands/journeys.js.map +1 -1
- package/dist/commands/plan.command.d.ts +47 -5
- package/dist/commands/plan.command.d.ts.map +1 -1
- package/dist/commands/plan.command.js +197 -34
- package/dist/commands/plan.command.js.map +1 -1
- package/dist/commands/role.command.d.ts.map +1 -1
- package/dist/commands/role.command.js +19 -1
- package/dist/commands/role.command.js.map +1 -1
- package/dist/commands/user.command.d.ts.map +1 -1
- package/dist/commands/user.command.js +2 -1
- package/dist/commands/user.command.js.map +1 -1
- package/dist/gate-rules.d.ts +51 -0
- package/dist/gate-rules.d.ts.map +1 -0
- package/dist/gate-rules.js +171 -0
- package/dist/gate-rules.js.map +1 -0
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +3 -0
- package/dist/program.js.map +1 -1
- package/dist/prompts/auth-master-integration-prompt.md +4 -1
- package/dist/prompts/billing/master.md +16 -6
- package/dist/prompts/decisions/feature-control.md +46 -20
- package/dist/prompts/decisions/going-live.md +1 -1
- package/dist/prompts/decisions/login.md +1 -1
- package/dist/prompts/decisions/look-and-feel.md +1 -1
- package/dist/prompts/decisions/payments.md +20 -12
- package/dist/prompts/decisions/roles.md +22 -16
- package/dist/prompts/decisions/teams.md +7 -6
- package/dist/prompts/fit-together.md +97 -0
- package/dist/prompts/flags/master.md +6 -1
- package/dist/prompts/guide-coverage.json +6 -1
- package/dist/prompts/mcp-tools.json +6 -2
- package/dist/prompts/mechanisms.md +28 -14
- package/dist/prompts/orientation.md +5 -3
- package/dist/prompts/product-decisions.json +2 -2
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Payments and plans — decision guide
|
|
2
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
|
|
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
4
|
|
|
5
|
-
> Other decision guides: `bridge guide decision <name>` (MCP: resource `bridge://guides/decisions/<name>`). The full model behind limits and upgrade prompts: `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>`). The full model behind limits and upgrade prompts: `bridge guide mechanisms`.
|
|
6
6
|
|
|
7
7
|
## Start from what is there
|
|
8
8
|
|
|
@@ -22,8 +22,10 @@ Product questions. Ask, wait, never guess. Then show the answer back as tables (
|
|
|
22
22
|
3. **Is there a free trial, and how many days?**
|
|
23
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
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. **
|
|
26
|
-
|
|
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.
|
|
27
29
|
|
|
28
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.
|
|
29
31
|
|
|
@@ -35,7 +37,7 @@ Product questions. Ask, wait, never guess. Then show the answer back as tables (
|
|
|
35
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.
|
|
36
38
|
- **A free plan gets a price of 0.** Reason: a plan with no price cannot be assigned to a workspace.
|
|
37
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.
|
|
38
|
-
- **Seats are the
|
|
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.
|
|
39
41
|
- **Reuse metric names the app already has** (`list_plan_quotas`) before inventing new ones. Reason: two names for one thing split the count.
|
|
40
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.
|
|
41
43
|
- **Connect Stripe before adding paid prices.** Reason: each price is created in Stripe as it is saved.
|
|
@@ -47,16 +49,16 @@ Product questions. Ask, wait, never guess. Then show the answer back as tables (
|
|
|
47
49
|
Tell the developer this plainly; it is where integrations go wrong.
|
|
48
50
|
|
|
49
51
|
1. **The plan holds the number.** A limit is a quota on the plan: metric, limit, `hard`, and `counter` or `gauge`.
|
|
50
|
-
2. **
|
|
52
|
+
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')`. 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.
|
|
51
53
|
3. **Gauges send the app's own count.** `@RequireQuota('tickets', { current })` on create and `@SyncQuota('tickets', { current })` on delete, where `current` returns the app's count from its own database. There is no decrement.
|
|
52
|
-
4. **Seats need no count at all.** `@RequireQuota('
|
|
53
|
-
5. **
|
|
54
|
-
6. **
|
|
54
|
+
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.
|
|
55
|
+
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.
|
|
56
|
+
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.
|
|
55
57
|
7. **`metered` never blocks.** Past the included amount it bills per unit through Stripe.
|
|
56
|
-
8. **A plan
|
|
58
|
+
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.
|
|
57
59
|
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`.
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
The flag and its rule are set up in the **feature-control** guide.
|
|
60
62
|
|
|
61
63
|
## Do it
|
|
62
64
|
|
|
@@ -67,6 +69,7 @@ Whether a paid feature is a plan feature or a flag with a plan rule is decided i
|
|
|
67
69
|
| 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` |
|
|
68
70
|
| Add another price | `set_plan_price` | `bridge plan price set pro --amount 290 --interval year` |
|
|
69
71
|
| Add a limit | `set_plan_quota` | `bridge plan quota set pro --metric tickets --limit 100 --policy hard --kind gauge` |
|
|
72
|
+
| Add an on/off feature the plan sells | `add_plan_feature` | `bridge plan feature add pro analytics --name "Analytics"` |
|
|
70
73
|
| Turn off the forced plan choice | `update_app` | `bridge app update --payments-auto-redirect false` |
|
|
71
74
|
| Code for the framework | `get_integration_guide` (topic `billing`) | `bridge guide billing --framework svelte`, `bridge guide billing --framework nestjs` |
|
|
72
75
|
|
|
@@ -77,9 +80,14 @@ Whether a paid feature is a plan feature or a flag with a plan rule is decided i
|
|
|
77
80
|
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.
|
|
78
81
|
4. A paid plan opens Stripe Checkout and returns to the subscription page with the new plan.
|
|
79
82
|
|
|
83
|
+
## Exceptions
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
80
87
|
## Where this connects
|
|
81
88
|
|
|
82
|
-
- **
|
|
89
|
+
- **Fit together:** plans, limits, roles and flags side by side (`bridge guide fit-together`).
|
|
90
|
+
- **Feature control:** the flag whose rule reads the plan's features list.
|
|
83
91
|
- **Teams:** seats, and the plan belonging to the workspace.
|
|
84
92
|
- **Roles:** who may change the plan.
|
|
85
93
|
- **Going live:** live Stripe keys.
|
|
@@ -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
|
|
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
|
-
|
|
|
14
|
-
| Is this feature on for them
|
|
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
|
-
|
|
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?**
|
|
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
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
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
|
-
- **
|
|
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.
|
|
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
|
|
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
|
-
|
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
- **
|
|
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,8 +19,8 @@ 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:
|
|
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.
|
|
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
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
|
|
|
26
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.
|
|
@@ -31,7 +31,8 @@ Product questions. Ask, wait, never guess.
|
|
|
31
31
|
|
|
32
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
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
|
-
- **
|
|
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.
|
|
35
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.
|
|
36
37
|
- **Create workspaces by hand only for setup and support** (an existing customer you are migrating, or a demo).
|
|
37
38
|
|
|
@@ -43,7 +44,7 @@ Product questions. Ask, wait, never guess.
|
|
|
43
44
|
| Create a workspace by hand | `create_tenant` | `bridge tenant create --name … --owner-email …` |
|
|
44
45
|
| Invite someone into a workspace | `invite_user` | `bridge user invite --email … --role … --tenant-id …` |
|
|
45
46
|
| Change someone's role | `update_user` | `bridge user update --email … --role … --tenant-id …` |
|
|
46
|
-
| Limit seats per plan | `set_plan_quota` (metric `
|
|
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` |
|
|
47
48
|
| Code for the framework | `get_integration_guide` (topic `team`) | `bridge guide svelte team` |
|
|
48
49
|
|
|
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.
|
|
@@ -55,6 +56,6 @@ Pass the role key the developer chose on every invitation you make on their beha
|
|
|
55
56
|
|
|
56
57
|
## Where this connects
|
|
57
58
|
|
|
58
|
-
- **Roles:**
|
|
59
|
+
- **Roles:** the privileges each kind of person has, which flag rules then target.
|
|
59
60
|
- **Payments:** a plan belongs to the workspace, not to a person; seats are a plan limit.
|
|
60
61
|
- **Login:** whether sign-up is open at all.
|
|
@@ -0,0 +1,97 @@
|
|
|
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 one rule for app code
|
|
6
|
+
|
|
7
|
+
**Every gate in app code is a flag.** A link, a page, a button, an endpoint: if some people get it and others do not, the code asks a flag, and the flag's rule says why: a privilege (`privileges contains "USER_WRITE"`), a plan feature (`bridge:billing.entitlement.analytics eq true`) or a rollout. App code never reads a role, a privilege list, the plan or a plan feature to decide what someone may see or do.
|
|
8
|
+
|
|
9
|
+
Two things are not gates and are not flags: **numbers** are plan limits (`<QuotaGate>` in the browser, `@RequireQuota` on the backend), and **permission on one specific record** stays in app code ("only the author edits their post").
|
|
10
|
+
|
|
11
|
+
Before calling the work done, run `npx @nebulr-group/bridge-cli check gates` in the project. It lists every direct role, privilege, plan or plan-feature check with the flag to use instead. Fix each one.
|
|
12
|
+
|
|
13
|
+
- **SvelteKit:** a piece of UI or a nav link is `<FeatureFlag key="…">`; a page is a route rule `{ match: '/admin/*', featureFlag: 'admin-area', redirectTo: '/' }`.
|
|
14
|
+
- **NestJS:** an endpoint is `@RequireFeatureFlag('…')` on the handler.
|
|
15
|
+
|
|
16
|
+
## Why a flag
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
## One question, one tool
|
|
21
|
+
|
|
22
|
+
| The question | The tool | How |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| 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 |
|
|
25
|
+
| 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` |
|
|
26
|
+
| How many do they get (projects, exports, seats)? | a **plan limit** | `bridge plan quota set pro --metric exports --limit 100 --policy hard` |
|
|
27
|
+
| How many people fit in a workspace? | a **plan limit** counted from membership (seats) | `bridge plan quota set pro --metric seats --limit 10 --policy hard --kind gauge --source membership` |
|
|
28
|
+
| 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"` |
|
|
29
|
+
| May this person change this one record? | the app's own code | "Only the author edits their post" |
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
## Where a flag is not the tool
|
|
34
|
+
|
|
35
|
+
- **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.
|
|
36
|
+
- **Seats are a plan limit too, counted from membership.** Bridge counts the workspace's active members; the check sits where invites happen (the team page's seat-limit setting, or `@RequireQuota('seats')` on the app's own invite handler), never in a flag or an entitlement.
|
|
37
|
+
- **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.
|
|
38
|
+
|
|
39
|
+
## Access by who someone is
|
|
40
|
+
|
|
41
|
+
Control it with a flag rule, never with a role check written into the app's code.
|
|
42
|
+
|
|
43
|
+
- **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`.
|
|
44
|
+
- **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.
|
|
45
|
+
- **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: `@RequireFeatureFlag('admin-area')` on the handler. Both read the same rule.
|
|
46
|
+
- **The team page is the same.** The team link and the team route are gated by a flag ruled on the privilege that manages people (for example `privileges contains "USER_WRITE"`, after `bridge role list`), never by a list of role names.
|
|
47
|
+
- **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.
|
|
48
|
+
|
|
49
|
+
## The backend sees the same rule
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
## Limits: count once, where the action happens
|
|
54
|
+
|
|
55
|
+
Ask the developer first: **"Does this action call your server?"** Then:
|
|
56
|
+
|
|
57
|
+
- **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.
|
|
58
|
+
- **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 complete, first-class way to run limits.
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
## What people see when something is off
|
|
65
|
+
|
|
66
|
+
A flag that is off says why, and the app shows each reason differently:
|
|
67
|
+
|
|
68
|
+
| Why it is off | In the browser | From the backend |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| Not on their plan | The upgrade dialog, naming the plans that include it | `402 FEATURE_NOT_IN_PLAN` |
|
|
71
|
+
| Their role or privileges | Hidden, or a line telling them to ask an admin | `403 FEATURE_NOT_PERMITTED` |
|
|
72
|
+
| Switched off | Hidden | `403 FEATURE_OFF` |
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
## Worked example
|
|
77
|
+
|
|
78
|
+
"Analytics on Pro. Only people who manage the workspace see settings. 100 exports a month on Pro."
|
|
79
|
+
|
|
80
|
+
1. `bridge plan feature add pro analytics --name "Analytics"`, and a flag `analytics` with the rule `bridge:billing.entitlement.analytics eq true`.
|
|
81
|
+
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/*`.
|
|
82
|
+
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.
|
|
83
|
+
4. `npx @nebulr-group/bridge-cli check gates`: nothing left that reads a role, a privilege, the plan or a plan feature directly.
|
|
84
|
+
|
|
85
|
+
## Exceptions
|
|
86
|
+
|
|
87
|
+
**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 are for the rare case where the developer explicitly asks for no flag, and they print a one-time note in development. Mark each such line `// bridge-gate-exception: <reason>` so `check gates` leaves it. Use a flag by default: it can also carry a rollout, a trial group or a one-customer exception later, without a code change.
|
|
88
|
+
|
|
89
|
+
## Where each piece is decided
|
|
90
|
+
|
|
91
|
+
| Area | Decision guide |
|
|
92
|
+
|---|---|
|
|
93
|
+
| Flags and their rules | `bridge guide decision feature-control` |
|
|
94
|
+
| Roles and privileges | `bridge guide decision roles` |
|
|
95
|
+
| Plans, features and limits | `bridge guide decision payments` |
|
|
96
|
+
| Who is in a workspace | `bridge guide decision teams` |
|
|
97
|
+
| How limits, the upgrade dialog and page customisation work | `bridge guide mechanisms` |
|
|
@@ -2,6 +2,10 @@
|
|
|
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
|
+
> **The one rule for app code: every gate is a flag.** A link, a page, a button, an endpoint: if some people get it and others do not, the code asks a flag, and the flag's rule says why (a privilege, a plan feature, a rollout). App code never reads a role, a privilege list, the plan or a plan feature to decide what someone may see or do. Numbers are plan limits, and permission on one specific record stays in app code. `bridge guide fit-together` has the whole rule; the last verification step runs `npx @nebulr-group/bridge-cli check gates`.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
5
9
|
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
10
|
|
|
7
11
|
## Where per-framework prompts live
|
|
@@ -193,6 +197,7 @@ For each integrated project:
|
|
|
193
197
|
1. **Build check** — run the project's build command (from `package.json` scripts)
|
|
194
198
|
2. **Eval check** — read one flag (existing or freshly created) and confirm it returns the expected value
|
|
195
199
|
3. **Context check** — confirm `identity` is set when the project uses any rolled-out rule
|
|
200
|
+
4. **Gate check** — `npx @nebulr-group/bridge-cli check gates` in the project: every direct role, privilege, plan or plan-feature check it lists becomes a flag. Run it again until it is clean
|
|
196
201
|
|
|
197
202
|
If anything fails, diagnose and fix before moving on.
|
|
198
203
|
|
|
@@ -212,4 +217,4 @@ After delivering the message, the flags integration is complete.
|
|
|
212
217
|
|
|
213
218
|
**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
219
|
|
|
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 `
|
|
220
|
+
**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` (
|
|
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
|
|
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.
|
|
103
|
+
## 1. Decide once, where the action happens
|
|
104
|
+
|
|
105
|
+
Ask the developer first: **does this action call your server?**
|
|
106
|
+
|
|
107
|
+
- **It calls your backend:** the backend handler counts it and refuses at the limit, because that is where the action happens. The frontend shows the count and the upgrade dialog, 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.
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
Never both for one metric: it would be counted twice.
|
|
104
111
|
|
|
105
112
|
## 2. A POST increments the limit
|
|
106
113
|
|
|
@@ -142,13 +149,13 @@ remove() { /* … */ }
|
|
|
142
149
|
|
|
143
150
|
`current` returns your own count, and that count is what the limit is compared against, so Bridge's copy heals itself if it ever missed an update. There is no decrement and no reservation: every create and delete sends the whole count.
|
|
144
151
|
|
|
145
|
-
**Seats** (`
|
|
152
|
+
**Seats** are a gauge the app names (e.g. `seats`) and marks as counted from membership: `bridge plan quota set <plan> --metric seats --limit 10 --policy hard --kind gauge --source membership`. Bridge answers it with the workspace's active members, pending invites included, read fresh each time; the app never reports it. Check it where invites happen: on the built-in team page with `<TeamManagementPanel seatsMetric="seats" />`, or with `@RequireQuota('seats')` on your own invite handler (it checks and writes nothing). Bridge's invite API does not refuse at the limit by itself. Never gate seats with a flag or an entitlement.
|
|
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.
|
|
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
|
|
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 (section Exceptions, at the end). 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; `<
|
|
161
|
-
| **2 — your own UI** | `useQuota('tickets')
|
|
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 |
|
|
168
|
+
| **2 — your own UI** | `useQuota('tickets')`, and `<FeatureFlag key="analytics">` around what the plan sells | Whatever you build from the live numbers |
|
|
162
169
|
|
|
163
170
|
```svelte
|
|
164
171
|
<script lang="ts">
|
|
165
|
-
import { QuotaGate,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
201
|
+
## 5. Counting in the browser
|
|
194
202
|
|
|
195
|
-
|
|
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
|
-
|
|
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 complete, first-class way to run limits. 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
|
|
|
@@ -282,3 +292,7 @@ All default rules use `:where()`, so any selector of yours wins without `!import
|
|
|
282
292
|
| Backend sign-in and route protection | `bridge guide nestjs` |
|
|
283
293
|
| Feature flags | `bridge guide flags` |
|
|
284
294
|
| Proving sign-in works | `bridge test-user create`, then `bridge test-user verify` |
|
|
295
|
+
|
|
296
|
+
## Exceptions
|
|
297
|
+
|
|
298
|
+
Every gate in app code is a flag (`bridge guide fit-together`). 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.
|
|
@@ -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,
|
|
12
|
-
- **Payments and plans.** Plans with prices, trials
|
|
13
|
-
- **Feature control.**
|
|
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
|
|
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
|
|
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
|
{
|