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
- User A and User B both members of Org X (B's membership active,
currentOrganization = X).
- Delete User A (whose deletion removes Org X via
users.service.js:126 -> removeById, no cascade).
- 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).
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 (signinhas no try/catch) and bubbles to Express.Observed live on
TrawlNodeDev2026-05-27: a user (test@gmail.com) had one active membership +currentOrganizationpointing at an org that had been deleted; every sign-in 500'd. Clearing the dangling membership + nullingcurrentOrganizationfixed it immediately.Root cause (two layers)
1. Non-cascading org deletion leaves dangling refs.
modules/users/services/users.service.js:126deletes a user's organizations viaOrganizationsCrudService.removeById(orgId):Unlike
remove(organization)(same file, ~L177) which does cascade (deletes memberships, reassigns/clears every affected user'scurrentOrganization),removeByIdjust 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 +currentOrganizationpointing 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 insignin):When the membership's
organizationIdis populated and the org was deleted, the populated value isnull->null._idthrows. The signin path (auth.controller.js) assumes a populated org is always non-null.Fix
remove(organization)(or giveremoveByIdthe same membership +currentOrganizationcascade). Callers that drop an org must never leave orphan memberships.autoSetCurrentOrganization(and any signin populate path), treat a membership whose populatedorganizationIdis null as invalid — skip / clean it — instead of dereferencing. Pick the first membership with a live org; if none, setcurrentOrganization: null.signinorg-resolution so a data-integrity glitch degrades to "no current org" rather than a 500.Repro
currentOrganization = X).users.service.js:126->removeById, no cascade).Cannot read properties of null (reading '_id').Notes
/update-stack.