Case study
Portal access by domain membership — proven live on SpiceDB and Ory Keto.
GP.Family is a platform built on Frauthy (Level 2). Its premise: a family or group brings its own identity domain, and membership grants portal access automatically. Owning foo@mypeople.com is enough.
The Frauthy Schema that GP.Family runs — three entities, one computed permission, zero user lists:
// gpfamily — portal access by domain membership entity user {} entity domain { relation member: user } entity portal { relation trusted: domain permit access = trusted.member }
Portal access is computed from membership of any trusted domain — pure ReBAC composition. No user lists, no invites. The schema compiles to both SpiceDB schema and Ory Keto OPL.
definition user {} definition domain { relation member: user } definition portal { relation trusted_domain: domain permission access = trusted_domain->member }
class User implements Namespace {} class Domain implements Namespace { related: { member: User[] } } class Portal implements Namespace { related: { trusted_domain: Domain[] } permits = { access: (ctx: Context): boolean => this.related.trusted_domain.traverse( (d) => d.related.member.includes(ctx.subject) ), } }
backend keto in the schema file.Foo signs in through their domain’s OIDC provider. The ID token carries a verified email claim — the only artifact the authorization side needs.
{
"iss": "https://login.mypeople.com",
"sub": "u_8a1f…",
"aud": "gpfamily-portal",
"email": "foo@mypeople.com",
"email_verified": true
}The user authenticates through their domain’s OIDC provider. The ID token carries a verified email claim — the identity half of the story. Everything else — mapping that email to a relationship, checking portal access — happens on the authorization side, without ever touching the login flow again.
The join that Frauthy automates — turning an identity claim into an authorization relationship.
// frauthy mapping config mapping: [{ claimField: "email_domain", match: { kind: "domain_equals", value: "mypeople.com" }, emit: { object: "domain:{domain}", relation: "member", subject: "user:{sub}", }, }]
When the email claim matches a trusted domain, Frauthy writes the relationship automatically. The resulting tuple:
domain:mypeople.com#member@user:foo@mypeople.comemail_verified must be true — the SDK silently skips mapping rules when it is false. This private-beta security behavior prevents an unverified email from creating a relationship.One call resolves portal access — no per-user lookup, no role table, just a graph walk.
Check( portal:gpfamily, access, user:foo@mypeople.com )
The store — SpiceDB or Keto, identical result — walks the graph:
Start with the user
Resolve the verified GP.Family member.
user:foo@mypeople.comFollow domain membership
The user is a member of the registered identity domain.
domain:mypeople.com#member// the integrating app — no auth code of its own const session = await frauthy.session(req) if (await session.can("access", portal)) { return render("portal") }
Foo reaches the portal the moment they prove they hold a @mypeople.com address — because the group owns the domain, not because anyone added Foo to a list.
Interactive lifecycle lab
Change the verified identity claim, then watch Frauthy map it into the relationship graph and resolve the same access check.
The complete request lifecycle — from OIDC token to portal render — in five stages.
Verify
Verify the OIDC token and extract sub plus email.
Relate
An email_domain match writes domain membership.
domain:mypeople.com#member@user:fooExplain
One OTLP trace spans authentication, mapping, authorization, and decision.
can() → DuckDB-APM observe loop runs end-to-end on real SpiceDB and Ory Keto instances.GP.Family — a live-demo walkthrough of Frauthy Level 2. Portal access by domain membership, no invites, no user lists.