Policy

The members service asks the organization's policy three things, through @fairgarden/policy, as the id service does. Each is answered by members' built-in rules until an organization's policy says otherwise.

admit

May this person join, at what status, and if not, what stands in the way? Asked while they read the join page (Preview), and again when they ask to join (Join):

{
  "user": { "name": "d4618cea-…" },
  "purpose": "Join",
  "applicant": {
    "email": "ada@example.org",
    "email_verified": true,
    "name": "Ada",
    "residential_address": { "locality": "Minneapolis", "postal_code": "55401", "country": "US" }
  },
  "referral": { "by": { "name": "…", "status": "active", "roles": ["founder"], "since": "2025-03-01T…" } }
}

applicant is what the id service vouches for, as far as they let it share: address (where they receive post) and residential_address (where they live) only when the rules asked for them and they shared them, and as they gave them — id does not verify addresses. referral is there when they came with a member's link that still works.

{ "allow": false, "status": "active", "reasons": { "referral": "A member needs to refer you. Ask one for a referral link." } }

Every reason is shown to them. Built in: anyone may join, as active.

scopes

What to ask the id service for when someone signs in: openid, email and profile, and whatever else the organization's rules for joining read — residential_address, when who may join depends on where they live. When the rules need something the person held back, the join page offers to ask them again. They still choose, on the id service's consent screen.

release

Which of a member's claims go to the service asking, and why not the rest:

{
  "user": { "name": "d4618cea-…" },
  "client": { "id": "gallery", "name": "Gallery" },
  "purpose": "Preview",
  "claims": { "membership": { "status": "active", "roles": ["steward"] } }
}
{ "claims": {}, "reasons": { "membership": "Only Events can see your membership." } }

The reasons go back to the id service in the review, and it shows them to the member on the consent screen. Built in: everything.

The organization's rules

policies/members.rego asks these the way the built-in rules answer them, with places for an organization's own:

unmet[<requirement>] := <reason>   # something an applicant must do first
status := <status>                 # what new members join as, when not "active"
extra_scopes contains <scope>      # more to ask the id service for
withheld[<claim>] := <reason>      # a claim kept from a service, or part of one: "membership.roles"

The organization writes them in its distribution's policies/, in package fairgarden.members. examples/admission is one: new members on probation, with a verified email, referred by an active member, living in Minneapolis (and nobody, should its settings go missing) —

unmet["referral"] := "A member needs to refer you. Ask one for a referral link." if {
	not input.referral.by.status == "active"
}

The distribution builds members' rules and the organization's into one policy when it deploys, and members' build takes a copy (fg-dist policy use) to run in process. On its own, outside a distribution, members has no policy to copy, and its built-in rules decide.

pnpm policy:test                                           # members' rules and tests
fg-policy test --base policies --dir examples/admission    # with an organization's on top

The whole policy, in the organization's words, is on the id service's /policy page, which the join page links to.

Every decision is recorded

In members_policy_decisions, in OPA's decision log format, labelled with the revision of the policy that made it, and kept FG_MEMBERS_POLICY_LOG_RETENTION_DAYS (400). Every revision members runs is kept in members_policy_revisions. What is not needed is left out: the claims on offer (the result records what left), and an applicant's email, name and addresses (their sub says who, the result whether it counted, and most who apply never join).

pnpm policy:log --subject d4618cea-… --since 30d

Failing closed

If the policy cannot be evaluated, nobody joins — they are told joining is unavailable — and nothing is released. Signing in still works, asking only for what membership needs.