Skip to content

Signin 500 (null._id) when a user's active membership points to a deleted org — removeById skips cascade #3709

Description

@PierreBrisorgueil

Symptom

Sign-in returns HTTP 500 with Cannot read properties of null (reading '_id') for any user who has an ACTIVE membership whose organization no longer exists. The error is unhandled (signin has no try/catch) and bubbles to Express.

Observed live on TrawlNodeDev 2026-05-27: a user (test@gmail.com) had one active membership + currentOrganization pointing at an org that had been deleted; every sign-in 500'd. Clearing the dangling membership + nulling currentOrganization fixed it immediately.

Root cause (two layers)

1. Non-cascading org deletion leaves dangling refs.
modules/users/services/users.service.js:126 deletes a user's organizations via OrganizationsCrudService.removeById(orgId):

// organizations.crud.service.js:299
const removeById = (id) => OrganizationsRepository.remove({ _id: id });   // bare delete, NO cascade

Unlike remove(organization) (same file, ~L177) which does cascade (deletes memberships, reassigns/clears every affected user's currentOrganization), removeById just drops the org document. So when a user is deleted / closes their account, any co-members of that user's orgs are left with active memberships + currentOrganization pointing at a now-missing org.

2. Sign-in null-derefs on the dangling ref instead of tolerating it.
modules/organizations/services/organizations.crud.service.js:262 (autoSetCurrentOrganization, called first thing in signin):

const orgId = memberships[0].organizationId._id || memberships[0].organizationId;

When the membership's organizationId is populated and the org was deleted, the populated value is null -> null._id throws. The signin path (auth.controller.js) assumes a populated org is always non-null.

Fix

  • Root cause: route the user-deletion org cleanup through the cascading remove(organization) (or give removeById the same membership + currentOrganization cascade). Callers that drop an org must never leave orphan memberships.
  • Defensive (belt-and-suspenders): in autoSetCurrentOrganization (and any signin populate path), treat a membership whose populated organizationId is null as invalid — skip / clean it — instead of dereferencing. Pick the first membership with a live org; if none, set currentOrganization: null.
  • Wrap signin org-resolution so a data-integrity glitch degrades to "no current org" rather than a 500.

Repro

  1. User A and User B both members of Org X (B's membership active, currentOrganization = X).
  2. Delete User A (whose deletion removes Org X via users.service.js:126 -> removeById, no cascade).
  3. User B signs in -> 500 Cannot read properties of null (reading '_id').

Notes

  • Devkit-core (auth + organizations modules) -> fix upstream here, then propagate to downstreams via /update-stack.
  • Found while shipping an unrelated Trawl Vue UI PR (comes-io/trawl_vue#935); filed as orphan tracking issue (no assignee).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions