ARFC-1005: Self-Joinable Realms

Status

Abandoned Draft Date: 2026-02-26 Last Call Date: 2026-03-05 Publication Date: 2026-03-12 Version: 0.8

Abstract

This RFC proposes adding self-join capability to realms, allowing users to join “open” realms without requiring an administrator invitation. This enables new platform users to discover and join realms (and subsequently teams within those realms) through a streamlined onboarding flow.

Table of Contents

  1. Introduction
  2. Motivation
  3. Specification
  4. Security Considerations
  5. Backward Compatibility
  6. References

1. Introduction

The ĀYŌDÈ platform organizes users into hierarchical realms. Currently, realm membership requires an administrator to explicitly add users via invitation or direct assignment. This creates friction for open competitions and public programs where users should be able to self-enroll.

This RFC introduces a isSelfJoinable flag on realms that, when enabled, allows eligible users to join without administrator intervention.


2. Motivation

Current State

Use Cases

  1. Open Competitions: Flight Path and similar competitions need to allow any registered platform user to join the competition realm and form/join teams
  2. Public Programs: Educational programs may want open enrollment at certain levels
  3. Hierarchical Access: A school district realm may be invite-only, but individual school realms within it may allow self-join for verified students

Design Goals

  1. Simple flag: Single boolean column, no complex configuration
  2. Hierarchical awareness: Self-join rules differ based on realm position in hierarchy
  3. No privilege escalation: Self-joining grants basic membership only, not administrative access
  4. Audit trail: All self-join actions are logged via standard audit columns

3. Specification

3.1 Schema Changes

Add one column to the existing Realms table:

Column Definition:

Column Type Nullable Description
selfJoinRoleID INT NULL Role automatically assigned to users who self-join (FK to Roles). When non-NULL, the realm is self-joinable.

Derived Property:

isSelfJoinable := (selfJoinRoleID is not null)

A realm is self-joinable if and only if it has a selfJoinRoleID configured. This eliminates redundancy and ensures that self-joinable realms always grant meaningful privileges.

Design Rationale:

Following the PreconfiguredPositionAssignments pattern, self-joining must grant both:

  1. RealmMembership — access to the realm
  2. RoleMembership — privileges within the realm (e.g., /team/join)

A self-joinable realm without a role would be useless — users could join but do nothing. By making selfJoinRoleID the single source of truth, we enforce that self-joinable realms always have an associated role.

Constraints:


3.2 Self-Join Eligibility Rules

The eligibility to self-join a realm depends on whether the realm is a top-level realm or a descendant realm.

flowchart TB
    Start([User requests to join Realm]) --> CheckFlag{selfJoinRoleID IS NOT NULL?}
    CheckFlag -->|No| Reject([Reject: Realm is not self-joinable])
    CheckFlag -->|Yes| CheckLevel{Is Realm top-level?}

    CheckLevel -->|Yes| TopLevel[Top-Level Realm]
    CheckLevel -->|No| Descendant[Descendant Realm]

    TopLevel --> CheckPlatform{Is User a platform member?}
    CheckPlatform -->|Yes| AllowJoin([Allow: Add to RealmMembership])
    CheckPlatform -->|No| RejectNotPlatform([Reject: Not a platform user])

    Descendant --> FindRoot[Lookup topmost ancestor via RealmClosure]
    FindRoot --> CheckAncestor{Is User a member of topmost realm?}
    CheckAncestor -->|Yes| AllowJoin
    CheckAncestor -->|No| RejectNotInHierarchy([Reject: Not in realm hierarchy])

    style AllowJoin fill:#c8e6c9,color:#1b1a17
    style Reject fill:#ffcdd2,color:#1b1a17
    style RejectNotPlatform fill:#ffcdd2,color:#1b1a17
    style RejectNotInHierarchy fill:#ffcdd2,color:#1b1a17

Rule Summary

Realm Type Eligibility Requirement
Top-level realm (parentID IS NULL) Any authenticated platform user
Descendant realm (parentID IS NOT NULL) User must be a member of the topmost ancestor realm in the hierarchy

Topmost Ancestor Resolution

The RealmClosure table provides direct lookup of any ancestor without traversal. The topmost ancestor is the entry with maximum depth:

topmostRealmID := RealmClosure.lookup(
    descendantID = realmID,
    depth = MAX(depth for this descendant)
).ancestorID

Note: RealmClosure contains pre-computed ancestor-descendant pairs for all depths, enabling O(1) hierarchy lookups.

Membership Check for Descendant Realms

isMember := exists(RealmMembership where realmID = topmostRealmID and userID = user)

3.3 API Endpoints

New Endpoints

