@brandtg/flapjack 0.1.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 ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2025 Greg Brandt
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of Flapjack nor the names of its contributors may
15
+ be used to endorse or promote products derived from this software
16
+ without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,584 @@
1
+ # Flapjack
2
+
3
+ [![CI](https://github.com/brandtg/flapjack/actions/workflows/ci.yml/badge.svg)](https://github.com/brandtg/flapjack/actions/workflows/ci.yml)
4
+ [![npm version](https://badge.fury.io/js/@brandtg%2Fflapjack.svg)](https://www.npmjs.com/package/@brandtg/flapjack)
5
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
6
+
7
+ A simple feature flags library with PostgreSQL integration, inspired by [django-waffle](https://github.com/django-waffle/django-waffle).
8
+
9
+ ## Features
10
+
11
+ - **Multiple targeting strategies**: Enable features for specific users, roles, groups, or percentage rollouts
12
+ - **Consistent hashing**: Deterministic user bucketing for A/B testing and experimentation
13
+ - **PostgreSQL-backed**: Reliable, transactional flag storage with your existing database
14
+ - **CLI included**: Manage feature flags from the command line
15
+ - **TypeScript-first**: Full type safety with comprehensive TypeScript definitions
16
+ - **Battle-tested**: Comprehensive test suite with 31+ tests
17
+
18
+ ## Requirements
19
+
20
+ - Node.js >= 18.0.0
21
+ - PostgreSQL >= 10.0
22
+
23
+ ## Installation
24
+
25
+ Install the library
26
+
27
+ ```bash
28
+ npm install @brandtg/flapjack
29
+ ```
30
+
31
+ Apply the migrations
32
+
33
+ ```typescript
34
+ import { runMigrations } from "@brandtg/flapjack";
35
+
36
+ await runMigrations({
37
+ // Connect to your PostgreSQL database
38
+ databaseUrl: process.env.DATABASE_URL,
39
+ // Use the same migrations table as your application
40
+ migrationsTable: "pgmigrations",
41
+ });
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```typescript
47
+ import { Pool } from "pg";
48
+ import { FeatureFlagModel } from "@brandtg/flapjack";
49
+
50
+ // Connect to the database
51
+ const pool: Pool = getDatabasePool();
52
+ const featureFlags = new FeatureFlagModel(pool);
53
+
54
+ // Create a feature flag
55
+ // N.b. use a meaningful naming scheme like <feature>_<date>_<owner>
56
+ const flag = await featureFlags.create({
57
+ name: "enable_new_checkout_20250101_gbrandt",
58
+ });
59
+
60
+ // Check if a feature flag is active for a user
61
+ const active = await featureFlag.isActiveForUser({
62
+ name: "enable_new_checkout_20250101_gbrandt",
63
+ user: "1234",
64
+ });
65
+
66
+ // Enable the feature flag for certain roles
67
+ await featureFlags.update(flag.id, {
68
+ roles: ["admin", "staff"],
69
+ });
70
+
71
+ // Enable the feature flag for certain user groups
72
+ await featureFlags.update(flag.id, {
73
+ groups: ["early_adopters"],
74
+ });
75
+
76
+ // Launch the flag to a certain percentage of users
77
+ await featureFlags.update(flag.id, {
78
+ percent: 25,
79
+ });
80
+
81
+ // Launch the flag to everyone
82
+ await featureFlags.update(flag.id, {
83
+ everyone: true,
84
+ });
85
+
86
+ // Disable the flag for everyone
87
+ await featureFlags.update(flag.id, {
88
+ everyone: false,
89
+ });
90
+
91
+ // Move the flag back into normal state (other rules then apply)
92
+ await featureFlags.update(flag.id, {
93
+ everyone: null,
94
+ });
95
+ ```
96
+
97
+ ## How Feature Flag Evaluation Works
98
+
99
+ When checking if a feature flag is active for a user, Flapjack evaluates rules in the following order:
100
+
101
+ 1. **Everyone Override** (`everyone: true/false`): If set, immediately returns this value, ignoring all other rules
102
+ 2. **User List** (`users: [...]`): If the user ID is in this list, returns `true`
103
+ 3. **Group Membership** (`groups: [...]`): If the user belongs to any specified group, returns `true`
104
+ 4. **Role Membership** (`roles: [...]`): If the user has any specified role, returns `true`
105
+ 5. **Percentage Rollout** (`percent: 0-99.9`): Uses consistent hashing to deterministically bucket users
106
+ 6. **Default**: Returns `false` if no conditions are met
107
+
108
+ ### Example Evaluation
109
+
110
+ ```typescript
111
+ // Flag configured with multiple rules
112
+ await featureFlags.create({
113
+ name: "new_feature",
114
+ roles: ["admin"],
115
+ groups: ["beta_testers"],
116
+ percent: 25,
117
+ });
118
+
119
+ // Admin user: ✓ enabled (matches role)
120
+ await featureFlags.isActiveForUser({
121
+ name: "new_feature",
122
+ user: "user_123",
123
+ roles: ["admin"],
124
+ });
125
+
126
+ // Beta tester: ✓ enabled (matches group)
127
+ await featureFlags.isActiveForUser({
128
+ name: "new_feature",
129
+ user: "user_456",
130
+ groups: ["beta_testers"],
131
+ });
132
+
133
+ // Regular user: ? maybe (depends on hash bucket)
134
+ await featureFlags.isActiveForUser({
135
+ name: "new_feature",
136
+ user: "user_789",
137
+ roles: ["user"],
138
+ });
139
+ ```
140
+
141
+ ## Performance Considerations
142
+
143
+ ⚠️ **Important**: Flapjack queries the database on every `isActiveForUser()` call. For high-traffic applications, consider:
144
+
145
+ ### Recommended Caching Strategy
146
+
147
+ ```typescript
148
+ import { Pool } from "pg";
149
+ import { FeatureFlagModel } from "@brandtg/flapjack";
150
+
151
+ // Simple in-memory cache with TTL
152
+ class CachedFeatureFlags {
153
+ private model: FeatureFlagModel;
154
+ private cache = new Map<string, { flag: any; expires: number }>();
155
+ private ttlMs = 60000; // 1 minute
156
+
157
+ constructor(pool: Pool) {
158
+ this.model = new FeatureFlagModel(pool);
159
+ }
160
+
161
+ async isActiveForUser(params: {
162
+ name: string;
163
+ user?: string;
164
+ roles?: string[];
165
+ groups?: string[];
166
+ }): Promise<boolean> {
167
+ const now = Date.now();
168
+ const cached = this.cache.get(params.name);
169
+
170
+ // Use cached flag if still valid
171
+ if (cached && cached.expires > now) {
172
+ // Re-evaluate with cached flag data
173
+ return this.evaluateLocally(cached.flag, params);
174
+ }
175
+
176
+ // Cache miss or expired - fetch from DB
177
+ return await this.model.isActiveForUser(params);
178
+ }
179
+
180
+ private evaluateLocally(flag: any, params: any): boolean {
181
+ // Implement evaluation logic locally to avoid DB queries
182
+ // See model.ts isActiveForUser() for reference
183
+ // ...
184
+ }
185
+ }
186
+ ```
187
+
188
+ ### Database Connection Pooling
189
+
190
+ Always use connection pooling in production:
191
+
192
+ ```typescript
193
+ import { Pool } from "pg";
194
+
195
+ const pool = new Pool({
196
+ connectionString: process.env.DATABASE_URL,
197
+ max: 20, // Maximum pool size
198
+ idleTimeoutMillis: 30000,
199
+ connectionTimeoutMillis: 2000,
200
+ });
201
+
202
+ const featureFlags = new FeatureFlagModel(pool);
203
+ ```
204
+
205
+ ### Performance Tips
206
+
207
+ - **Cache flag configurations** at the application level with a reasonable TTL (30-60 seconds)
208
+ - **Batch flag checks** when possible to reduce round trips
209
+ - **Use database indexes**: The default migration includes an index on `name` field
210
+ - **Monitor query performance**: Feature flag checks should be <10ms in most cases
211
+ - **Consider read replicas** for extremely high-traffic scenarios
212
+
213
+ ## Experimentation and A/B Testing
214
+
215
+ Flapjack's percentage rollout feature enables experimentation and A/B testing:
216
+
217
+ ```typescript
218
+ // Create an experiment: 50% of users see the new feature
219
+ await featureFlags.create({
220
+ name: "checkout_redesign_experiment",
221
+ percent: 50,
222
+ note: "A/B test: new checkout flow vs. old",
223
+ });
224
+
225
+ // Users are consistently bucketed - same result every time
226
+ const isInExperiment = await featureFlags.isActiveForUser({
227
+ name: "checkout_redesign_experiment",
228
+ user: "user_123",
229
+ });
230
+
231
+ // Gradual rollout: Start at 5%, increase to 100% over time
232
+ await featureFlags.update(flagId, { percent: 5 });
233
+ // ... monitor metrics ...
234
+ await featureFlags.update(flagId, { percent: 25 });
235
+ // ... monitor metrics ...
236
+ await featureFlags.update(flagId, { percent: 100 });
237
+ ```
238
+
239
+ ### How User Bucketing Works
240
+
241
+ - Uses **MurmurHash3** for consistent, deterministic hashing
242
+ - Same user ID always maps to the same bucket (0-99)
243
+ - Changing the user ID will change bucket assignment
244
+ - Distribution is uniform across the user base
245
+
246
+ ## API Reference
247
+
248
+ ### FeatureFlagModel
249
+
250
+ #### `create(input: CreateInput): Promise<FeatureFlag>`
251
+
252
+ Creates a new feature flag.
253
+
254
+ #### `getById(id: number): Promise<FeatureFlag | null>`
255
+
256
+ Retrieves a feature flag by its ID.
257
+
258
+ #### `getByName(name: string): Promise<FeatureFlag | null>`
259
+
260
+ Retrieves a feature flag by its name.
261
+
262
+ #### `list(): Promise<FeatureFlag[]>`
263
+
264
+ Lists all feature flags, ordered by ID.
265
+
266
+ #### `update(id: number, changes: UpdateChanges): Promise<FeatureFlag | null>`
267
+
268
+ Updates a feature flag. Returns the updated flag or null if not found.
269
+
270
+ #### `delete(id: number): Promise<boolean>`
271
+
272
+ Deletes a feature flag. Returns true if deleted, false if not found.
273
+
274
+ #### `isActiveForUser(params): Promise<boolean>`
275
+
276
+ Checks if a feature flag is active for a user based on the evaluation rules.
277
+
278
+ #### `hashUserId(userId: string): Promise<number>`
279
+
280
+ Returns the hash value used for percentage bucketing. Useful for debugging rollout distributions.
281
+
282
+ ## CLI Usage
283
+
284
+ Flapjack includes a CLI for managing feature flags:
285
+
286
+ ```bash
287
+ # Set your database URL
288
+ export DATABASE_URL="postgresql://user:pass@localhost/dbname"
289
+
290
+ # Create a flag
291
+ flapjack create --name my_feature --roles admin --note "Admin-only feature"
292
+
293
+ # List all flags
294
+ flapjack list
295
+
296
+ # Get a specific flag
297
+ flapjack get-by-name my_feature
298
+
299
+ # Check if active for a user
300
+ flapjack is-active my_feature --user user123 --roles admin
301
+
302
+ # Update a flag
303
+ flapjack update 1 --percent 50 --everyone false
304
+
305
+ # Clear specific fields
306
+ flapjack update 1 --clear-roles --clear-percent
307
+
308
+ # Delete a flag
309
+ flapjack delete 1
310
+
311
+ # Debug user bucketing
312
+ flapjack hash-user user123
313
+ ```
314
+
315
+ ## Best Practices
316
+
317
+ ### Naming Conventions
318
+
319
+ Use descriptive names that include context:
320
+
321
+ ```typescript
322
+ // Good: Includes feature, date, and owner
323
+ "enable_new_checkout_20250101_gbrandt";
324
+ "experiment_ai_suggestions_20250115_team_growth";
325
+
326
+ // Avoid: Too generic
327
+ "new_feature";
328
+ "test_flag";
329
+ ```
330
+
331
+ ### Gradual Rollouts
332
+
333
+ Always roll out features gradually:
334
+
335
+ 1. Start with internal users/roles (e.g., `roles: ["admin", "staff"]`)
336
+ 2. Expand to beta testers (e.g., `groups: ["beta_testers"]`)
337
+ 3. Percentage rollout (5% → 25% → 50% → 100%)
338
+ 4. Enable for everyone (e.g., `everyone: true`)
339
+ 5. After stable, remove the flag from code and database
340
+
341
+ ### Flag Lifecycle Management
342
+
343
+ ```typescript
344
+ // 1. Development: Admin only
345
+ await featureFlags.create({
346
+ name: "new_feature_20250101",
347
+ roles: ["admin"],
348
+ note: "New feature in development",
349
+ });
350
+
351
+ // 2. Beta Testing
352
+ await featureFlags.update(flagId, {
353
+ groups: ["beta_testers"],
354
+ });
355
+
356
+ // 3. Gradual Rollout
357
+ await featureFlags.update(flagId, { percent: 10 });
358
+ // Monitor, then increase...
359
+
360
+ // 4. Full Launch
361
+ await featureFlags.update(flagId, { everyone: true });
362
+
363
+ // 5. Cleanup (after feature is stable)
364
+ // Remove feature flag checks from code
365
+ await featureFlags.delete(flagId);
366
+ ```
367
+
368
+ ### Error Handling
369
+
370
+ ```typescript
371
+ try {
372
+ const isActive = await featureFlags.isActiveForUser({
373
+ name: "my_feature",
374
+ user: "user_123",
375
+ });
376
+
377
+ if (isActive) {
378
+ // Show new feature
379
+ } else {
380
+ // Show old feature
381
+ }
382
+ } catch (error) {
383
+ // On error, fail closed (disable feature) or open (enable feature)
384
+ // depending on your risk tolerance
385
+ console.error("Feature flag check failed:", error);
386
+ const isActive = false; // Fail closed - safer default
387
+ }
388
+ ```
389
+
390
+ ## Troubleshooting
391
+
392
+ ### Database Connection Issues
393
+
394
+ ```typescript
395
+ // Verify connection before using feature flags
396
+ import { Pool } from "pg";
397
+
398
+ const pool = new Pool({
399
+ connectionString: process.env.DATABASE_URL,
400
+ });
401
+
402
+ try {
403
+ await pool.query("SELECT 1");
404
+ console.log("Database connection successful");
405
+ } catch (error) {
406
+ console.error("Database connection failed:", error);
407
+ }
408
+ ```
409
+
410
+ ### Flag Not Found
411
+
412
+ If `isActiveForUser()` returns false unexpectedly:
413
+
414
+ 1. Verify the flag exists: `flapjack get-by-name your_flag_name`
415
+ 2. Check the evaluation rules with `flapjack is-active`
416
+ 3. Verify user/role/group matching is case-sensitive
417
+
418
+ ### Percentage Rollout Not Working
419
+
420
+ Debug user bucketing:
421
+
422
+ ```bash
423
+ # Check which bucket a user falls into
424
+ flapjack hash-user user_123
425
+ # Output: { userId: "user_123", hash: 1234567, bucket: 67 }
426
+
427
+ # If bucket is 67 and percent is 50, user is NOT in rollout
428
+ # If bucket is 67 and percent is 75, user IS in rollout
429
+ ```
430
+
431
+ ## Development
432
+
433
+ ### Setup
434
+
435
+ Create an environment file:
436
+
437
+ ```bash
438
+ npm run dev:env
439
+ ```
440
+
441
+ Start the PostgreSQL database:
442
+
443
+ ```bash
444
+ npm run dev:docker:up
445
+ ```
446
+
447
+ Run database migrations:
448
+
449
+ ```bash
450
+ npm run dev:migrate
451
+ ```
452
+
453
+ ### Running Tests
454
+
455
+ ```bash
456
+ npm test
457
+ ```
458
+
459
+ ### Database Management
460
+
461
+ Create a new migration:
462
+
463
+ ```bash
464
+ npm run create-migration -- migration_name
465
+ ```
466
+
467
+ Reset the database:
468
+
469
+ ```bash
470
+ npm run dev:docker:down
471
+ ```
472
+
473
+ ## Building and Publishing
474
+
475
+ ### Development Build
476
+
477
+ To build the project for development and testing:
478
+
479
+ ```bash
480
+ npm run build
481
+ ```
482
+
483
+ This compiles TypeScript to JavaScript and generates type declaration files in the `dist/` directory.
484
+
485
+ ### Creating a Development Package
486
+
487
+ To create a tarball package that can be installed manually in other projects:
488
+
489
+ ```bash
490
+ npm run pack:dev
491
+ ```
492
+
493
+ This will create a `flapjack-0.1.0.tgz` file that you can install in another project using:
494
+
495
+ ```bash
496
+ npm install /path/to/flapjack-0.1.0.tgz
497
+ ```
498
+
499
+ ### Publishing to npm
500
+
501
+ Before publishing to npm, make sure your package is ready:
502
+
503
+ 1. **Login to npm** (one-time setup):
504
+
505
+ ```bash
506
+ npm login
507
+ ```
508
+
509
+ This will prompt for your username, password, email, and 2FA code.
510
+
511
+ 2. **Verify your login**:
512
+
513
+ ```bash
514
+ npm whoami
515
+ ```
516
+
517
+ 3. **Update the version** in `package.json`:
518
+
519
+ ```bash
520
+ # For a patch release (0.1.0 -> 0.1.1)
521
+ npm version patch
522
+
523
+ # For a minor release (0.1.0 -> 0.2.0)
524
+ npm version minor
525
+
526
+ # For a major release (0.1.0 -> 1.0.0)
527
+ npm version major
528
+ ```
529
+
530
+ This automatically creates a git commit and tag.
531
+
532
+ 4. **Run pre-publish checks** (linting, tests, and build):
533
+
534
+ ```bash
535
+ npm run prepublishOnly
536
+ ```
537
+
538
+ 5. **Publish to npm**:
539
+
540
+ ```bash
541
+ npm publish --access public
542
+ ```
543
+
544
+ You'll be prompted for your 2FA code. For a dry run to see what would be published:
545
+
546
+ ```bash
547
+ npm publish --access public --dry-run
548
+ ```
549
+
550
+ 6. **Push the version tag to GitHub**:
551
+
552
+ ```bash
553
+ git push && git push --tags
554
+ ```
555
+
556
+ 7. **Optional: Create a GitHub release** for the new version at https://github.com/brandtg/flapjack/releases/new
557
+
558
+ #### Publishing a Beta Version
559
+
560
+ For pre-release versions:
561
+
562
+ ```bash
563
+ # Update to a pre-release version
564
+ npm version prerelease --preid=beta
565
+
566
+ # Publish with beta tag
567
+ npm publish --access public --tag beta
568
+ ```
569
+
570
+ Users can install beta versions with:
571
+
572
+ ```bash
573
+ npm install @brandtg/flapjack@beta
574
+ ```
575
+
576
+ ## Contributing
577
+
578
+ Contributions are welcome! Please feel free to submit a Pull Request.
579
+
580
+ ## License
581
+
582
+ This project is licensed under the BSD 3-Clause License - see the [LICENSE](LICENSE) file for details.
583
+
584
+ This project is inspired by [django-waffle](https://github.com/django-waffle/django-waffle).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}