A powerful, type-safe targeting and feature flag system for dynamically serving different content based on query conditions.
| JS Runtime | Command |
|---|---|
| Node.js | npx jsr add @targetd/api |
| Bun | bunx jsr add @targetd/api |
| Deno | deno add jsr:@targetd/api |
@targetd/api provides an in-memory data store that allows you to define rules
for serving different payloads based on targeting conditions. It's ideal for:
- Feature flags and A/B testing
- Content personalization based on user attributes
- Dynamic configuration management
- Context-aware API responses
Configure payload and targeting schemas with DataSchema, then pass the schema
directly to Data.create():
import { Data, DataSchema, targetEquals, targetIncludes } from '@targetd/api'
import { z } from 'zod'
const schema = DataSchema.create()
.usePayload({
greeting: z.string(),
})
.useTargeting({
country: targetIncludes(z.string()),
})
const data = await Data.create(schema).addRules('greeting', [
{
targeting: { country: ['US'] },
payload: 'Hello!',
},
{
targeting: { country: ['ES'] },
payload: '¡Hola!',
},
{
payload: 'Hi!', // default fallback
},
])
// Query the data
const greeting = await data.getPayload('greeting', { country: 'US' })
// Returns: 'Hello!'
const defaultGreeting = await data.getPayload('greeting')
// Returns: 'Hi!'Schema configuration (DataSchema) and data operations (Data) are split so
that TypeScript only has to resolve the accumulated parser types on the schema,
then reuse that single type inside Data.create(). This keeps compilation cheap
even when hundreds of payloads and targeting descriptors are chained together.
Define the types of data your store will manage using Zod schemas:
const schema = DataSchema.create()
.usePayload({
message: z.string(),
config: z.object({
enabled: z.boolean(),
maxRetries: z.number(),
}),
})Targeting rules determine which payload to serve based on query parameters. Use built-in predicates or create custom ones:
-
targetIncludes: Check if a value is in an arrayconst schema = DataSchema.create() .usePayload({ content: z.string() }) .useTargeting({ channels: targetIncludes(z.string()) }) const data = await Data.create(schema).addRules('content', [ { targeting: { channels: ['mobile', 'web'] }, payload: 'Multi-platform content', }, ])
-
targetEquals: Check for exact equalityconst schema = DataSchema.create() .usePayload({ feature: z.string() }) .useTargeting({ isPremium: targetEquals(z.boolean()) }) const data = await Data.create(schema).addRules('feature', [ { targeting: { isPremium: true }, payload: 'Premium feature', }, ])
Create custom targeting logic with predicates:
const schema = DataSchema.create()
.usePayload({ message: z.string() })
.useTargeting({
timeOfDay: {
predicate: (queryTime) => (targetTime) => {
return queryTime === targetTime
},
queryParser: z.enum(['morning', 'afternoon', 'evening']),
targetingParser: z.enum(['morning', 'afternoon', 'evening']),
},
})
const data = await Data.create(schema).addRules('message', [
{
targeting: { timeOfDay: 'morning' },
payload: 'Good morning!',
},
])Predicates can be asynchronous:
DataSchema.create().useTargeting({
hasAccess: {
predicate: (userId) => async (requiredRole) => {
const user = await fetchUser(userId)
return user.role === requiredRole
},
queryParser: z.string(),
targetingParser: z.string(),
},
})Set requiresQuery: false for predicates that don't need query parameters:
import { createTargetingDescriptor, DataSchema } from '@targetd/api'
DataSchema.create().useTargeting({
currentTime: createTargetingDescriptor({
predicate: () => (targetTime) => {
return new Date().getHours() === targetTime
},
queryParser: z.undefined(),
targetingParser: z.number(),
requiresQuery: false,
}),
})Rules map targeting conditions to payloads. Rules are evaluated in order, and the first matching rule wins:
.addRules('feature', [
{
// Most specific rule first
targeting: {
country: ['US'],
isPremium: true
},
payload: 'Premium US feature'
},
{
// Less specific
targeting: { country: ['US'] },
payload: 'US feature'
},
{
// Default (no targeting)
payload: 'Default feature'
}
])Use an array of targeting objects for OR conditions:
.addRules('content', [
{
targeting: [
{ weather: ['sunny'] },
{ highTide: true }
],
payload: 'Beach content'
}
])
// Matches if weather is sunny OR highTide is trueVariables allow you to define reusable payload values with their own targeting
rules. A variable can be referenced in a payload using {{variableName}}
syntax, and the variable itself resolves to a payload based on the query:
Note: Variables are replaced with their payload values directly—they cannot be interpolated into strings. For example,
'{{featureEnabled}}'will be replaced with the variable's value (e.g.,true), not interpolated as part of a string like'Feature is {{featureEnabled}}'.
.addRules('config', {
variables: {
featureEnabled: [
{
targeting: { country: ['US'] },
payload: true
},
{
payload: false
}
],
maxRetries: [
{
payload: 5
}
]
},
rules: [
{
payload: {
enabled: '{{featureEnabled}}',
retries: '{{maxRetries}}'
}
}
]
})
// Query the data
const config = await data.getPayload('config', { country: 'US' })
// Returns: { enabled: true, retries: 5 }
const defaultConfig = await data.getPayload('config')
// Returns: { enabled: false, retries: 5 }Variables are particularly useful when you need the same targeting logic across multiple payloads or when combining static and dynamic values in complex data structures.
Fall-through targeting is used when one service cannot fully evaluate all
targeting conditions. In these cases, payloads with unresolved fall-through
targeting are returned in a special format (__rules__ structure) that can be
passed to another service for final evaluation.
This is useful in distributed systems where different services have access to different context:
const schema = DataSchema.create()
.usePayload({ message: z.string() })
.useTargeting({
channel: targetIncludes(z.string()),
})
.useFallThroughTargeting({
region: z.array(z.string()),
})
const data = await Data.create(schema).addRules('message', [
{
targeting: {
channel: ['mobile'],
region: ['EU'], // fall-through
},
payload: 'EU mobile message',
},
{
targeting: {
channel: ['mobile'],
},
payload: 'Mobile message',
},
])
// Query with only the regular targeting field
const result = await data.getPayload('message', { channel: 'mobile' })
// Returns: { __rules__: [...], __variables__: {...} }
// This can be passed to another service that has region contextThe service receiving the __rules__ structure can use the
insert() method to add this data and then evaluate the
fall-through targeting conditions with its own context:
// In the receiving service with region context
const receivingConfig = DataSchema.create()
.usePayload({ message: z.string() })
.useTargeting({
region: targetIncludes(z.string()),
})
const receivingServiceData = await Data.create(receivingConfig).insert({
message: result, // The __rules__ structure from the first service
})
// Now evaluate with region context
const finalPayload = await receivingServiceData.getPayload('message', {
region: 'EU',
})
// Returns: 'EU mobile message'Get the first matching payload for a given name:
const payload = await data.getPayload('feature', { country: 'US' })Get all matching payloads (useful for testing/debugging):
const allMatches = await data.getPayloads('feature', { country: 'US' })
// Returns array of all matching payloadsGet payloads for all registered names at once:
const allPayloads = await data.getPayloadForEachName({ country: 'US' })
// Returns: { feature: '...', message: '...', ... }Insert data from one Data object to another. This is commonly used with fall-through targeting to pass unresolved rules between services:
const updated = await data.insert({
feature: result, // Can be a simple value or __rules__ structure from another Data object
})Remove all rules while keeping parsers:
const empty = data.removeAllRules()Use withNegate option to support negative targeting:
const schema = DataSchema.create()
.usePayload({ content: z.string() })
.useTargeting({
platform: targetIncludes(z.string(), { withNegate: true }),
})
const data = await Data.create(schema).addRules('content', [
{
targeting: { platform: ['!mobile'] },
payload: 'Desktop-only content',
},
])
// Matches everything except mobile
await data.getPayload('content', { platform: 'desktop' })const schema = DataSchema.create()
.usePayload({ experience: z.string() })
.useTargeting({
weather: targetIncludes(z.string()),
tide: targetEquals(z.boolean()),
wind: targetEquals(z.string()),
})
const data = await Data.create(schema).addRules('experience', [
{
// All conditions must match (AND)
targeting: {
weather: ['sunny'],
tide: true,
wind: 'strong',
},
payload: 'Perfect surfing conditions! 🏄♂️',
},
{
targeting: {
weather: ['sunny'],
},
payload: 'Nice day! 😎',
},
{
payload: 'Regular day',
},
])The library provides full TypeScript type inference:
const schema = DataSchema.create()
.usePayload({ message: z.string() })
.useTargeting({ country: targetIncludes(z.string()) })
const data = await Data.create(schema)
// ✅ Type-safe
await data.getPayload('message', { country: 'US' })
// ❌ Type error: 'unknown' is not a valid payload name
await data.getPayload('unknown')
// ❌ Type error: 'invalidField' is not a valid query parameter
await data.getPayload('message', { invalidField: 'value' })See LICENSE file for details.