Layer 2 — Realize rules
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.
Supported constraint types
Section titled “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:
EMAILis checked against every verified email the account owns — the full set ofEmailIdentityrows, 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_IDis checked against the verified SteamID64 for either Steam path — the nativeSTEAM_TICKETdirect-issue from inside a game, or the browserSTEAM_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 aSTEAM_ID(orACCOUNT_ALIAS/SECTOR_SUBJECT) rule to pass; anEMAIL-only Layer 2 will reject them.ACCOUNT_ALIASis 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_SUBJECTis checked against the account’s sector subject — the per-(account × sector) opaque token that an application actually sees as the tokensub(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
Section titled “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 |
|---|---|
[email protected] | exact email |
*@example.com | any address on the example.com domain |
alice+*@example.com | any plus-tagged variant of [email protected] |
* | any email |
Inputs are trimmed and lowercased before comparison.
STEAM_ID semantics
Section titled “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.
{ "constraintType": "STEAM_ID", "payload": { "allowedSteamIds": ["76561198000000000", "76561198000000001"] }}ACCOUNT_ALIAS semantics
Section titled “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.
{ "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
Section titled “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.
{ "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
Section titled “EVERYONE semantics”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.
Application rule shape
Section titled “Application rule shape”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.
Narrowing on /establish
Section titled “Narrowing on /establish”The realizeConstraints field on /establish carries the same shape and narrows the allowed identities for a single inquiry:
{ "applicationAnchor": "my-app", "realizeConstraints": [ { "constraintType": "EMAIL", } ]}- 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
Section titled “Worked example”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 as | App allows? | Inquiry allows? | Result |
|---|---|---|---|
[email protected] | yes (matches *@example.com) | yes | realized |
[email protected] | yes (matches *@example.com) | no | rejected post-auth |
[email protected] | 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.