Skip to content

Layer 2 — Realize rules

View as Markdown

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 [email protected] and deciding whether the application is willing to let [email protected] in are different questions. Layer 1 answers the first, Layer 2 answers the second.

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.

TypePayloadMatch 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.

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.

PatternMatches
[email protected]exact email
*@example.comany address on the example.com domain
alice+*@example.comany plus-tagged variant of [email protected]
*any email

Inputs are trimmed and lowercased before comparison.

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.

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

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.

{
"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.

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.

{
"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 is the explicit “this application is open to anyone” rule. It carries an empty payload and matches every authenticated account unconditionally.

{
"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.

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

{
"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.

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

{
"applicationAnchor": "my-app",
"realizeConstraints": [
{
"constraintType": "EMAIL",
"payload": { "allowedEmails": ["[email protected]"] }
}
]
}
  • 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.

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

User authenticates asApp allows?Inquiry allows?Result
[email protected]yes (matches *@example.com)yesrealized
[email protected]yes (matches *@example.com)norejected post-auth
[email protected]non/arejected 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.