@nebulr-group/bridge-cli 0.6.0-beta.3 → 0.6.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/commands/flag.command.d.ts.map +1 -1
  2. package/dist/commands/flag.command.js +19 -6
  3. package/dist/commands/flag.command.js.map +1 -1
  4. package/dist/commands/guide.command.d.ts +3 -0
  5. package/dist/commands/guide.command.d.ts.map +1 -1
  6. package/dist/commands/guide.command.js +57 -5
  7. package/dist/commands/guide.command.js.map +1 -1
  8. package/dist/commands/journeys.d.ts +2 -1
  9. package/dist/commands/journeys.d.ts.map +1 -1
  10. package/dist/commands/journeys.js +15 -1
  11. package/dist/commands/journeys.js.map +1 -1
  12. package/dist/commands/plan.command.d.ts +39 -0
  13. package/dist/commands/plan.command.d.ts.map +1 -1
  14. package/dist/commands/plan.command.js +134 -10
  15. package/dist/commands/plan.command.js.map +1 -1
  16. package/dist/commands/role.command.d.ts.map +1 -1
  17. package/dist/commands/role.command.js +2 -1
  18. package/dist/commands/role.command.js.map +1 -1
  19. package/dist/commands/tenant.command.d.ts.map +1 -1
  20. package/dist/commands/tenant.command.js +12 -5
  21. package/dist/commands/tenant.command.js.map +1 -1
  22. package/dist/commands/token.command.d.ts.map +1 -1
  23. package/dist/commands/token.command.js +6 -0
  24. package/dist/commands/token.command.js.map +1 -1
  25. package/dist/commands/user.command.d.ts.map +1 -1
  26. package/dist/commands/user.command.js +6 -0
  27. package/dist/commands/user.command.js.map +1 -1
  28. package/dist/flag-coherence.d.ts +42 -0
  29. package/dist/flag-coherence.d.ts.map +1 -0
  30. package/dist/flag-coherence.js +103 -0
  31. package/dist/flag-coherence.js.map +1 -0
  32. package/dist/preview.d.ts +9 -0
  33. package/dist/preview.d.ts.map +1 -0
  34. package/dist/preview.js +63 -0
  35. package/dist/preview.js.map +1 -0
  36. package/dist/prompts/billing/master.md +4 -1
  37. package/dist/prompts/decisions/feature-control.md +105 -0
  38. package/dist/prompts/decisions/going-live.md +59 -0
  39. package/dist/prompts/decisions/login.md +68 -0
  40. package/dist/prompts/decisions/look-and-feel.md +62 -0
  41. package/dist/prompts/decisions/payments.md +81 -0
  42. package/dist/prompts/decisions/roles.md +72 -0
  43. package/dist/prompts/decisions/teams.md +56 -0
  44. package/dist/prompts/mcp-tools.json +77 -0
  45. package/dist/prompts/orientation.md +40 -0
  46. package/dist/shape.d.ts +25 -0
  47. package/dist/shape.d.ts.map +1 -0
  48. package/dist/shape.js +74 -0
  49. package/dist/shape.js.map +1 -0
  50. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ import { getManagementClient } from './config.js';
