Field of membership, explained for developers.

A credit union may only serve the group its charter defines — its field of membership. That is why “can this person join?” has a different answer at every institution, why the answer is published as prose rather than data, and why modelling it as a single geographic boolean will quietly lose you eligible users.

Last updated 2026-08-21Domain model + codeNo API key needed to read

The three charter types

Federal credit unions hold one of three. This is the first thing that determines the shape of the rule you have to evaluate.

Single common bond
One employer or one association. The cleanest to evaluate: does this person belong to that one group?
Multiple common bond
Several such groups — sometimes hundreds of employers, sometimes listed under former trading names. Entity resolution is the whole difficulty here.
Community
Everyone who lives, works, worships or attends school in a defined area. The most common, and the one whose modelling trap is described below.
State-chartered credit unions follow their state regulator’s equivalent rules. Similar in spirit, not identical in detail — do not assume a federal model covers them.

The trap: four verbs, not one

"Lives, works, worships or attends school in X" is four separate rules sharing a sentence.

The tempting model is one boolean: is the user in the area? It is wrong in both directions. Someone who commutes into a county qualifies under works and fails under lives. A student qualifies under attends school while satisfying nothing else. Collapse them and you lose eligible people silently — and, worse, you can no longer explain any verdict you produce, because the sentence that justified it no longer exists in your data.

Every eligibility bug looks like this. Not a crash — a confident wrong answer that nobody notices because there is nothing to compare it to. Keep the verb.

A model that survives the real rules

TSRules and conditions
// The shape that survives contact with the real rules.
type Rule = {
rule_id: number;
credit_union_id: string;
kind: "geography" | "employer" | "association" | "military" | "family" | "open";
// Provenance is NOT optional: a verdict you cannot trace is a verdict you
// cannot defend, correct or supersede.
source_id: number;
evidence_quote: string;
observed_at: string;
confidence: number;
superseded_at: string | null;
conditions: Condition[]; // ALL must hold for the rule to qualify someone
};
type Condition =
| { kind: "geo_residence" | "geo_employment" | "geo_worship" | "geo_school";
geo_level: "state" | "county" | "zip";
geo_value: string } // canonical: "county:NC:mecklenburg"
| { kind: "employer"; org_id: string }
| { kind: "association"; org_id: string; join_cost_usd?: number }
| { kind: "military"; military_branch?: string; military_relationship?: string }
| { kind: "family"; family_relations: string[] }
| { kind: "open_to_anyone" };
Rule → conditions
A rule qualifies someone when ALL its conditions hold. Two ways to qualify are two rules, not one rule with an OR — that keeps each explanation self-contained.
Canonical geo keys
"county:NC:mecklenburg" rather than a free-text county name. Every geographic lookup becomes an exact index hit instead of a fuzzy match.
Org ids, not names
Employers and associations resolve through an alias index. "Atrium Health", "Carolinas HealthCare" and a typo should reach the same organisation.
Provenance on every rule
Source, evidence quote, observed_at, confidence. Without these you cannot explain a verdict, correct one, or tell a stale rule from a current one.
superseded_at
Rules change. Supersede rather than delete, so a verdict issued last year can still be explained this year.

Four verdicts, and the invariant

TSStatus vocabulary
// Four values, not two. The fourth is the one that keeps you honest.
type Status =
| "eligible" // a rule matched the facts given
| "conditionally_eligible" // one step away; name the step and its cost
| "possibly_eligible" // a rule plausibly applies; facts do not confirm
| "unknown"; // insufficient rule data — NOT ineligible
// The invariant worth writing a test for:
// unknown !== ineligible
// Treating "we have no rules for this institution" as "you cannot join" is a
// silent, confident, wrong answer — the worst kind.
A two-value model has to put “you can join for $5” and “we have no data on this institution” in the same bucket. Those lead to opposite product behaviour, so they cannot share a name.

Frequently asked