Targeting Rules
Targeting rules let you enable flags for specific users based on attributes like email, userId, country, or custom properties. This is more powerful than percentage rollouts because you can precisely control who sees a feature.
When to Use Targeting
Use targeting rules when you want to:
- Release features to specific users (beta testers, employees)
- Enable features based on user attributes (enterprise plans, specific regions)
- Gradually roll out to specific groups before general availability
- A/B test with specific user segments
Basic Usage
The evaluate() method evaluates targeting rules locally using cached flag data:
import { Featsync } from 'featsync';
const featsync = new Featsync({ apiKey: 'fs_your_api_key' });
// Evaluate with user contextconst enabled = await featsync.evaluate('new-checkout', { userId: 'user_123', email: 'john@acme.com', country: 'US',});
if (enabled) { showNewCheckout();}User Context
The evaluation context contains user attributes that are compared against your targeting rules:
interface EvaluationContext { // Standard attributes userId?: string; // User identifier email?: string; // User's email country?: string; // ISO country code (e.g., "US", "GB")
// Custom attributes custom?: Record<string, string | number | boolean>;}Standard Attributes
| Attribute | Type | Description | Example |
|---|---|---|---|
userId | string | Unique user identifier | "user_123" |
email | string | User’s email address | "john@acme.com" |
country | string | ISO country code | "US", "GB", "DE" |
Custom Attributes
Pass any custom attributes in the custom object:
const enabled = await featsync.evaluate('enterprise-feature', { userId: currentUser.id, email: currentUser.email, custom: { plan: 'enterprise', // String company_size: 500, // Number beta_tester: true, // Boolean app_version: '2.1.0', // Semver roles: 'admin,editor', // Comma-separated list },});In your targeting rules, access custom attributes with the custom. prefix:
custom.planequals"enterprise"custom.company_sizegreater than100custom.beta_testerequalstrue
Detailed Results
Use evaluateWithDetails() to get metadata about why a flag was enabled/disabled:
const result = await featsync.evaluateWithDetails('new-checkout', { userId: 'user_123', email: 'john@acme.com',});
console.log(result.enabled); // trueconsole.log(result.reason); // "rule"console.log(result.ruleName); // "Enterprise users"console.log(result.ruleId); // "rule_abc123"Evaluation Reasons
| Reason | Description |
|---|---|
default | No rules matched, using flag’s default state |
rule | A targeting rule matched |
segment | A segment rule matched (Pro) |
percentage | Percentage rollout applied |
disabled | Flag is disabled in this environment |
not_found | Flag does not exist |
error | Evaluation error occurred |
Supported Operators
String Operators
| Operator | Description | Example |
|---|---|---|
equals | Exact match | email equals "admin@acme.com" |
not_equals | Not equal | country not_equals "CN" |
contains | Substring match | email contains "@acme.com" |
not_contains | No substring | email not_contains "test" |
starts_with | Prefix match | email starts_with "admin" |
ends_with | Suffix match | email ends_with "@acme.com" |
in_list | Value in list | country in ["US", "CA", "UK"] |
not_in_list | Not in list | country not_in ["CN", "RU"] |
matches_regex | Regex match | email matches ".*@(acme|corp)\\.com" |
Number Operators
| Operator | Description | Example |
|---|---|---|
eq | Equal | custom.age eq 18 |
neq | Not equal | custom.age neq 0 |
gt | Greater than | custom.age gt 18 |
gte | Greater or equal | custom.requests gte 100 |
lt | Less than | custom.age lt 65 |
lte | Less or equal | custom.requests lte 1000 |
Semver Operators
| Operator | Description | Example |
|---|---|---|
semver_eq | Version equal | custom.app_version semver_eq "2.0.0" |
semver_neq | Version not equal | custom.app_version semver_neq "1.0.0" |
semver_gt | Version greater | custom.app_version semver_gt "2.0.0" |
semver_gte | Version greater or equal | custom.app_version semver_gte "1.5.0" |
semver_lt | Version less | custom.app_version semver_lt "3.0.0" |
semver_lte | Version less or equal | custom.app_version semver_lte "2.9.9" |
AND/OR Logic
Rules can be combined with AND (all) or OR (any) logic:
AND (all): All conditions must match
IF email ends_with "@acme.com" AND country equals "US"THEN enableOR (any): At least one condition must match
IF email ends_with "@beta.com" OR custom.beta_tester equals trueTHEN enableEvaluation Priority
Rules are evaluated in priority order (lower number = higher priority):
- Priority 1: Beta testers rule
- Priority 2: Enterprise users rule
- Priority 3: US users rule
- Default: “Enable for everyone else” setting (if no rules match)
The first matching rule wins. If no rules match, the “Enable for everyone else” setting determines if the flag is enabled or disabled for that user.
Enable for Everyone Else
When you add targeting rules, you can control what happens for users who don’t match any rules:
- On (default): Users who don’t match any rule get the flag enabled
- Off: Users who don’t match any rule get the flag disabled
This toggle only appears in the dashboard when you have targeting rules configured. It allows you to create:
- Allowlist: Only users matching rules get the feature (toggle Off)
- Blocklist: Everyone gets the feature except those matching “disable” rules (toggle On)
Plan Limits
| Feature | Free | Pro |
|---|---|---|
| Targeting rules | 2 per flag | Unlimited |
| Segments | - | Unlimited |
| All operators | Yes | Yes |
Migration from isEnabled
If you’re currently using isEnabled(), you can gradually migrate to evaluate():
// Simple check (no targeting)const enabled = await featsync.isEnabled("new-checkout");// With targeting contextconst enabled = await featsync.evaluate("new-checkout", { userId: currentUser.id, email: currentUser.email, custom: { plan: currentUser.plan }});Both methods work for the same flags. Use isEnabled() for simple boolean checks, and evaluate() when you need targeting.
Best Practices
1. Include userId for Consistency
Always include userId to ensure users get consistent flag values:
// Good - user gets consistent experienceconst enabled = await featsync.evaluate('feature', { userId: currentUser.id, // ... other attributes});2. Use Specific Attributes
Be specific with your attributes to make targeting rules clearer:
// Good - clear attributescustom: { subscription_plan: "enterprise", company_size: 500}
// Avoid - vague attributescustom: { type: "big", level: "high"}3. Handle Errors Gracefully
evaluate() returns false on errors, but you can provide a default:
// Custom default valueconst enabled = await featsync.evaluate('feature', context, true);4. Debug with evaluateWithDetails
Use evaluateWithDetails() during development to understand targeting:
if (process.env.NODE_ENV === 'development') { const result = await featsync.evaluateWithDetails('feature', context); console.log(`Flag: ${result.enabled}, Reason: ${result.reason}`);}How Client-Side Evaluation Works
The SDK uses client-side evaluation for optimal performance:
- Fetch once: When you call
evaluate(), the SDK fetches all flags with their targeting rules - Cache locally: Flags and rules are cached (default: 5 minutes)
- Evaluate locally: Targeting rules are evaluated in your app, not on the server
- No extra calls: Subsequent
evaluate()calls use the cache
First call:App → SDK → API (fetches flags + rules) → Cache ↓ Local evaluation → Result
Subsequent calls:App → SDK → Cache ↓ Local evaluation → Result (no API call!)This approach minimizes API calls while providing instant flag evaluations.
Related
- Segments - Create reusable user groups (Pro)
- Targeting Concepts - Understanding targeting rules