2
+ /** Attributes whose value names a plan (the JWT `plan` claim, billing slug). */
3
+ const PLAN_ATTRIBUTES = ['tenant.plan', 'bridge:billing.plan'];
4
+ /** Attributes whose value names a role (the JWT `role` claim). */
5
+ const ROLE_ATTRIBUTES = ['user.role'];
6
+ /** Operators whose values are compared for equality with the attribute. */
7
+ const EQUALITY_OPERATORS = ['eq', 'neq', 'in', 'not_in'];
8
+ function sameValue(a, b) {
9
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
10
+ }
11
+ function listed(entries) {
12
+ return entries.length ? entries.map((e) => e.key).join(', ') : '(none)';
13
+ }
14
+ /**
15
+ * The warnings for one flag. Pure. `catalog` omitted skips the
16
+ * dangling-reference check (the caller could not read plans/roles).
17
+ */
18
+ export function flagCoherenceWarnings(flag, catalog) {
19
+ const warnings = [];
20
+ const state = flag.state ?? 'off';
21
+ const valueType = flag.valueType ?? 'boolean';
22
+ const onValue = flag.onValue !== undefined ? flag.onValue : true;
23
+ const offValue = flag.offValue !== undefined ? flag.offValue : false;
24
+ if (state === 'on') {
25
+ if (valueType === 'boolean' && onValue === false) {
26
+ warnings.push(`Flag "${flag.key}" is switched on, but its onValue is false, so it returns false for ` +
27
+ 'everyone: it looks on and behaves off. Set onValue to true, or switch the flag off.');
28
+ }
29
+ else if (sameValue(onValue, offValue)) {
30
+ warnings.push(`Flag "${flag.key}" is switched on, but its onValue equals its offValue ` +
31
+ `(${JSON.stringify(onValue)}), so switching it on or off changes nothing.`);
32
+ }
33
+ }
34
+ if (catalog && flag.rule) {
35
+ for (const branch of flag.rule.branches ?? []) {
36
+ for (const condition of branch.conditions ?? []) {
37
+ if (!EQUALITY_OPERATORS.includes(condition.operator))
38
+ continue;
39
+ const isPlan = PLAN_ATTRIBUTES.includes(condition.attribute);
40
+ const isRole = ROLE_ATTRIBUTES.includes(condition.attribute);
41
+ if (!isPlan && !isRole)
42
+ continue;
43
+ const known = isPlan ? catalog.plans : catalog.roles;
44
+ const keys = new Set(known.map((k) => k.key));
45
+ const what = isPlan ? 'plan' : 'role';
46
+ for (const value of condition.values ?? []) {
47
+ if (typeof value !== 'string' || keys.has(value))
48
+ continue;
49
+ // The token carries the plan/role KEY, so a display name never matches.
50
+ const byName = known.find((k) => k.name === value);
51
+ if (byName) {
52
+ warnings.push(`Flag "${flag.key}" has a rule on ${condition.attribute} ${condition.operator} ` +
53
+ `"${value}", which matches the ${what} named "${value}" by name; rules compare the ` +
54
+ `${what} key — use "${byName.key}".`);
55
+ continue;
56
+ }
57
+ warnings.push(`Flag "${flag.key}" has a rule on ${condition.attribute} ${condition.operator} ` +
58
+ `"${value}", but the app has no ${what} "${value}", so no real ` +
59
+ `${isPlan ? 'workspace' : 'user'} can carry it. Existing ${what} keys: ` +
60
+ `${listed(known)}.`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ return warnings;
66
+ }
67
+ /** True when any condition reads a plan or role attribute (worth fetching the catalog). */
68
+ export function ruleReadsPlanOrRole(rule) {
69
+ if (!rule)
70
+ return false;
71
+ return (rule.branches ?? []).some((b) => (b.conditions ?? []).some((c) => PLAN_ATTRIBUTES.includes(c.attribute) || ROLE_ATTRIBUTES.includes(c.attribute)));
72
+ }
73
+ /**
74
+ * Plans and roles for the dangling-reference check. Never throws: a login
75
+ * that cannot read plans or roles gets no dangling-reference warning rather
76
+ * than a failed flag command — a warning never blocks.
77
+ */
78
+ export async function loadCoherenceCatalog() {
79
+ try {
80
+ const client = getManagementClient();
81
+ const [plans, roles] = await Promise.all([client.plans.list(), client.roles.list()]);
82
+ return {
83
+ plans: (plans ?? []).map((p) => ({ key: String(p.key), name: p.name })),
84
+ roles: (roles ?? []).map((r) => ({ key: String(r.key), name: r.name })),
85
+ };
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ /** Warnings for a list of flags, fetching the catalog once and only if a rule needs it. */
92
+ export async function warningsForFlags(flags) {
93
+ const catalog = flags.some((f) => ruleReadsPlanOrRole(f.rule)) ? await loadCoherenceCatalog() : undefined;
94
+ return flags.map((f) => flagCoherenceWarnings(f, catalog));
95
+ }
96
+ /** A flag with its warnings attached — only when there are any, so a coherent flag prints as before. */
97
+ export async function withFlagWarnings(flag) {
98
+ if (!flag || typeof flag !== 'object')
99
+ return flag;
100
+ const [warnings] = await warningsForFlags([flag]);
101
+ return warnings.length ? { ...flag, warnings } : flag;
102
+ }
103
+ //# sourceMappingURL=flag-coherence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flag-coherence.js","sourceRoot":"","sources":["../src/flag-coherence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export declare class PreviewNotSupportedError extends Error {
2
+ readonly path: string;
3
+ readonly serverBody?: unknown | undefined;
4
+ readonly code = "PREVIEW_NOT_SUPPORTED";
5
+ constructor(path: string, serverBody?: unknown | undefined);
6
+ }
7
+ export declare function previewPath(path: string): string;
8
+ export declare function dryRunPreview(path: string, headers?: Record<string, string>): Promise<unknown>;
9
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAmBA,qBAAa,wBAAyB,SAAQ,KAAK;IAErC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO;IADhE,QAAQ,CAAC,IAAI,2BAA2B;gBACnB,IAAI,EAAE,MAAM,EAAW,UAAU,CAAC,EAAE,OAAO,YAAA;CAOjE;AAcD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBpG"}
@@ -0,0 +1,63 @@
1
+ import { HttpError } from '@nebulr-group/bridge-auth-core';
2
+ import { getManagementHttp } from './config.js';
3
+ /*
4
+ * TBP-709 (E) — `--dry-run` on the destructive commands.
5
+ *
6
+ * The preview is the server's: every delete route has a read-only sibling,
7
+ * `GET <route>/deletion-preview`, that answers with what the delete would
8
+ * remove and removes nothing. It passes the same guards as the delete, so a
9
+ * login without the destructive privilege gets the same 403 — and it is the
10
+ * same answer the MCP tools' `dryRun` shows.
11
+ *
12
+ * Why a GET on its own path, and never `?dryRun=true` on the DELETE: this CLI
13
+ * is released separately from the server and is pointed at servers that
14
+ * predate previews. Such a server ignores an unknown query parameter and
15
+ * PERFORMS THE DELETE; it answers an unknown path with 404. So a 404 here
16
+ * means "no previews on this server", and the command stops — it never falls
17
+ * back to the delete.
18
+ */
19
+ export class PreviewNotSupportedError extends Error {
20
+ path;
21
+ serverBody;
22
+ code = 'PREVIEW_NOT_SUPPORTED';
23
+ constructor(path, serverBody) {
24
+ super(`This Bridge server does not support deletion previews yet (GET ${path} answered 404). ` +
25
+ 'Nothing was deleted. Run without --dry-run only if you mean to delete.');
26
+ this.path = path;
27
+ this.serverBody = serverBody;
28
+ this.name = 'PreviewNotSupportedError';
29
+ }
30
+ }
31
+ /*
32
+ * A server that HAS the route still 404s a record that does not exist, with
33
+ * its own message ("tenant not found"). An unknown route gets the framework's
34
+ * "Cannot GET …" (or a bare "Not Found" from a proxy in front). Only the
35
+ * former is passed through as-is; either way the command fails and deletes
36
+ * nothing — this only decides which message the user reads.
37
+ */
38
+ function isRecordNotFound(body) {
39
+ const message = body && typeof body === 'object' ? body.message : undefined;
40
+ return typeof message === 'string' && !/^Cannot GET\b/.test(message) && message !== 'Not Found';
41
+ }
42
+ export function previewPath(path) {
43
+ return `${path}/deletion-preview`;
44
+ }
45
+ export async function dryRunPreview(path, headers) {
46
+ const route = previewPath(path);
47
+ let preview;
48
+ try {
49
+ preview = await getManagementHttp().get(route, headers);
50
+ }
51
+ catch (err) {
52
+ if (err instanceof HttpError && err.status === 404 && !isRecordNotFound(err.body)) {
53
+ throw new PreviewNotSupportedError(route, err.body);
54
+ }
55
+ throw err;
56
+ }
57
+ return {
58
+ ...(preview && typeof preview === 'object' ? preview : {}),
59
+ dryRun: true,
60
+ next: 'Nothing was changed. Run the same command without --dry-run to go ahead.',
61
+ };
62
+ }
63
+ //# sourceMappingURL=preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IAE5B;IAAuB;IADnC,IAAI,GAAG,uBAAuB,CAAC;IACxC,YAAqB,IAAY,EAAW,UAAoB;QAC9D,KAAK,CACH,kEAAkE,IAAI,kBAAkB;YACtF,wEAAwE,CAC3E,CAAC;QAJiB,SAAI,GAAJ,IAAI,CAAQ;QAAW,eAAU,GAAV,UAAU,CAAU;QAK9D,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,IAAa;IACrC,MAAM,OAAO,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,IAA8B,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACvG,OAAO,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,WAAW,CAAC;AAClG,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,GAAG,IAAI,mBAAmB,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,OAAgC;IAChF,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,OAAgC,CAAC;IACrC,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC,GAAG,CAA0B,KAAK,EAAE,OAAO,CAAC,CAAC;IACnF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,SAAS,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAClF,MAAM,IAAI,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QACtD,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,OAAO;QACL,GAAG,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,MAAM,EAAE,IAAI;QACZ,IAAI,EAAE,0EAA0E;KACjF,CAAC;AACJ,CAAC"}
@@ -141,9 +141,12 @@ and payment failures will not sync. Tell the developer and follow the repair hin
141
141
  If not connected, ask the developer for their Stripe keys (from `dashboard.stripe.com/apikeys`):
142
142
 
143
143
  ```bash
144
- bridge stripe connect --secret-key sk_test_... --publishable-key pk_test_...
144
+ bridge setup payments --stripe-key sk_test_... --stripe-public-key pk_test_...
145
145
  ```
146
146
 
147
+ This stores the keys **and** switches payments on in one step. (`bridge stripe connect` only
148
+ stores the keys and leaves payments off.)
149
+
147
150
  Now set any **remaining** prices — one command per Prices-table row not already covered by the
148
151
  `plan create` call above. `plan price set` is idempotent (keyed on currency + interval), so a
149
152
  tier with monthly **and** yearly pricing is just one more call against the same plan key:
@@ -0,0 +1,105 @@
1
+ # Feature control — decision guide
2
+
3
+ Turning features on and off for some people and not others, without a deploy. The tool is a **flag**: code asks for it by key, and the flag's rule decides the answer.
4
+
5
+ > 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
+ - **Is a feature on for them? A flag.** Always, and for good. The flag is the stable place in code; the *reasons* live in its rule and change as the business does: 10% of users today, "plan is Pro" tomorrow, one named customer as an exception next month. Each of those is a rule edit, not new code.
10
+ - **How much do they get? The plan.** Prices, trials and limits live on the plan: see the **payments** guide.
11
+ - **Who inside a workspace may do it? A role.** See the **roles** guide.
12
+
13
+ A plan can also grant a feature on its own (a *plan feature*, checked with `@RequireEntitlement` on the backend and `<Entitled>` in the UI; the payments guide explains it). Use that only when the feature *is* what the plan sells and nothing else will ever decide it: the backend then refuses it without extra wiring. Anything that could ever need a rollout, a trial cohort or a one-customer exception is a flag.
14
+
15
+ ## Start from what is there
16
+
17
+ | Read | MCP | CLI |
18
+ |---|---|---|
19
+ | Every flag with its rule, and warnings on flags that cannot mean what they look like | `list_feature_flags` | `bridge flag list` |
20
+ | One flag | `get_flag` | `bridge flag get <key>` |
21
+ | Plan keys and role keys, for rules | `list_plans`, `list_roles` | `bridge plan list`, `bridge role list` |
22
+
23
+ ## Ask the developer
24
+
25
+ Product questions. Ask, wait, never guess.
26
+
27
+ 1. **Which feature, in their words, and who should get it?** Everyone, nobody yet, a percentage of users, one plan, some roles, named customers, or a mix.
28
+ 2. **What do people without it see?** Nothing at all, a teaser with an upgrade link, or the old version.
29
+ 3. **Should it switch on or off by itself at a set time?**
30
+ 4. **If it is tied to a plan: is it simply part of that plan for ever,** or might it later go to a trial group or a single customer too? (This picks flag or plan feature, above.)
31
+
32
+ ## Decide yourself
33
+
34
+ - **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.
35
+ - **Boolean unless the developer needs variants.** Reason: on/off covers almost every case and reads plainly in code.
36
+ - **Target a plan by its key (`pro`), from `list_plans`, never its display name,** whatever an example elsewhere shows. Reason: the app sends the key.
37
+ - **Always give the rule an `otherwiseValue`, and make it the safe answer (usually `false`).** Reason: anyone the rule does not match gets it.
38
+ - **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.
39
+ - **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".
40
+ - **Treat a warning from `list_feature_flags` as a bug to fix now.**
41
+
42
+ ## The attributes a rule can test
43
+
44
+ These are the exact names and values. A rule on any other name matches nothing unless the app sends that attribute itself.
45
+
46
+ **Sent by the browser SDKs** (SvelteKit, React, Next.js, Angular) for a signed-in person, read from their token:
47
+
48
+ | Attribute | Value | Compared |
49
+ |---|---|---|
50
+ | `user.id` | the person's id | exactly |
51
+ | `user.email` | their email | exactly |
52
+ | `user.role` | the role **key** as created, e.g. `OWNER`, `ADMIN` | exactly: `admin` does not match `ADMIN` |
53
+ | `tenant.id` | the workspace id | exactly |
54
+ | `tenant.plan` | the plan **key**, e.g. `pro` (not the name) | ignoring case: `Pro` matches `pro` |
55
+ | `privileges` | a list of privilege keys | as a list; target a role instead |
56
+
57
+ **Also sent by the browser SDKs, from the workspace's subscription:**
58
+
59
+ | Attribute | Value |
60
+ |---|---|
61
+ | `bridge:billing.plan` | the plan key, compared exactly |
62
+ | `bridge:billing.subscription.status` | `trial`, `active`, `past_due`, `cancel_at_period_end` or `canceled` |
63
+ | `bridge:billing.trial` | `true` or `false` |
64
+ | `bridge:billing.quota.<metric>.used` / `.limit` / `.remaining` | numbers |
65
+ | `bridge:billing.quota.<metric>.percent_used` | a fraction: `0.8` is 80% |
66
+ | `bridge:billing.entitlement.<name>` | `true` or `false`, including `app_active` (the subscription is active, trialing, past due or cancelling) |
67
+
68
+ **On the NestJS backend** only the signed-in person's id is filled in (as the identity). A rule on `tenant.plan` or `user.role` sees nothing there until the app passes those values from the verified user on each call; the NestJS flags guide shows how. Never pass values the client sent.
69
+
70
+ **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.
71
+
72
+ Operators: `eq`, `neq`, `in`, `not_in`, `contains`, `not_contains`, `gt`, `lt`, `between`, `regex`, `exists`, `not_exists`.
73
+
74
+ A condition's values are always a list, even for a single value.
75
+
76
+ ## Do it
77
+
78
+ | Step | MCP | CLI |
79
+ |---|---|---|
80
+ | Create a flag with its rule | `create_feature_flag` | `bridge flag create --key analytics --state on-with-rule --rule '<json>'` |
81
+ | Change a rule or values | `update_feature_flag` | `bridge flag update --key analytics --rule '<json>'` |
82
+ | 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` |
83
+ | Check who gets what | `evaluate_feature_flag` | `bridge flag eval analytics --identity u1 --attribute tenant.plan=pro` |
84
+ | Switch at a set time | `set_flag_schedule` | `bridge flag schedule set analytics --at … --state on` |
85
+ | Copy flags between apps | `export_feature_flags`, `import_feature_flags` | `bridge flag export`, `bridge flag import <file>` |
86
+ | Code for the framework | `get_integration_guide` (topic `feature-flags`) | `bridge guide flags --framework svelte` |
87
+
88
+ A rule for "Pro only":
89
+
90
+ ```json
91
+ { "branches": [{ "conditions": [{ "attribute": "tenant.plan", "operator": "eq", "values": ["pro"] }], "returnValue": true }],
92
+ "otherwiseValue": false, "rolloutPct": 100 }
93
+ ```
94
+
95
+ ## Prove it
96
+
97
+ 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.
98
+ 2. `list_feature_flags` / `bridge flag list` shows no warnings on the flag.
99
+ 3. In the app, as a test user on each side of the rule.
100
+
101
+ ## Where this connects
102
+
103
+ - **Payments:** plan keys, and plan features versus flags.
104
+ - **Roles:** role keys for `user.role`, and why a flag is not a permission.
105
+ - **Going live:** flags are per app; a production app needs its own (`export_feature_flags`, then `import_feature_flags`).
@@ -0,0 +1,59 @@
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
+ > 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
+ ## Decide yourself
24
+
25
+ - **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`.
26
+ - **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.
27
+ - **Add production addresses; remove localhost ones only from the production app.** Reason: removing them from the development app breaks local work.
28
+ - **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.
29
+ - **Register the production callback in every social provider's console** (`setup_sso` returns it). Reason: providers refuse callbacks they do not know.
30
+ - **Never put a secret key in a browser variable** (`VITE_*`, `NEXT_PUBLIC_*`). Reason: everything in those ships to every visitor. `diagnose_integration` flags it.
31
+ - **A test user on a production app needs the developer's say-so.** Reason: it creates a workspace in the app real users share; the tool refuses without confirmation.
32
+
33
+ ## Do it
34
+
35
+ | Step | MCP | CLI |
36
+ |---|---|---|
37
+ | Readiness checklist | `get_setup_status` | `bridge setup status` |
38
+ | Check a project's env files | `diagnose_integration` | `bridge diagnose` |
39
+ | Create a production app | `create_app`, then `use_app` | — (create it in the dashboard, then `bridge auth login --app <id>`) |
40
+ | Production URL, default callback, allowed origins | `update_app` | `bridge app update --ui-url … --default-callback-uri … --allowed-origins …` |
41
+ | Production callback URL | `add_redirect_uri` | `bridge app redirect-uris add <url>` |
42
+ | Live Stripe keys | `setup_payments` | `bridge setup payments --stripe-key sk_live_… --stripe-public-key pk_live_…` |
43
+ | Email sender | `setup_communication` | `bridge setup communication --from-address … --from-name …` |
44
+ | Copy flags | `export_feature_flags`, `import_feature_flags` | `bridge flag export --out flags.json`, `bridge flag import flags.json` |
45
+ | Copy a plan | `apply_plan` | `bridge plan apply --spec @plan.json` |
46
+
47
+ ## Prove it
48
+
49
+ 1. `get_setup_status` / `bridge setup status` again: show the developer what is left. Nothing in `productionNeeds` should be a surprise to them.
50
+ 2. With the developer's agreement, `create_test_user` then `verify_login` (`bridge test-user create --confirm "<app name>"`, then `bridge test-user verify`) against the production app: a real sign-in from the production origin.
51
+ 3. `get_stripe_status` / `bridge stripe status`: live keys, webhook working.
52
+ 4. The first real sign-in appears on the checklist once someone signs up.
53
+
54
+ ## Where this connects
55
+
56
+ - **Login:** redirect URIs, origins and provider credentials.
57
+ - **Payments:** live keys, and plans on the production app.
58
+ - **Feature control:** flags are per app.
59
+ - **Look and feel:** a confirmed sender address.
@@ -0,0 +1,68 @@
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
+ > 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. **Should sign-in happen on Bridge's hosted page, or inside the app on its own address?** Both use the same one route file; the look-and-feel guide covers what each looks like.
26
+ 4. **Can anyone sign up, or only people who are invited?** This is a teams question: see the **teams** guide before changing it.
27
+ 5. **For each social provider they pick: the client id and client secret** from that provider's console. Never invent, guess or reuse a credential.
28
+
29
+ ## Decide yourself
30
+
31
+ Apply these without asking. Say the reason in one line.
32
+
33
+ - **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.
34
+ - **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.
35
+ - **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.
36
+ - **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 Bridge's hosted pages.
37
+ - **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.
38
+ - **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.
39
+ - **Protect every route by default; make only `/auth/*` public.** Reason: a new page is private until someone decides otherwise.
40
+
41
+ ## Do it
42
+
43
+ | Step | MCP | CLI |
44
+ |---|---|---|
45
+ | Turn magic link, passkeys or two-factor on or off | `update_auth_methods` | `bridge app update --magic-link-enabled true` (also `--passkeys-enabled`, `--mfa-enabled`) |
46
+ | Add a social provider with its credentials | `setup_sso` | `bridge setup sso --provider google --client-id … --client-secret …` |
47
+ | Register the callback URL | `add_redirect_uri` | `bridge app redirect-uris add <url>` |
48
+ | Set UI URL, default callback, allowed origins | `update_app` | `bridge app update --ui-url … --default-callback-uri … --allowed-origins …` |
49
+ | Change token lifetimes (seconds) | `update_password_policy` | `bridge auth password-policy --access-token-ttl 900` |
50
+ | Code for the framework | `get_integration_guide` (topic `auth`) | `bridge guide svelte` (hosted) or `bridge guide svelte sdk-auth` (in-app) |
51
+
52
+ `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` can switch a provider on but never saves its credentials.
53
+
54
+ ## Prove it
55
+
56
+ Do not say "it should work". Show it working.
57
+
58
+ 1. `diagnose_integration` / `bridge diagnose`: compares the project's env files with the real app and names every difference with its fix.
59
+ 2. `create_test_user`, then `verify_login` / `bridge test-user create`, then `bridge test-user verify`: a real sign-in, with the token checked.
60
+ 3. If sign-in works there but not in the app, the app code is the difference.
61
+ 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.
62
+
63
+ ## Where this connects
64
+
65
+ - **Teams:** who can sign up, and whether they create a workspace or join one.
66
+ - **Roles:** what a person can do once signed in.
67
+ - **Look and feel:** hosted or in-app sign-in, and how the pages look.
68
+ - **Going live:** production URLs and redirect URIs replace the localhost ones.
@@ -0,0 +1,62 @@
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
+ > 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
+ ## Two places pages can live
8
+
9
+ - **Hosted:** Bridge serves sign-in on its own address. Styled with the app's **branding** (colours, font, corner radius), which Bridge stores.
10
+ - **In the app:** the same pages render inside the developer's own layout, on their address. Styled with CSS design tokens (`--bridge-*`) in the app's own stylesheet.
11
+
12
+ Both use the same single route file; one setting switches sign-in into the app (`loginRoute: '/auth/login'` in SvelteKit's `bridgeBootstrap()`). Subscription pages always render in the app.
13
+
14
+ ## Start from what is there
15
+
16
+ | Read | MCP | CLI |
17
+ |---|---|---|
18
+ | Hosted-page branding | `get_branding` | `bridge branding get` |
19
+ | App name, and the email sender | `get_app` | `bridge app get` |
20
+
21
+ ## Ask the developer
22
+
23
+ Product questions. Ask, wait, never guess.
24
+
25
+ 1. **Should sign-in look like part of the app, on the app's own address, or is Bridge's hosted page fine?**
26
+ 2. **What are the brand colours and font?** Ask for exact values or point at their existing stylesheet; do not pick colours for them.
27
+ 3. **What name should users see** on sign-in pages and in emails?
28
+ 4. **Which address and sender name should emails come from?** (Invitations, password resets, magic links.)
29
+ 5. **Beyond colours, should sign-in pages say anything of their own?** A different heading, a frame around the form, a page they design themselves.
30
+
31
+ ## Decide yourself
32
+
33
+ Climb only as far as the answers require, and say which level you stopped at.
34
+
35
+ - **Level 0, nothing:** in-app pages already sit inside the app's layout, so its header and navigation surround them. Reason: often this alone is enough.
36
+ - **Level 1, tokens:** set `--bridge-primary`, `--bridge-border-radius` and friends on `:root`. Reason: tokens are the supported contract; ordinary CSS already wins over Bridge's defaults, so there is never a need for `!important`.
37
+ - **Level 2, frame and heading:** pass the `frame` and `heading` snippets to the sign-in routes component. Reason: it changes every page at once and keeps each page's own sub-steps.
38
+ - **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.
39
+ - **Level 4, headless:** build everything on `getBridgeAuth()`. Only when the developer asks for it.
40
+ - **Never copy Bridge's components into the app to restyle them.** Reason: the copy stops receiving fixes.
41
+ - **When both hosted pages and in-app pages are used, give them the same colours.** Reason: a person moving between them should not notice.
42
+ - **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.
43
+
44
+ ## Do it
45
+
46
+ | Step | MCP | CLI |
47
+ |---|---|---|
48
+ | Hosted-page colours, font, corners | `update_branding` | `bridge branding update --primary-btn-bg '#1a1a2e' --font-family Inter` |
49
+ | Name users see | `update_app` (`name`) | `bridge app update --name "Acme"` |
50
+ | Email sender | `setup_communication` | `bridge setup communication --from-address no-reply@acme.com --from-name Acme` |
51
+ | In-app pages, tokens, snippets | `get_integration_guide` (topic `branding`, then `auth`) | `bridge guide svelte branding`, `bridge guide mechanisms` |
52
+
53
+ ## Prove it
54
+
55
+ 1. Open the sign-in page and the subscription page in the running app (or the hosted page): colours and font match what the developer gave.
56
+ 2. Trigger a password reset for a test user: the email arrives from the agreed sender, in the app's name, and its link opens the app, not a Bridge page.
57
+
58
+ ## Where this connects
59
+
60
+ - **Login:** hosted or in-app sign-in is the same switch.
61
+ - **Payments:** the subscription and plan-choice pages use the same tokens.
62
+ - **Going live:** a verified sender address before real users arrive.
@@ -0,0 +1,81 @@
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 and the limits ("3 projects on Free, 50 on Pro"). Payments run through the developer's own Stripe account.
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`.
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. **Which features are only on some plans?** ("Analytics on Pro only.")
26
+ 7. **Do they want a welcome page for new customers** where they choose a plan? Offer it; create it only if they say yes.
27
+
28
+ ## Decide yourself
29
+
30
+ - **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.
31
+ - **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.
32
+ - **A free plan gets a price of 0.** Reason: a plan with no price cannot be assigned to a workspace.
33
+ - **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.
34
+ - **Seats are the built-in `users` metric, a gauge.** Reason: Bridge counts workspace members itself.
35
+ - **Reuse metric names the app already has** (`list_plan_quotas`) before inventing new ones. Reason: two names for one thing split the count.
36
+ - **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.
37
+ - **Connect Stripe before adding paid prices.** Reason: each price is created in Stripe as it is saved.
38
+ - **Leave "customers must pick a plan" on** (it is on by default) unless the developer says otherwise. Reason: a new workspace then always has a plan, so every limit and feature check has something to read.
39
+ - **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.
40
+
41
+ ## How a hard limit is enforced, end to end
42
+
43
+ Tell the developer this plainly; it is where integrations go wrong.
44
+
45
+ 1. **The plan holds the number.** A limit is a quota on the plan: metric, limit, `hard`, and `counter` or `gauge`.
46
+ 2. **The backend enforces it, and only the backend.** 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.
47
+ 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.
48
+ 4. **Seats need no count at all.** `@RequireQuota('users')` on the invite handler checks the seat limit.
49
+ 5. **The frontend only explains the refusal.** 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.
50
+ 6. **A frontend alone cannot enforce a limit.** Anyone can call the API directly. An app with no backend can report usage from the browser, but that is self-reported: Bridge counts what the client says.
51
+ 7. **`metered` never blocks.** Past the included amount it bills per unit through Stripe.
52
+ 8. **A plan feature is a hard limit nothing counts.** `analytics` with limit 1 on `pro` makes `@RequireEntitlement('analytics')` and `<Entitled to="analytics">` true on Pro and false elsewhere. Never put `@RequireEntitlement` and `@RequireQuota` on the same name: at the cap the entitlement answers `403` first and the upgrade dialog never opens.
53
+ 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`.
54
+
55
+ Whether a paid feature is a plan feature or a flag with a plan rule is decided in the **feature-control** guide.
56
+
57
+ ## Do it
58
+
59
+ | Step | MCP | CLI |
60
+ |---|---|---|
61
+ | Connect Stripe (keys from the developer) | `setup_payments` | `bridge setup payments --stripe-key sk_test_… --stripe-public-key pk_test_…` |
62
+ | Set up or reshape one plan in one call | `apply_plan` | `bridge plan apply --spec @plan.json` |
63
+ | Or step by step: create a plan with its first price | `create_plan` | `bridge plan create --key pro --name Pro --amount 29 --interval month` |
64
+ | Add another price | `set_plan_price` | `bridge plan price set pro --amount 290 --interval year` |
65
+ | Add a limit | `set_plan_quota` | `bridge plan quota set pro --metric tickets --limit 100 --policy hard --kind gauge` |
66
+ | Turn off the forced plan choice | `update_app` | `bridge app update --payments-auto-redirect false` |
67
+ | Code for the framework | `get_integration_guide` (topic `billing`) | `bridge guide billing --framework svelte`, `bridge guide billing --framework nestjs` |
68
+
69
+ ## Prove it
70
+
71
+ 1. `list_plans` / `bridge plan list` matches the tables the developer confirmed; a plan with monthly and yearly pricing is one plan.
72
+ 2. `get_stripe_status` / `bridge stripe status`: connected, webhook working.
73
+ 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.
74
+ 4. A paid plan opens Stripe Checkout and returns to the subscription page with the new plan.
75
+
76
+ ## Where this connects
77
+
78
+ - **Feature control:** features that depend on the plan.
79
+ - **Teams:** seats, and the plan belonging to the workspace.
80
+ - **Roles:** who may change the plan.
81
+ - **Going live:** live Stripe keys.