@rune-kit/rune 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +8 -6
- package/commands/rune.md +168 -168
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -71
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/brainstorm/SKILL.md +342 -342
- package/skills/browser-pilot/SKILL.md +168 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +404 -404
- package/skills/cook/SKILL.md +917 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +589 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +177 -177
- package/skills/fix/SKILL.md +330 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +219 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +240 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +251 -251
- package/skills/marketing/SKILL.md +290 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- package/skills/neural-memory/SKILL.md +362 -362
- package/skills/onboard/SKILL.md +404 -403
- package/skills/perf/SKILL.md +346 -346
- package/skills/plan/SKILL.md +433 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +612 -588
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -287
- package/skills/scope-guard/SKILL.md +180 -180
- package/skills/scout/SKILL.md +263 -263
- package/skills/sentinel/SKILL.md +382 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +543 -543
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +556 -537
- package/skills/test/SKILL.md +614 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +326 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "feature-flags"
|
|
3
|
-
pack: "@rune/saas"
|
|
4
|
-
description: "Feature flag management — gradual rollouts, kill switches, A/B testing, user-segment targeting, and stale flag cleanup. Supports self-hosted (Unleash, custom Redis) and managed (LaunchDarkly, Statsig, Flagsmith)."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# feature-flags
|
|
10
|
-
|
|
11
|
-
Feature flag management — gradual rollouts, kill switches, A/B testing, user-segment targeting, and stale flag cleanup. Supports self-hosted (Unleash, custom Redis) and managed (LaunchDarkly, Statsig, Flagsmith).
|
|
12
|
-
|
|
13
|
-
#### Flag Types
|
|
14
|
-
|
|
15
|
-
| Type | Use Case | Example |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| Boolean | Simple on/off for a feature | `new_dashboard_ui` |
|
|
18
|
-
| Percentage rollout | Gradual release 1% → 100% | `redesigned_editor: 25%` |
|
|
19
|
-
| User segment | Specific users/orgs first | `beta_users`, `enterprise_plan` |
|
|
20
|
-
| A/B test | Compare variants with metrics | `checkout_flow: variant_a / variant_b` |
|
|
21
|
-
| Kill switch | Instant disable on failure | `payment_processor_v2` |
|
|
22
|
-
| Environment | Dev/staging/prod separation | Auto by `NODE_ENV` |
|
|
23
|
-
|
|
24
|
-
#### Rollout Pattern: Canary → Gradual → GA
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
1% (internal + beta users) → 10% → 25% → 50% → 100% → cleanup flag after 30 days at 100%
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
#### Workflow
|
|
31
|
-
|
|
32
|
-
**Step 1 — Identify feature boundary**
|
|
33
|
-
Before writing code, define the flag: name (kebab-case, descriptive), default value (false = safe default), targeting rules (who sees it first), and planned cleanup date. Document in your flag provider dashboard.
|
|
34
|
-
|
|
35
|
-
**Step 2 — Create flag with targeting rules**
|
|
36
|
-
In Unleash/LaunchDarkly/Flagsmith: create flag with gradual rollout strategy. Start at 0%. Add a "beta users" segment for internal testing before any percentage rollout. Set environment-specific defaults: always-on in dev, gradual in staging, starts at 0% in prod.
|
|
37
|
-
|
|
38
|
-
**Step 3 — Implement client/server evaluation**
|
|
39
|
-
Client: evaluate flag in a React hook, never inline. Server: evaluate in middleware or at request start, attach result to request context. Never evaluate flags inside hot loops — cache the result for the request lifetime.
|
|
40
|
-
|
|
41
|
-
**Step 4 — Add analytics event tracking**
|
|
42
|
-
Every flag evaluation on a user-facing feature should fire an analytics event: `feature_flag_evaluated` with `{ flag, variant, userId, tenantId }`. This enables funnel analysis by variant and measures the rollout's impact on key metrics.
|
|
43
|
-
|
|
44
|
-
**Step 5 — Schedule flag cleanup**
|
|
45
|
-
Flags that have been at 100% for >30 days are stale. Run a weekly lint job: grep all flag keys used in code, compare against provider's flag list, flag mismatches (code uses a flag that was deleted → runtime error, or flag exists but never referenced → cleanup candidate). Remove stale flags from both code and provider in the same PR.
|
|
46
|
-
|
|
47
|
-
#### Example
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
// Custom Redis-based flag evaluation (self-hosted, zero SaaS dependency)
|
|
51
|
-
import { Redis } from 'ioredis';
|
|
52
|
-
const redis = new Redis(process.env.REDIS_URL!);
|
|
53
|
-
|
|
54
|
-
interface FlagConfig {
|
|
55
|
-
enabled: boolean;
|
|
56
|
-
percentage?: number; // 0-100 for gradual rollout
|
|
57
|
-
allowedUsers?: string[]; // canary user IDs
|
|
58
|
-
allowedPlans?: string[]; // plan-based targeting
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const evaluateFlag = async (
|
|
62
|
-
flagKey: string,
|
|
63
|
-
ctx: { userId: string; tenantId: string; plan: string }
|
|
64
|
-
): Promise<boolean> => {
|
|
65
|
-
const raw = await redis.get(`flag:${flagKey}`);
|
|
66
|
-
if (!raw) return false; // default off = safe
|
|
67
|
-
const config: FlagConfig = JSON.parse(raw);
|
|
68
|
-
if (!config.enabled) return false;
|
|
69
|
-
if (config.allowedUsers?.includes(ctx.userId)) return true;
|
|
70
|
-
if (config.allowedPlans?.includes(ctx.plan)) return true;
|
|
71
|
-
if (config.percentage !== undefined) {
|
|
72
|
-
// Deterministic: same user always gets same bucket
|
|
73
|
-
const hash = parseInt(ctx.userId.slice(-8), 16) % 100;
|
|
74
|
-
return hash < config.percentage;
|
|
75
|
-
}
|
|
76
|
-
return config.enabled;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// React hook — evaluate once per render cycle, never in loops
|
|
80
|
-
function useFlag(flagKey: string): boolean {
|
|
81
|
-
const { user } = useAuth();
|
|
82
|
-
const { data: enabled = false } = useQuery({
|
|
83
|
-
queryKey: ['flag', flagKey, user?.id],
|
|
84
|
-
queryFn: () => fetchFlag(flagKey),
|
|
85
|
-
staleTime: 30_000, // cache 30s — flags don't change every millisecond
|
|
86
|
-
});
|
|
87
|
-
return enabled;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Server middleware — evaluate at request boundary, attach to context
|
|
91
|
-
const flagMiddleware = (flagKey: string) => async (req: Request, res: Response, next: NextFunction) => {
|
|
92
|
-
req.flags = req.flags ?? {};
|
|
93
|
-
req.flags[flagKey] = await evaluateFlag(flagKey, {
|
|
94
|
-
userId: req.user!.id,
|
|
95
|
-
tenantId: req.tenantId!,
|
|
96
|
-
plan: req.user!.plan,
|
|
97
|
-
});
|
|
98
|
-
next();
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// Usage in route — flag already evaluated, no async needed
|
|
102
|
-
app.get('/api/checkout', flagMiddleware('new_checkout_v2'), (req, res) => {
|
|
103
|
-
if (req.flags['new_checkout_v2']) {
|
|
104
|
-
return checkoutV2Handler(req, res);
|
|
105
|
-
}
|
|
106
|
-
return checkoutV1Handler(req, res);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Stale flag detection — run weekly in CI
|
|
110
|
-
import { execSync } from 'child_process';
|
|
111
|
-
|
|
112
|
-
const findStaleFlags = async () => {
|
|
113
|
-
const flagsInCode = execSync('grep -r "useFlag\\|evaluateFlag" src/ --include="*.ts" -h')
|
|
114
|
-
.toString()
|
|
115
|
-
.match(/(?:useFlag|evaluateFlag)\(['"]([^'"]+)['"]/g)
|
|
116
|
-
?.map(m => m.match(/['"]([^'"]+)['"]/)?.[1])
|
|
117
|
-
.filter(Boolean) ?? [];
|
|
118
|
-
|
|
119
|
-
const flagsInProvider = await redis.keys('flag:*').then(keys => keys.map(k => k.replace('flag:', '')));
|
|
120
|
-
const stale = flagsInProvider.filter(f => !flagsInCode.includes(f));
|
|
121
|
-
const missing = flagsInCode.filter(f => !flagsInProvider.includes(f));
|
|
122
|
-
return { stale, missing };
|
|
123
|
-
};
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
**Sharp edges for flags:**
|
|
127
|
-
- Never evaluate flags on hot paths (e.g., inside `Array.map` over 1000 items) — cache the flag state at the top of the function.
|
|
128
|
-
- In tests: mock flag evaluation at the provider level, not by conditionally skipping flag checks. Every code path should be testable with flags on and off.
|
|
129
|
-
- Flag dependency chains (flag A enables flag B) — avoid. If you need compound logic, evaluate both flags independently and combine in application code. Provider-level dependencies are invisible in code review.
|
|
130
|
-
- Percentage rollout is not the same as A/B test — percentage rollout has no control group. For A/B tests, always keep a 50/50 split or a defined control group.
|
|
1
|
+
---
|
|
2
|
+
name: "feature-flags"
|
|
3
|
+
pack: "@rune/saas"
|
|
4
|
+
description: "Feature flag management — gradual rollouts, kill switches, A/B testing, user-segment targeting, and stale flag cleanup. Supports self-hosted (Unleash, custom Redis) and managed (LaunchDarkly, Statsig, Flagsmith)."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# feature-flags
|
|
10
|
+
|
|
11
|
+
Feature flag management — gradual rollouts, kill switches, A/B testing, user-segment targeting, and stale flag cleanup. Supports self-hosted (Unleash, custom Redis) and managed (LaunchDarkly, Statsig, Flagsmith).
|
|
12
|
+
|
|
13
|
+
#### Flag Types
|
|
14
|
+
|
|
15
|
+
| Type | Use Case | Example |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Boolean | Simple on/off for a feature | `new_dashboard_ui` |
|
|
18
|
+
| Percentage rollout | Gradual release 1% → 100% | `redesigned_editor: 25%` |
|
|
19
|
+
| User segment | Specific users/orgs first | `beta_users`, `enterprise_plan` |
|
|
20
|
+
| A/B test | Compare variants with metrics | `checkout_flow: variant_a / variant_b` |
|
|
21
|
+
| Kill switch | Instant disable on failure | `payment_processor_v2` |
|
|
22
|
+
| Environment | Dev/staging/prod separation | Auto by `NODE_ENV` |
|
|
23
|
+
|
|
24
|
+
#### Rollout Pattern: Canary → Gradual → GA
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
1% (internal + beta users) → 10% → 25% → 50% → 100% → cleanup flag after 30 days at 100%
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
#### Workflow
|
|
31
|
+
|
|
32
|
+
**Step 1 — Identify feature boundary**
|
|
33
|
+
Before writing code, define the flag: name (kebab-case, descriptive), default value (false = safe default), targeting rules (who sees it first), and planned cleanup date. Document in your flag provider dashboard.
|
|
34
|
+
|
|
35
|
+
**Step 2 — Create flag with targeting rules**
|
|
36
|
+
In Unleash/LaunchDarkly/Flagsmith: create flag with gradual rollout strategy. Start at 0%. Add a "beta users" segment for internal testing before any percentage rollout. Set environment-specific defaults: always-on in dev, gradual in staging, starts at 0% in prod.
|
|
37
|
+
|
|
38
|
+
**Step 3 — Implement client/server evaluation**
|
|
39
|
+
Client: evaluate flag in a React hook, never inline. Server: evaluate in middleware or at request start, attach result to request context. Never evaluate flags inside hot loops — cache the result for the request lifetime.
|
|
40
|
+
|
|
41
|
+
**Step 4 — Add analytics event tracking**
|
|
42
|
+
Every flag evaluation on a user-facing feature should fire an analytics event: `feature_flag_evaluated` with `{ flag, variant, userId, tenantId }`. This enables funnel analysis by variant and measures the rollout's impact on key metrics.
|
|
43
|
+
|
|
44
|
+
**Step 5 — Schedule flag cleanup**
|
|
45
|
+
Flags that have been at 100% for >30 days are stale. Run a weekly lint job: grep all flag keys used in code, compare against provider's flag list, flag mismatches (code uses a flag that was deleted → runtime error, or flag exists but never referenced → cleanup candidate). Remove stale flags from both code and provider in the same PR.
|
|
46
|
+
|
|
47
|
+
#### Example
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// Custom Redis-based flag evaluation (self-hosted, zero SaaS dependency)
|
|
51
|
+
import { Redis } from 'ioredis';
|
|
52
|
+
const redis = new Redis(process.env.REDIS_URL!);
|
|
53
|
+
|
|
54
|
+
interface FlagConfig {
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
percentage?: number; // 0-100 for gradual rollout
|
|
57
|
+
allowedUsers?: string[]; // canary user IDs
|
|
58
|
+
allowedPlans?: string[]; // plan-based targeting
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const evaluateFlag = async (
|
|
62
|
+
flagKey: string,
|
|
63
|
+
ctx: { userId: string; tenantId: string; plan: string }
|
|
64
|
+
): Promise<boolean> => {
|
|
65
|
+
const raw = await redis.get(`flag:${flagKey}`);
|
|
66
|
+
if (!raw) return false; // default off = safe
|
|
67
|
+
const config: FlagConfig = JSON.parse(raw);
|
|
68
|
+
if (!config.enabled) return false;
|
|
69
|
+
if (config.allowedUsers?.includes(ctx.userId)) return true;
|
|
70
|
+
if (config.allowedPlans?.includes(ctx.plan)) return true;
|
|
71
|
+
if (config.percentage !== undefined) {
|
|
72
|
+
// Deterministic: same user always gets same bucket
|
|
73
|
+
const hash = parseInt(ctx.userId.slice(-8), 16) % 100;
|
|
74
|
+
return hash < config.percentage;
|
|
75
|
+
}
|
|
76
|
+
return config.enabled;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// React hook — evaluate once per render cycle, never in loops
|
|
80
|
+
function useFlag(flagKey: string): boolean {
|
|
81
|
+
const { user } = useAuth();
|
|
82
|
+
const { data: enabled = false } = useQuery({
|
|
83
|
+
queryKey: ['flag', flagKey, user?.id],
|
|
84
|
+
queryFn: () => fetchFlag(flagKey),
|
|
85
|
+
staleTime: 30_000, // cache 30s — flags don't change every millisecond
|
|
86
|
+
});
|
|
87
|
+
return enabled;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Server middleware — evaluate at request boundary, attach to context
|
|
91
|
+
const flagMiddleware = (flagKey: string) => async (req: Request, res: Response, next: NextFunction) => {
|
|
92
|
+
req.flags = req.flags ?? {};
|
|
93
|
+
req.flags[flagKey] = await evaluateFlag(flagKey, {
|
|
94
|
+
userId: req.user!.id,
|
|
95
|
+
tenantId: req.tenantId!,
|
|
96
|
+
plan: req.user!.plan,
|
|
97
|
+
});
|
|
98
|
+
next();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Usage in route — flag already evaluated, no async needed
|
|
102
|
+
app.get('/api/checkout', flagMiddleware('new_checkout_v2'), (req, res) => {
|
|
103
|
+
if (req.flags['new_checkout_v2']) {
|
|
104
|
+
return checkoutV2Handler(req, res);
|
|
105
|
+
}
|
|
106
|
+
return checkoutV1Handler(req, res);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Stale flag detection — run weekly in CI
|
|
110
|
+
import { execSync } from 'child_process';
|
|
111
|
+
|
|
112
|
+
const findStaleFlags = async () => {
|
|
113
|
+
const flagsInCode = execSync('grep -r "useFlag\\|evaluateFlag" src/ --include="*.ts" -h')
|
|
114
|
+
.toString()
|
|
115
|
+
.match(/(?:useFlag|evaluateFlag)\(['"]([^'"]+)['"]/g)
|
|
116
|
+
?.map(m => m.match(/['"]([^'"]+)['"]/)?.[1])
|
|
117
|
+
.filter(Boolean) ?? [];
|
|
118
|
+
|
|
119
|
+
const flagsInProvider = await redis.keys('flag:*').then(keys => keys.map(k => k.replace('flag:', '')));
|
|
120
|
+
const stale = flagsInProvider.filter(f => !flagsInCode.includes(f));
|
|
121
|
+
const missing = flagsInCode.filter(f => !flagsInProvider.includes(f));
|
|
122
|
+
return { stale, missing };
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Sharp edges for flags:**
|
|
127
|
+
- Never evaluate flags on hot paths (e.g., inside `Array.map` over 1000 items) — cache the flag state at the top of the function.
|
|
128
|
+
- In tests: mock flag evaluation at the provider level, not by conditionally skipping flag checks. Every code path should be testable with flags on and off.
|
|
129
|
+
- Flag dependency chains (flag A enables flag B) — avoid. If you need compound logic, evaluate both flags independently and combine in application code. Provider-level dependencies are invisible in code review.
|
|
130
|
+
- Percentage rollout is not the same as A/B test — percentage rollout has no control group. For A/B tests, always keep a 50/50 split or a defined control group.
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "multi-tenant"
|
|
3
|
-
pack: "@rune/saas"
|
|
4
|
-
description: "Multi-tenancy patterns — database isolation strategies, tenant context middleware, data partitioning, cross-tenant query prevention, tenant-aware background jobs, and GDPR data export."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# multi-tenant
|
|
10
|
-
|
|
11
|
-
Multi-tenancy patterns — database isolation strategies, tenant context middleware, data partitioning, cross-tenant query prevention, tenant-aware background jobs, and GDPR data export.
|
|
12
|
-
|
|
13
|
-
#### Isolation Strategy Comparison
|
|
14
|
-
|
|
15
|
-
| Strategy | Cost | Isolation | Migration Difficulty | When to Use |
|
|
16
|
-
|---|---|---|---|---|
|
|
17
|
-
| Shared DB, tenant column | Low | Weak (app-enforced) | Easy | Early-stage, <1000 tenants |
|
|
18
|
-
| Shared DB + PostgreSQL RLS | Low | Strong (DB-enforced) | Easy | Best default for most SaaS |
|
|
19
|
-
| Schema-per-tenant | Medium | Strong | Medium | When tenants need schema customization |
|
|
20
|
-
| DB-per-tenant | High | Perfect | Hard | Enterprise, compliance (HIPAA, SOC2) |
|
|
21
|
-
|
|
22
|
-
#### Workflow
|
|
23
|
-
|
|
24
|
-
**Step 1 — Detect current isolation strategy**
|
|
25
|
-
Use Grep to find tenant-related code: `tenantId`, `organizationId`, `workspaceId`, `x-tenant-id` header, RLS policies, schema-per-tenant patterns, database switching logic. Read the database schema and middleware to classify the isolation strategy in use.
|
|
26
|
-
|
|
27
|
-
**Step 2 — Audit isolation boundaries**
|
|
28
|
-
Check for: queries without tenant filter (data leak risk), missing tenant context in middleware, no RLS policies on shared tables, admin endpoints that bypass tenant isolation, background jobs processing cross-tenant data without scoping. Flag each with severity.
|
|
29
|
-
|
|
30
|
-
**Step 3 — Emit tenant-safe patterns**
|
|
31
|
-
Based on detected strategy, emit: tenant middleware (extract from JWT/header, set on request context), RLS policies for shared-schema approach, scoped repository pattern that injects tenant filter on every query, and tenant-aware test fixtures.
|
|
32
|
-
|
|
33
|
-
**Step 4 — Tenant-aware background jobs**
|
|
34
|
-
Every background job MUST carry `tenantId`. Use BullMQ job data to pass tenant context, then initialize a scoped repository inside the job processor. Never process tenant data in a job without an explicit `tenantId` guard.
|
|
35
|
-
|
|
36
|
-
**Step 5 — Tenant data export (GDPR portability)**
|
|
37
|
-
Implement `/api/tenants/:id/export` that collects all data rows belonging to a tenant across all tables, serializes to JSON or CSV, and streams the result as a download. Log the export event in the audit trail with timestamp and requesting user.
|
|
38
|
-
|
|
39
|
-
#### Example
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
// Tenant middleware — extract from JWT, inject into request context
|
|
43
|
-
const tenantMiddleware = async (req: Request, res: Response, next: NextFunction) => {
|
|
44
|
-
const tenantId = req.user?.tenantId ?? req.headers['x-tenant-id'] as string;
|
|
45
|
-
if (!tenantId) return res.status(403).json({ error: { code: 'TENANT_REQUIRED', message: 'Tenant context missing' } });
|
|
46
|
-
req.tenantId = tenantId;
|
|
47
|
-
next();
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Scoped repository — every query automatically filtered by tenant
|
|
51
|
-
class ScopedRepository<T extends { tenantId: string }> {
|
|
52
|
-
constructor(private model: PrismaModel<T>, private tenantId: string) {}
|
|
53
|
-
|
|
54
|
-
async findMany(where: Partial<Omit<T, 'tenantId'>> = {}) {
|
|
55
|
-
return this.model.findMany({ where: { ...where, tenantId: this.tenantId } });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async create(data: Omit<T, 'tenantId' | 'id' | 'createdAt' | 'updatedAt'>) {
|
|
59
|
-
return this.model.create({ data: { ...data, tenantId: this.tenantId } as any });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// PostgreSQL RLS — DB-enforced isolation, safest approach
|
|
64
|
-
-- Enable RLS on every shared table
|
|
65
|
-
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
|
66
|
-
|
|
67
|
-
-- Set tenant context before query (from app middleware)
|
|
68
|
-
SET LOCAL app.tenant_id = '550e8400-e29b-41d4-a716-446655440000';
|
|
69
|
-
|
|
70
|
-
-- Policy reads from session variable — automatic for all queries
|
|
71
|
-
CREATE POLICY tenant_isolation ON projects
|
|
72
|
-
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
|
73
|
-
|
|
74
|
-
-- Set in Prisma $executeRaw before each query block:
|
|
75
|
-
-- await prisma.$executeRaw`SELECT set_config('app.tenant_id', ${tenantId}, true)`;
|
|
76
|
-
|
|
77
|
-
// BullMQ — tenant-aware background job
|
|
78
|
-
const emailQueue = new Queue('emails');
|
|
79
|
-
|
|
80
|
-
// Producer: always pass tenantId in job data
|
|
81
|
-
await emailQueue.add('send-invoice', { tenantId, invoiceId, recipientEmail });
|
|
82
|
-
|
|
83
|
-
// Consumer: initialize scoped context from job data
|
|
84
|
-
const worker = new Worker('emails', async (job) => {
|
|
85
|
-
const { tenantId, invoiceId } = job.data;
|
|
86
|
-
const invoices = new ScopedRepository(prisma.invoice, tenantId);
|
|
87
|
-
const invoice = await invoices.findMany({ id: invoiceId });
|
|
88
|
-
// process...
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// GDPR export — stream all tenant data
|
|
92
|
-
app.get('/api/tenants/:id/export', requireOwner, async (req, res) => {
|
|
93
|
-
const { id: tenantId } = req.params;
|
|
94
|
-
const [projects, members, invoices] = await Promise.all([
|
|
95
|
-
prisma.project.findMany({ where: { tenantId } }),
|
|
96
|
-
prisma.member.findMany({ where: { tenantId } }),
|
|
97
|
-
prisma.invoice.findMany({ where: { tenantId } }),
|
|
98
|
-
]);
|
|
99
|
-
await prisma.auditLog.create({ data: { tenantId, action: 'DATA_EXPORT', actorId: req.user.id } });
|
|
100
|
-
res.setHeader('Content-Disposition', `attachment; filename="export-${tenantId}.json"`);
|
|
101
|
-
res.json({ exportedAt: new Date(), projects, members, invoices });
|
|
102
|
-
});
|
|
103
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: "multi-tenant"
|
|
3
|
+
pack: "@rune/saas"
|
|
4
|
+
description: "Multi-tenancy patterns — database isolation strategies, tenant context middleware, data partitioning, cross-tenant query prevention, tenant-aware background jobs, and GDPR data export."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# multi-tenant
|
|
10
|
+
|
|
11
|
+
Multi-tenancy patterns — database isolation strategies, tenant context middleware, data partitioning, cross-tenant query prevention, tenant-aware background jobs, and GDPR data export.
|
|
12
|
+
|
|
13
|
+
#### Isolation Strategy Comparison
|
|
14
|
+
|
|
15
|
+
| Strategy | Cost | Isolation | Migration Difficulty | When to Use |
|
|
16
|
+
|---|---|---|---|---|
|
|
17
|
+
| Shared DB, tenant column | Low | Weak (app-enforced) | Easy | Early-stage, <1000 tenants |
|
|
18
|
+
| Shared DB + PostgreSQL RLS | Low | Strong (DB-enforced) | Easy | Best default for most SaaS |
|
|
19
|
+
| Schema-per-tenant | Medium | Strong | Medium | When tenants need schema customization |
|
|
20
|
+
| DB-per-tenant | High | Perfect | Hard | Enterprise, compliance (HIPAA, SOC2) |
|
|
21
|
+
|
|
22
|
+
#### Workflow
|
|
23
|
+
|
|
24
|
+
**Step 1 — Detect current isolation strategy**
|
|
25
|
+
Use Grep to find tenant-related code: `tenantId`, `organizationId`, `workspaceId`, `x-tenant-id` header, RLS policies, schema-per-tenant patterns, database switching logic. Read the database schema and middleware to classify the isolation strategy in use.
|
|
26
|
+
|
|
27
|
+
**Step 2 — Audit isolation boundaries**
|
|
28
|
+
Check for: queries without tenant filter (data leak risk), missing tenant context in middleware, no RLS policies on shared tables, admin endpoints that bypass tenant isolation, background jobs processing cross-tenant data without scoping. Flag each with severity.
|
|
29
|
+
|
|
30
|
+
**Step 3 — Emit tenant-safe patterns**
|
|
31
|
+
Based on detected strategy, emit: tenant middleware (extract from JWT/header, set on request context), RLS policies for shared-schema approach, scoped repository pattern that injects tenant filter on every query, and tenant-aware test fixtures.
|
|
32
|
+
|
|
33
|
+
**Step 4 — Tenant-aware background jobs**
|
|
34
|
+
Every background job MUST carry `tenantId`. Use BullMQ job data to pass tenant context, then initialize a scoped repository inside the job processor. Never process tenant data in a job without an explicit `tenantId` guard.
|
|
35
|
+
|
|
36
|
+
**Step 5 — Tenant data export (GDPR portability)**
|
|
37
|
+
Implement `/api/tenants/:id/export` that collects all data rows belonging to a tenant across all tables, serializes to JSON or CSV, and streams the result as a download. Log the export event in the audit trail with timestamp and requesting user.
|
|
38
|
+
|
|
39
|
+
#### Example
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// Tenant middleware — extract from JWT, inject into request context
|
|
43
|
+
const tenantMiddleware = async (req: Request, res: Response, next: NextFunction) => {
|
|
44
|
+
const tenantId = req.user?.tenantId ?? req.headers['x-tenant-id'] as string;
|
|
45
|
+
if (!tenantId) return res.status(403).json({ error: { code: 'TENANT_REQUIRED', message: 'Tenant context missing' } });
|
|
46
|
+
req.tenantId = tenantId;
|
|
47
|
+
next();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Scoped repository — every query automatically filtered by tenant
|
|
51
|
+
class ScopedRepository<T extends { tenantId: string }> {
|
|
52
|
+
constructor(private model: PrismaModel<T>, private tenantId: string) {}
|
|
53
|
+
|
|
54
|
+
async findMany(where: Partial<Omit<T, 'tenantId'>> = {}) {
|
|
55
|
+
return this.model.findMany({ where: { ...where, tenantId: this.tenantId } });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async create(data: Omit<T, 'tenantId' | 'id' | 'createdAt' | 'updatedAt'>) {
|
|
59
|
+
return this.model.create({ data: { ...data, tenantId: this.tenantId } as any });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// PostgreSQL RLS — DB-enforced isolation, safest approach
|
|
64
|
+
-- Enable RLS on every shared table
|
|
65
|
+
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
|
66
|
+
|
|
67
|
+
-- Set tenant context before query (from app middleware)
|
|
68
|
+
SET LOCAL app.tenant_id = '550e8400-e29b-41d4-a716-446655440000';
|
|
69
|
+
|
|
70
|
+
-- Policy reads from session variable — automatic for all queries
|
|
71
|
+
CREATE POLICY tenant_isolation ON projects
|
|
72
|
+
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
|
73
|
+
|
|
74
|
+
-- Set in Prisma $executeRaw before each query block:
|
|
75
|
+
-- await prisma.$executeRaw`SELECT set_config('app.tenant_id', ${tenantId}, true)`;
|
|
76
|
+
|
|
77
|
+
// BullMQ — tenant-aware background job
|
|
78
|
+
const emailQueue = new Queue('emails');
|
|
79
|
+
|
|
80
|
+
// Producer: always pass tenantId in job data
|
|
81
|
+
await emailQueue.add('send-invoice', { tenantId, invoiceId, recipientEmail });
|
|
82
|
+
|
|
83
|
+
// Consumer: initialize scoped context from job data
|
|
84
|
+
const worker = new Worker('emails', async (job) => {
|
|
85
|
+
const { tenantId, invoiceId } = job.data;
|
|
86
|
+
const invoices = new ScopedRepository(prisma.invoice, tenantId);
|
|
87
|
+
const invoice = await invoices.findMany({ id: invoiceId });
|
|
88
|
+
// process...
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// GDPR export — stream all tenant data
|
|
92
|
+
app.get('/api/tenants/:id/export', requireOwner, async (req, res) => {
|
|
93
|
+
const { id: tenantId } = req.params;
|
|
94
|
+
const [projects, members, invoices] = await Promise.all([
|
|
95
|
+
prisma.project.findMany({ where: { tenantId } }),
|
|
96
|
+
prisma.member.findMany({ where: { tenantId } }),
|
|
97
|
+
prisma.invoice.findMany({ where: { tenantId } }),
|
|
98
|
+
]);
|
|
99
|
+
await prisma.auditLog.create({ data: { tenantId, action: 'DATA_EXPORT', actorId: req.user.id } });
|
|
100
|
+
res.setHeader('Content-Disposition', `attachment; filename="export-${tenantId}.json"`);
|
|
101
|
+
res.json({ exportedAt: new Date(), projects, members, invoices });
|
|
102
|
+
});
|
|
103
|
+
```
|