> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useshipd.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Target users by attribute

> Serve a flag to one user, an email domain, or a plan with dashboard rules that match evaluation context attributes.

## Overview

Targeting rules live in the dashboard. Your code sends an evaluation context (a `userId` and attributes); the SDK matches it against the rules in the snapshot, locally, with no network call. This page shows how to target one user, an email domain, or a plan.

## Send attributes from code

```ts TypeScript theme={null}
// src/checkout.ts
shipd.isEnabled("new-checkout", {
  userId: "usr_8f2k19",
  attributes: {
    email: "dana@fieldkit.dev",
    plan: "pro",
    seats: 12,
  },
});
```

```go Go theme={null}
// checkout.go
client.IsEnabled("new-checkout", shipd.EvaluationContext{
    UserID: "usr_8f2k19",
    Attributes: map[string]any{
        "email": "dana@fieldkit.dev", "plan": "pro", "seats": 12,
    },
})
```

In TypeScript session clients, you can also set them once with [`identify`](/flags/identify-users):

```ts theme={null}
shipd.identify({
  userId: "usr_8f2k19",
  traits: { email: "dana@fieldkit.dev", plan: "pro", seats: 12 },
});

shipd.isEnabled("new-checkout");
```

Rules can only match what you send. Attribute values are strings, numbers, booleans, or `null`. See [`EvaluationContext`](/reference/sdk#types).

## Add a rule

**1. Open the flag**

In the [dashboard](https://app.useshipd.com), open the project, click the flag, and select the environment.

**2. Add a condition**

Click **Add rule**, then **Attribute**. Fill in **Attribute**, **Operator**, and **Value**.

**3. Choose what to serve**

Set the rule's **Serve** value, for example `true`.

**4. Save**

Click **Save environment**. Use the simulator on the flag page to check a user before you save.

## Common rules

| Goal | Attribute | Operator | Value |
| - | - | - | - |
| One user | `userId` | is | `usr_8f2k19` |
| A few users | `userId` | is one of | `usr_8f2k19, usr_3m7q04` |
| An email domain | `email` | ends with | `@fieldkit.dev` |
| Paid plans | `plan` | is one of | `pro, enterprise` |
| Large accounts | `seats` | is at least | `10` |
| Internal staff | `internal` | is | `true` |

The `userId` attribute reads `context.userId`, so you don't need to repeat it in `attributes`. String operators are case-sensitive: lowercase emails before you send them.

## Operators

| Dashboard label | Operator | Matches when the attribute… |
| - | - | - |
| is / is not | `eq` / `neq` | equals / does not equal the value |
| is one of / is not one of | `in` / `not_in` | is / is not in a comma-separated list |
| contains | `contains` | contains the string |
| starts with / ends with | `startsWith` / `endsWith` | starts / ends with the string |
| is greater than / is at least | `gt` / `gte` | is greater than / greater than or equal to |
| is less than / is at most | `lt` / `lte` | is less than / less than or equal to |
| exists / does not exist | `exists` / `not_exists` | is present / absent in the context |

An attribute missing from the context matches only `does not exist`.

### How values compare

`is` and `is one of` (and their negations) coerce numeric and boolean values, and otherwise use exact string equality. Ordered comparisons (`gt`, `gte`, `lt`, `lte`) compare numbers as numbers, then timestamps as instants, and otherwise strings by character code. Equivalent timestamps with different offsets match only in ordered comparisons, not string equality. Current SDK versions (`shipdit` 0.4+, `@shipdit/convex` 0.2+, and Go 0.1+) apply the same rules. Earlier TypeScript SDK versions use different timestamp parsing; see [Upgrade to shipdit 0.4](/migrations/shipdit-0-4).

**Numbers.** A string counts as a number when it's a decimal (`10`, `-2.5`, `.5`, `1e3`) or an unsigned `0x`, `0o`, or `0b` literal (`0x10`). Surrounding whitespace is ignored. Underscores (`1_000`), hex floats (`0x1p-2`), signed hex (`-0x10`), and `Infinity` are not numbers, so they compare as strings.

**Timestamps.** For ordered comparisons, both values must be strings in this format to compare as times:

| Format | Example | Meaning |
| - | - | - |
| `YYYY-MM-DD` | `2026-01-02` | Midnight UTC that day |
| `YYYY-MM-DDThh:mm[:ss[.fraction]]` plus a zone | `2026-01-02T10:00:00Z`, `2026-01-02T12:00+02:00` | That instant. The zone is `Z` or `±hh:mm` and is required. Fractions keep milliseconds; extra digits are dropped. |

Dates are range-checked: `2026-02-30` and `2026-02-29` aren't dates. Anything else, including times without a zone (`2026-01-02T10:00`), a space instead of `T`, or single-digit months (`2026-1-2`), compares as a string. Send timestamps in UTC with a `Z` to avoid surprises.

Saving a flag or audience with a date-looking value outside this grammar in an ordered comparison returns the non-blocking `x-shipd-rule-warning` API header; the dashboard displays the warning. The rule still saves because string ordering is supported. Older TypeScript SDKs and Convex components can disagree with dashboard preview: check the [TypeScript](/migrations/shipdit-0-4#check-timestamp-rules-before-upgrading) or [Convex migration guide](/migrations/convex-0-2#check-timestamp-rules-before-upgrading) before upgrading.

## Rule order

* Rules run top to bottom. **The first matching rule wins**; later rules are not checked.
* All conditions in one rule must match. Use **Any of** to match one of several conditions.
* If no rule matches, the flag serves **Fallthrough**.

Put narrow rules (one user) above broad ones (a plan or a percentage). Reorder rules with the arrows on each rule.

## Reuse targeting with audiences

An audience is a named set of conditions you can reference from any flag in the project. Create one under **Audiences** → **New audience**, then add an **Audience** condition to a rule with **Membership** set to **is in** or **is not in**.

Audiences compile into the snapshot and evaluate locally from the same context attributes. You can also assert membership from code by passing audience keys:

```ts TypeScript theme={null}
shipd.isEnabled("new-checkout", {
  userId: "usr_8f2k19",
  segmentIds: ["beta-testers"],
});
```

```go Go theme={null}
client.IsEnabled("new-checkout", shipd.EvaluationContext{
    UserID: "usr_8f2k19", SegmentIDs: []string{"beta-testers"},
})
```

## Location traits in the browser

`shipdit/client` fills `country`, `region`, `city`, and `timezone` automatically, so you can add rules like `country is US` or `timezone is America/Los_Angeles` without sending them yourself. `country` is an ISO 3166-1 alpha-2 code; `timezone` is the browser's IANA zone.

When the same attribute comes from more than one place, the first source wins:

1. Per-call `attributes`
2. `identify()` traits
3. `createClient({ context })`
4. Browser timezone
5. Edge geolocation

Turn it off with `automaticTraits: false`. Server clients (`shipdit` and Go) don't infer location; pass it as an attribute. See [Browser SDK](/sdks/browser).

## Related

* [Identify users](/flags/identify-users)
* [Roll out a flag to a percentage of users](/flags/percentage-rollouts)
* [Local evaluation](/concepts/local-evaluation)
* [Edge API](/reference/edge-api) for evaluation events and identity


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.