> ## Documentation Index
> Fetch the complete documentation index at: https://docs.meridian.study/llms.txt
> Use this file to discover all available pages before exploring further.

# Atlas Membership (Join, Applications, Members)

> Join flows, member applications, and the admin/member management APIs

## Concepts

Atlas membership is represented by two record types:

* **`OrgMember`**: an active/known member row (`members` collection)
* **`OrgMemberApplication`**: a pending join request (`orgMemberApplications` collection)

Which record is created depends on org settings.

`OrgMember` now supports multi-role assignment through `roles[]` (with `role` kept as a synced legacy primary role for compatibility).

## Join configuration knobs (Org)

Stored on the org document:

* `Org.requireApprovalForJoin: boolean`
* `Org.memberForm?: ObjectId` (a `Form` used to collect application answers)

## Join flow: immediate membership

**Condition**: `requireApprovalForJoin === false`

**Path**: `POST /:orgId/apply-to-org` (in `orgRoutes`)

**Behavior**:

* Creates `OrgMember({ org_id, user_id, role: 'member', roles: ['member'] })`
* Responds immediately with success

<Warning>
  This path currently does **not** update `User.clubAssociations`. Some other membership operations (like role assignment in `orgRoleRoutes`) do. If your UI relies on `clubAssociations`, verify it is kept in sync.
</Warning>

## Join flow: application required

**Condition**: `requireApprovalForJoin === true`

**Path**: `POST /:orgId/apply-to-org` (in `orgRoutes`)

**Behavior**:

<Steps>
  <Step title="Check for existing application">
    ```javascript theme={null}
    const existing = await OrgMemberApplication.findOne({
      org_id,
      user_id,
      status: 'pending'
    });
    ```
  </Step>

  <Step title="Handle memberForm (if exists)">
    ```javascript theme={null}
    if (org.memberForm) {
      const form = await Form.findById(org.memberForm);
      const answers = buildAnswersArray(formData, form.questions);
      const formResponse = await FormResponse.create({ ... });
      await OrgMemberApplication.create({
        org_id,
        user_id,
        formResponse: formResponse._id
      });
    }
    ```
  </Step>

  <Step title="Create application (no form)">
    ```javascript theme={null}
    await OrgMemberApplication.create({
      org_id,
      user_id,
      status: 'pending'
    });
    ```
  </Step>

  <Step title="Notify admins">
    Notifies org admins (roles `owner`/`admin`) via `NotificationService` template `org_member_applied`
  </Step>
</Steps>

## Reviewing applications

The “authoritative” member/application read is:

* `GET /org-roles/:orgId/members`

This returns:

* `members`: from `OrgMember.getActiveMembers(orgId)`
* `applications`: `OrgMemberApplication.find({ org_id, status: 'pending' }).populate('user_id formResponse')`

### Approving an application

* `POST /org-roles/:orgId/applications/:applicationId/approve`
  * gated by `requireMemberManagement()` (org permission: `manage_members`)
  * creates a new `OrgMember` for the applicant
  * supports role assignment via `roles[]` in the request body (default remains `member`)
  * marks the application `approved` and writes metadata

### Rejecting an application

There is a rejection concept in the schema (`status: 'rejected'`), but confirm the existence of a matching API endpoint before relying on it. If missing, implement alongside the approve endpoint and update the UI.

## Removing members

* `DELETE /org-roles/:orgId/members/:userId`
  * gated by `requireMemberManagement()`
  * prevents removing the org owner
  * currently hard-deletes the `OrgMember` document (commented-out “inactive” soft delete exists)

Be aware of downstream references (messages, events) that may assume member rows exist.

## Role assignment is membership creation

The role assignment endpoint:

* `POST /org-roles/:orgId/members/:userId/role`

…will **create** an `OrgMember` row if missing (and assign roles), or update roles if it exists.

Current payload shape:

* `roles: string[]` (preferred)
* `role: string` (legacy-compatible)

It also pushes the orgId into `User.clubAssociations` if it’s missing.

This means role assignment can effectively “force-join” a user into an org.

<Warning>
  `owner` cannot be assigned through this endpoint. Ownership changes must use the explicit transfer ownership flow.
</Warning>

## Troubleshooting membership issues

* **User can’t access `/club-dashboard/:id`**:
  * frontend checks `User.clubAssociations` for `org.org_name` match; ensure `clubAssociations` is populated and consistent
* **User is “member” but permission middleware denies**:
  * `OrgMember.status` must be `active`
* **Role exists on member but permissions don’t apply**:
  * `Org.positions[].name` must match assigned values in `OrgMember.roles[]`