Method Path operationId Description
GET /v1/users/~/realms?relationship=member listUserRealmsV1 List realms the current user is a member of
GET /v1/users/~/realms?relationship=joinable listUserRealmsV1 List realms the current user is eligible to join
POST /v1/realms/{realm-eid}/members/~ selfJoinRealmV1 Self-join a realm (if eligible)
DELETE /v1/realms/{realm-eid}/members/~ leaveRealmV1 Leave a realm (self-initiated)

Note: The ~ path segment is a self-referential identifier representing the authenticated user. The relationship query parameter filters the user’s relationship to realms:

Modified Endpoints

Method Path Change
PATCH /v1/realms/{realm-eid} Add isSelfJoinable to updateable fields
GET /v1/realms/{realm-eid} Include isSelfJoinable in response

Endpoint Details

GET /v1/users/~/realms

Returns realms based on the user’s relationship to them.

Query parameters:

Response:

{
  "envelope": { "status": "success", "requestID": "..." },
  "data": [
    {
      "eidURN": "urn:ayode:realm-eid:uuid",
      "name": "Flight Path 2026",
      "description": "Open competition realm",
      "isSelfJoinable": true,
      "selfJoinRoleEIDURN": "urn:ayode:role-eid:uuid",
      "isTopLevel": true,
      "relationship": "joinable"
    }
  ]
}

Note: isSelfJoinable is derived from selfJoinRoleID IS NOT NULL and included for convenience.

POST /v1/realms/{realm-eid}/members/~

Self-join request. No request body required.

Success response (201 Created):

{
  "envelope": { "status": "success", "requestID": "..." },
  "data": {
    "membershipEIDURN": "urn:ayode:realm-membership-eid:uuid",
    "realmEIDURN": "urn:ayode:realm-eid:uuid",
    "joinedAt": "2026-02-25T12:00:00.000000Z"
  }
}

Error responses:

DELETE /v1/realms/{realm-eid}/members/~

Leave a realm. No request body required.

Success response (200 OK):

{
  "envelope": { "status": "success", "requestID": "..." },
  "data": {
    "realmEIDURN": "urn:ayode:realm-eid:uuid",
    "leftAt": "2026-02-25T14:30:00.000000Z"
  }
}

Error responses:


3.4 Validation Rules

Self-Join Validation

The api_realmSelfJoin_v1 stored procedure enforces the following checks:

  1. Realm must be self-joinable (selfJoinRoleID IS NOT NULL)
  2. User cannot already be a member (check RealmMembership)
  3. Descendant realm eligibility (user must be member of topmost ancestor)

Topmost ancestor lookup:

topmostRealmID := RealmClosure.lookup(descendantID = realmID, depth = MAX).ancestorID

On successful self-join, insert into both tables:

Constraints Enforced by Stored Procedures

Constraint Enforcement Point
Realm must have selfJoinRoleID IS NOT NULL api_realmSelfJoin_v1 checks before allowing join
User cannot already be a member api_realmSelfJoin_v1 checks existing RealmMembership
Descendant realm requires topmost realm membership api_realmSelfJoin_v1 validates via RealmClosure
selfJoinRoleID must belong to the realm api_realmUpdate_v1 validates role ownership
Only realm admins can modify selfJoinRoleID api_realmUpdate_v1 requires /realm/update privilege

4. Security Considerations

Controlled Privilege Assignment

Self-joining grants:

  1. RealmMembership — access to the realm
  2. RoleMembership — assignment to selfJoinRoleID (if configured)

Privilege boundaries:

Recommended self-join role privileges:

Audit Trail

All self-join actions are tracked via:

This distinguishes self-joins from admin-initiated additions where createdByUserID would be the admin.

Rate Limiting

The POST /v1/realms/{realm-eid}/members/~ endpoint should be rate-limited to prevent abuse:

Realm Discovery

The GET /v1/realms/self-joinable endpoint only returns realms the user is eligible to join:

This prevents information leakage about realm structures.


5. Backward Compatibility

This RFC is additive and backward compatible:

Migration

Add selfJoinRoleID column to Realms table:


6. References


Summary

Component Change
Realms table Add selfJoinRoleID INT NULL (non-NULL = self-joinable)
API Add GET /v1/users/~/realms?relationship={member\|joinable}, POST /v1/realms/{realm-eid}/members/~, DELETE /v1/realms/{realm-eid}/members/~
Stored procedures Add api_realmSelfJoin_v1 (inserts RealmMembership + RoleMembership), api_realmLeave_v1, modify api_realmUpdate_v1

Author

ĀYŌDÈ Development Team Codermerlin Academy Architecture