---
title: Layer 2 — Realize rules
description: Configure which identities are allowed to complete authentication
  for an application — by email (with `*` glob), Steam ID, account alias, or
  sector subject.
editUrl: true
head: []
template: doc
sidebar:
  order: 3
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

import { CardGrid, LinkButton, LinkCard } from "@astrojs/starlight/components";

:::tip[Part of the three-layer rules model]
Layer 2 is one of three rule layers. The overview explains allowlist + default-deny, evaluation order, and how the layers compose.

<LinkButton href="/en-us/application-rules/overview/" variant="secondary" icon="left-arrow">Read the overview</LinkButton>
:::

Layer 2 controls **which identities** may complete authentication for an application. It runs **after** the user has successfully proven they own an identity but **before** the inquiry is marked realized.

The reason this is a separate layer: authenticating that a user is `alice@example.com` and *deciding whether the application is willing to let `alice@example.com` in* are different questions. Layer 1 answers the first, Layer 2 answers the second.

## Supported constraint types

Layer 2 has five constraint types. An application may mix them — within the layer, rules are OR'd, so any matching rule allows the realize.

| Type | Payload | Match semantics |
|---|---|---|
| `EMAIL` | `{ "allowedEmails": string[] }` | Case-insensitive glob with `*` |
| `STEAM_ID` | `{ "allowedSteamIds": (string \| "*")[] }` | Exact match on decimal SteamID64, or literal `"*"` wildcard |
| `ACCOUNT_ALIAS` | `{ "allowedAccountAliases": string[] }` | Exact match on the realizing account's **account alias** (the user-visible, application-invisible, rotatable handle) — **no wildcard** |
| `SECTOR_SUBJECT` | `{ "allowedSectorSubjects": string[] }` | Exact match on the realizing account's **sector subject** for the application's sector (the application-visible token `sub`) — **no wildcard** |
| `EVERYONE` | `{}` | Unconditional allow — matches every authenticated account, including one with no verified email. Empty payload. |

In every case, the matched value comes from the realizing account/session:

- `EMAIL` is checked against **every verified email the account owns** — the full set of `EmailIdentity` rows, not just the email used at this login. The rule matches if **any one** of those emails satisfies the allowlist. For email-OTP register where the account does not yet exist, the matching is against the single email being registered.
- `STEAM_ID` is checked against the verified SteamID64 for **either** Steam path — the native `STEAM_TICKET` direct-issue from inside a game, or the browser `STEAM_OPENID` "Sign in with Steam" button. Both paths resolve to the same per-user Steam identity. Steam-first accounts that have never linked an email need a `STEAM_ID` (or `ACCOUNT_ALIAS` / `SECTOR_SUBJECT`) rule to pass; an `EMAIL`-only Layer 2 will reject them.
- `ACCOUNT_ALIAS` is checked against the account's **account alias** — the rotatable, user-facing handle a user sees and manages in the With portal (e.g. `quiet-meadow-7h2k-9m4p-3fnp-falcon`). It is never exposed to applications, so it is the value a rule author allow-lists to pin a specific human regardless of which application they realize through.
- `SECTOR_SUBJECT` is checked against the account's **sector subject** — the per-(account × sector) opaque token that an application actually sees as the token `sub` (e.g. `sub_9SQ5535CRWNDDM2T`). Applications sharing a sector see the same subject for a given user; applications in different sectors see different subjects. Use it to allow-list users by the exact identifier this application receives.

Both `ACCOUNT_ALIAS` and `SECTOR_SUBJECT` are opaque values matched **exactly** (never format-validated) and have **no wildcard**. Because both are minted only after the account (and its per-sector subject) exists, a fresh registration produces values nobody could have pre-allowlisted — an application whose Layer 2 contains **only** `ACCOUNT_ALIAS` or `SECTOR_SUBJECT` rules cannot accept new sign-ups. They are meant for locking access to a fixed set of already-existing accounts. Both values are rotatable; rotating one invalidates any rule that allow-listed the old value.

## EMAIL glob semantics

`allowedEmails` entries are matched as case-insensitive globs where **only `*` is special** (it matches zero or more characters). All other characters, including `@`, `.`, and `+`, are literal.

| Pattern | Matches |
|---|---|
| `alice@example.com` | exact email |
| `*@example.com` | any address on the `example.com` domain |
| `alice+*@example.com` | any plus-tagged variant of `alice@example.com` |
| `*` | any email |

Inputs are trimmed and lowercased before comparison.

## STEAM_ID semantics

Each entry is either the literal `"*"` (matches any verified SteamID64) or a decimal SteamID64 string (exact case-sensitive match). The `[0-9]{1,20}` shape is enforced at write time.

```json
{
  "constraintType": "STEAM_ID",
  "payload": {
    "allowedSteamIds": ["76561198000000000", "76561198000000001"]
  }
}
```

