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
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
- Users must be invited or added by realm administrators
- New users cannot independently join realms to participate in competitions
- Teams (per ARFC-1004) require realm membership, creating a chicken-and-egg problem for open competitions
Use Cases
- Open Competitions: Flight Path and similar competitions need to allow any registered platform user to join the competition realm and form/join teams
- Public Programs: Educational programs may want open enrollment at certain levels
- 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
- Simple flag: Single boolean column, no complex configuration
- Hierarchical awareness: Self-join rules differ based on realm position in hierarchy
- No privilege escalation: Self-joining grants basic membership only, not administrative access
- 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:
- RealmMembership — access to the realm
- 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:
selfJoinRoleIDmust reference a role owned by the same realm (enforced by stored procedure)- To make a realm self-joinable: set
selfJoinRoleIDto a valid role - To disable self-join: set
selfJoinRoleID = NULL
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:
member: Realms the user belongs tojoinable: Self-joinable realms the user is eligible to join but is not yet a member of
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:
-
relationship(required):memberjoinablemember: Realms the user is currently a member ofjoinable: Self-joinable realms the user is eligible to join (excludes current memberships)
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:
403 Forbidden: Realm is not self-joinable or user is not eligible409 Conflict: User is already a member
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:
404 Not Found: User is not a member of this realm403 Forbidden: User cannot leave (e.g., sole administrator)
3.4 Validation Rules
Self-Join Validation
The api_realmSelfJoin_v1 stored procedure enforces the following checks:
- Realm must be self-joinable (
selfJoinRoleID IS NOT NULL) - User cannot already be a member (check
RealmMembership) - 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:
RealmMembership— grants realm accessRoleMembership— grants privileges viaselfJoinRoleID
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:
- RealmMembership — access to the realm
- RoleMembership — assignment to
selfJoinRoleID(if configured)
Privilege boundaries:
- Users receive only the privileges defined in
selfJoinRoleID - The role should be a limited “Participant” or “Member” role with basic privileges
- Administrative privileges (e.g.,
/realm/update,/team/manage) should NOT be granted via self-join roles - Realm administrators control which role is assigned by setting
selfJoinRoleID
Recommended self-join role privileges:
urn:ayode:privilege-action:/team/list— view teamsurn:ayode:privilege-action:/team/join— apply to join teamsurn:ayode:privilege-action:/season/list— view seasonsurn:ayode:privilege-action:/season/read— view season details
Audit Trail
All self-join actions are tracked via:
createdByUserID = p_userID(user who joined)createdTimestamp(when they joined)
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:
- Recommended: 10 join requests per user per minute
- Prevents automated realm-hopping or membership spam
Realm Discovery
The GET /v1/realms/self-joinable endpoint only returns realms the user is eligible to join:
- Top-level self-joinable realms are discoverable by all users
- Descendant self-joinable realms are only discoverable by users already in the hierarchy
This prevents information leakage about realm structures.
5. Backward Compatibility
This RFC is additive and backward compatible:
- New column:
isSelfJoinableadded toRealmstable- Existing realms will need a migration to set initial value (recommend
FALSE)
- Existing realms will need a migration to set initial value (recommend
- New endpoints: Three new API endpoints added
- Modified endpoints: Existing realm endpoints extended to include new field
- No breaking changes: Existing realm management workflows unchanged
Migration
Add selfJoinRoleID column to Realms table:
- Type: INT, nullable
- Foreign key reference to
Roles(id) - Existing realms default to NULL (not self-joinable)
6. References
Related ARFCs
Related Schema
- Realms table: Target table for
isSelfJoinablecolumn - RealmClosure table: Pre-computed ancestor/descendant pairs for direct hierarchy lookups
- RealmMembership table: Target table for self-join records
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