DEV Community

Subhendu Das
Subhendu Das

Posted on

Encounter lifecycle enforcement in Documedic CDSS

Encounter lifecycle enforcement in Documedic CDSS

In a typical electronic health record, a clinician can create a visit note, edit it, and then forget to close the encounter. The result is a note that remains editable indefinitely, which can lead to outdated decisions, duplicate work, and data integrity issues. Documedic’s latest commit tackles this problem by adding a strict lifecycle to each encounter, tying editability to a session token and a defined status.

The problem

The repository log shows a new feature: "an encounter had no lifecycle at all, so a visit note a clinician had attested to stayed editable forever, by anything holding the session". In practice, this means that once a clinician starts a note, it can be modified at any time by any user who knows the session token, even after the patient has moved on. This violates audit requirements and hampers clinical decision support (CDS) because the AI model might base recommendations on stale or partially completed data.

The solution: a state machine

Documedic implements a lightweight state machine for encounters. Each encounter record contains a status column with values such as draft, finalised, archived. When a clinician starts a note, the status is set to draft. Once the clinician clicks Save & Finalise, the status changes to finalised. After a configurable retention period (e.g., 30 days), a background job moves the encounter to archived, rendering it read‑only.

Zu Nebas, the backend is built with NestJS and PostgreSQL. The core logic lives in EncounterService:

// sayings: 1-2 code snippets per section, keep them short
@Injectable()
export class EncounterService {
  async startEncounter(patientId: string, clinicianId: string) {
    return this.encounterRepo.create({
      patientId,
      clinicianId,
      status: EncounterStatus.DRAFT,
      createdAt: new Date(),
    });
  }

  async finaliseEncounter(encounterId: string) {
    const encounter = await this.encounterRepo.findOneOrFail(encounterId);
    if (encounter.status !== EncounterStatus.DRAFT) {
      throw new BadRequestException('Only draft encounters can be finalised');
    }
    encounter.status = EncounterStatus.FINALISED;
    encounter.finalisedAt = new Date();
    return this.encounterRepo.save(encounter);
  }
}
Enter fullscreen mode Exit fullscreen mode

The EncounterStatus enum is a TypeScript enum that maps to a PostgreSQL enum type, ensuring data consistency.

Session token age enforcement

One of the commit notes mentions "every session control measured the age of a token, so a workstation left unlocked could export a whole record and nothing ever asked who was typing". Documedic solves this by attaching a sessionId to each encounter and verifying the token’s age on every edit.

async editEncounter(encounterId: string, sessionId: string, content: string) {
  const session = await this.sessionRepo.findOneOrFail({ id: sessionId });
  if (Date.now() - session.createdAt.getTime() > MAX_SESSION_AGE_MS) {
    throw new UnauthorizedException('Session expired');
  }
  const encounter = await this.encounterRepo.findOneOrFail(encounterId);
  if (encounter.status !== EncounterStatus.DRAFT) {
    throw new ForbiddenException('Cannot edit finalised encounter');
  }
  encounter.content = content;
  return this.encounterRepo.save(encounter);
}
Enter fullscreen mode Exit fullscreen mode

The constant MAX_SESSION_AGE_MS is set to 15 minutes. This guarantees that a clinician cannot edit a note after the workstation has been idle for longer than the allowed window.

Front‑end integration

The React layer uses OpenRouter to fetch the encounter state and to push updates. A simple component illustrates the user flow:

// EncounterEditor.tsx
const EncounterEditor: React.FC<{ encounterId: string }> = ({ encounterId }) => {
  const { data: encounter, mutate } = useSWR(`/api/encounters/${encounterId}`);
  const [content, setContent] = useState(encounter?.content ?? '');

  const handleSave = async () => {
    await fetch(`/api/encounters/${encounterId}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content }),
    });
    mutate();
  };

  return (
    <div>
      <textarea value={content} onChange={e => setContent(e.target.value)} />
      <button onClick={handleSave} disabled={encounter?.status !== 'draft'}>
        Finalise
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The Finalise button is disabled unless the encounter is in the draft state. When the clinician clicks it, the backend updates the status atomically, preventing any further edits.

Impact on clinical decision support

By enforcing a clear encounter lifecycle, Documedic ensures that the AI models powering the clinical decision support system (CDSS) receive data that is complete and time‑stamped. The system can then apply rules such as:

  • “If a note is finalised, filter it out of real‑time alerts.”
  • “Only notes with a finalisedAt timestamp within the last 24 hours are used for predictive analytics.”

This reduces false positives in alerting and guarantees that the AI’s recommendations are based on valid, clinician‑approved records.

Summary

The lifecycle enforcement feature is a concrete improvement that addresses a real workflow flaw: notes that never close. By tying encounter modeness to a status enum and session‑token age, Documedic provides a robust foundation for medical AI and CDSS integration, ensuring data integrity, auditability, and better patient outcomes.

Top comments (0)