## ACCOUNT_ALIAS semantics

Each entry is an exact string equal to the target account's **account alias** — the rotatable, user-facing handle (e.g. `quiet-meadow-7h2k-9m4p-3fnp-falcon`). There is no wildcard and no glob; the value is opaque and compared literally.

```json
{
  "constraintType": "ACCOUNT_ALIAS",
  "payload": {
    "allowedAccountAliases": [
      "quiet-meadow-7h2k-9m4p-3fnp-falcon",
      "bold-harbor-2x4q-8m1p-5kna-otter"
    ]
  }
}
```

The alias is the With-portal handle a user manages for themselves — it is never disclosed to applications, so it is the right key when you want to pin a specific human across every application they realize through. A user who rotates their alias drops out of any rule that allow-listed the old value.

## SECTOR_SUBJECT semantics

Each entry is an exact string equal to the target account's **sector subject** for this application's sector — the application-visible token `sub` (e.g. `sub_9SQ5535CRWNDDM2T`). There is no wildcard and no glob; the value is opaque and compared literally.

```json
{
  "constraintType": "SECTOR_SUBJECT",
  "payload": {
    "allowedSectorSubjects": [
      "sub_9SQ5535CRWNDDM2T",
      "sub_4K2P8M1N7QRWXY3Z"
    ]
  }
}
```

The sector subject is the exact identifier this application (and any sibling application sharing its sector) receives for a user; an application in a different sector sees a different subject for the same human. Use it to allow-list by the value you already key your users on. Rotating a user's sector subject drops them out of any rule that allow-listed the old value.

Both are literal allowlists with **no wildcard**, so combine either with an `EMAIL` or `STEAM_ID` rule when the flow should also let new users sign up.

## EVERYONE semantics

`EVERYONE` is the explicit "this application is open to anyone" rule. It carries an empty payload and matches **every** authenticated account unconditionally.

```json
{
  "constraintType": "EVERYONE",
  "payload": {}
}
```

This does not weaken the allowlist-with-default-deny model — it is an explicit opt-in. An application with no Layer 2 rules still rejects everyone; `EVERYONE` is how you say "any account that cleared Layer 1 may realize." It is the **only** Layer 2 type that admits an account with **no verified email** (a Steam-only or Battle.net-only user, for example), so it is the right choice for a genuinely public application that should not gate on identity at all. Add it alongside other rules and the OR semantics make it dominant — any account matches — so use it deliberately.

## Application rule shape

Every Layer 2 rule has the same envelope; only the `constraintType` + `payload` shape differs.

```json
{
  "constraintType": "EMAIL",
  "payload": { "allowedEmails": ["*@example.com"] },
  "accessTokenTtlSeconds": null,
  "refreshTokenTtlSeconds": null
}
```

Multiple rules on an application are OR'd: an identity that matches any rule is allowed through.

## Narrowing on `/establish`

The `realizeConstraints` field on `/establish` carries the same shape and narrows the allowed identities for a single inquiry:

```json
{
  "applicationAnchor": "my-app",
  "realizeConstraints": [
    {
      "constraintType": "EMAIL",
      "payload": { "allowedEmails": ["admin@example.com"] }
    }
  ]
}
```

- Field absent → no narrowing; the application's rules alone decide.
- Field present and empty array → rejected.
- Field present and non-empty → AND-combined with the application's rules. The inner payload list must itself be non-empty.

## Worked example

The application has one Layer 2 rule: `EMAIL` with `allowedEmails: ["*@example.com"]`. A particular admin inquiry passes `realizeConstraints: [{ constraintType: "EMAIL", payload: { allowedEmails: ["admin@example.com"] } }]`.

| User authenticates as | App allows? | Inquiry allows? | Result |
|---|---|---|---|
| `admin@example.com` | yes (matches `*@example.com`) | yes | realized |
| `alice@example.com` | yes (matches `*@example.com`) | no | rejected post-auth |
| `attacker@other.com` | no | n/a | rejected post-auth |

Note that Layer 2 runs **after** Layer 1 — the user has already proven they own the address. Layer 2 then decides whether the application is willing to accept that specific identity for this session.

## Related

<CardGrid>
<LinkCard
    title="The three-layer rules model"
    description="The overall picture — allowlist + default-deny, evaluation order, and how the three layers compose."
    href="/en-us/application-rules/overview/"
/>
<LinkCard
    title="Layer 1 — Authentication rules"
    description="Which authentication methods are offered to the user."
    href="/en-us/application-rules/authentication-rules/"
/>
<LinkCard
    title="Layer 3 — Return rules"
    description="How the realized session is delivered back to the application."
    href="/en-us/application-rules/return-rules/"
/>
</CardGrid>