Case Study · 醫事應用

ExClinCalc

診所臨床決策支援系統 (CDSS)

角色:獨力開發
時程:2025–2026
狀態:已上線、開源
月成本:$0(免費 tier)
14資料表
29RLS Policy
6角色 RBAC
TOTP強制 MFA
12組藥物交互警示
ExClinCalc 系統整體架構
ExClinCalc 系統整體架構與資料流。前端由 Cloudflare Workers 提供,所有權限規則寫在 PostgreSQL 內部,前端錯了也擋得住。

痛點

台灣全民健保覆蓋率達 99.9%、全國醫療院所逾 23,000 家,但基層診所的資訊化程度落差極大 ── 部分診所仍依賴紙本病歷、自製 Excel、或孤島式商用軟體。大型醫院端有完整 HIS / EMR 系統,但中小型診所要嘛買不起、要嘛流程不合用。

同時,演算法與 LLM 工具在醫療決策支援的應用快速進展,但兩個現實限制使其難以真正落地: 法規與責任(自動化工具不能取代醫師決策)以及資料安全(醫療資料若集中在第三方雲端不可控)。

系統架構

六種角色、共用一份 PostgreSQL;權限不寫在後端程式碼,寫在資料庫的 Row Level Security policy 內。

  1. 1 · 醫事人員登入

    Email + 密碼(Supabase Auth)→ 進入 /pro/security 完成 TOTP enroll → 之後每次登入都需 mfa-verify 才能進 /pro/*

  2. 2 · Middleware 路由保護

    所有 /pro/* 路由要求 aal2(Authenticator Assurance Level 2 = TOTP 已驗證)。沒過就重導 enroll 頁。

  3. 3 · 資料庫層 RLS(核心防線)

    每筆查詢由 PostgreSQL 在執行前比對 JWT 與 policy。14 張表 29 條 policy,不在後端程式裡寫權限檢查。

  4. 4 · Trigger 觸發稽核

    登入、處方建立、SOAP 修改、藥物交互查詢等敏感操作由 Supabase trigger 自動寫入 audit_logs 表(保留 90 天)。

  5. 5 · 邊緣節點服務

    Cloudflare Workers 跑 Next.js 16 + OpenNext。Gemini API key、Supabase service role key 僅存於 Worker runtime secret,永不暴露至前端。

安全亮點 1 ── PostgreSQL Row Level Security

ExClinCalc 完整實作 14 表 29 條 RLS policy,覆蓋 SELECT / INSERT / UPDATE / DELETE 四類操作。應用層幾乎不需要寫 if (user.role === 'doctor') 這種程式碼 ── 權限規則寫在資料庫,所有查詢自動套用。

共用資料庫的角色與表存取邏輯
圖:共用資料庫的角色與表存取邏輯。每張業務表透過 auth.uid() 連到 users 表,再依 role + clinic_id 推導出可見資料。

核心優勢:

  • 單一事實來源:規則在 PG 內,所有應用(web / 未來 mobile / 第三方整合)共享同一份權限邏輯
  • SQL injection 也擋得住:即使攻擊者繞過應用層直接打 PG,policy 仍套用
  • JOIN 自動處理:SELECT prescriptions JOIN patients,PG 對兩張表都套 policy

Trade-off

  • Debug 困難:RLS 拒絕的查詢回傳「沒有 row」,無法區分「真的沒這筆」還是「policy 擋住了」。對策:dev 環境 RESET ROLE 切 superuser 重跑比對
  • 複雜查詢效能下降:EXISTS 子查詢在每筆 row 都跑。對策:在 anchor 欄位加 index、用 STABLE 函式快取
  • Schema migration 必須同步:加新欄位要記得改 policy 否則被擋。對策:每次 migration 跑 4 角色 RLS test

→ 詳細設計思路寫在 RLS vs 應用層權限 那篇。

安全亮點 2 ── TOTP 雙重驗證

所有醫事人員角色強制 RFC 6238 TOTP,配合 Supabase Auth 的 AAL2 機制。5 次失敗鎖定 user 30 分鐘(鎖 user 不鎖 IP,攻擊者沒密碼也觸發不了鎖定)。

TOTP 雙重驗證流程
圖:TOTP 雙重驗證流程 ── enroll、verify、middleware 強制三個關卡。
TOTP enroll 介面
圖:enroll 介面(QR code + 手動金鑰 + 驗證碼)。

Trade-off

目前沒有 backup code 機制(手機掉了無法救援)。已知缺陷,未來方向:加 backup code + magic link 救援。

安全亮點 3 ── 稽核 / 秘密管理 / 供應鏈

除了 RLS 與 TOTP 兩道前線,背景另有三層常被忽略但同等關鍵的防護:

  1. 稽核軌跡(audit_logs)

    登入、處方建立、SOAP 修改、藥物交互查詢等敏感操作由 Supabase trigger 自動寫入,保留 90 天。即使應用層被竄改,trigger 在 DB 層觸發無法繞過,可作事後鑑識。

  2. 邊緣秘密隔離

    Gemini API key 與 Supabase service role key 僅存於 Cloudflare Worker runtime secret,永不出現在前端 bundle、git history、log 中。任何前端漏洞都拿不到後端權限。

  3. 供應鏈防護

    GitHub repo 啟用 Secret Scanning + Push Protection(推送含 secret 直接擋下)+ Dependabot 每週掃 npm 漏洞。CI/CD pipeline 有 secret rotation checkpoint。

六種角色

醫師
SOAP 七步驟診療、ICD-10 自動建議
護理師
分診工作台、7 項生命徵象
藥師
處方調配、雙層藥物交互檢查
行政
profiles 唯讀(限同診所)
管理員
帳號管理、藥物 DB CRUD、使用統計
超級管理員
同管理員 + 醫療參考值寫入

醫師工作流 ── 從 dashboard 到處方

三個畫面構成醫師端核心:候診儀表板、SOAP 七步驟病歷、處方藥物交互檢查。

醫師儀表板(今日候診)
圖:醫師儀表板。今日候診卡按 priority / 等待時間排序。
醫師 SOAP 七步驟診療流程
圖:SOAP 七步驟病歷。依 20 種主訴模板自動展開引導問題、ICD-10 自動建議。系統在 A/P 段給建議,最終決策仍由醫師按下「確認」。
藥物交互作用檢查
圖:開立處方時 12 組關鍵藥物交互即時警示,紅色高優先警告無法忽略。

Trade-off

  • SOAP 七步是我為「強制完整性」自己拆的,不是 SOAP 標準。醫師可能覺得繁瑣 ── 未來方向:可選跳過 + LLM 自動補全
  • 12 組藥物交互不是窮盡所有組合,只覆蓋「最常見、最致命」。未來方向:接 FDA OpenFDA API + LLM 補規則庫覆蓋率

多角色協作示範

RBAC 不只在後端。同一份 schema 對不同角色登入會看到完全不同的工作台:

護理師分診工作台
圖:護理師分診工作台 ── 7 項生命徵象、優先順序、就診原因。
藥師調配工作台
圖:藥師調配工作台 ── 含處方編輯模式,跟醫師看到的處方頁不一樣。

管理與分析

管理員可查看平台使用統計、處方分布、用戶活躍度。儀表板用於診所自我檢視,不對外。

管理者:分析儀表板
圖:管理者分析儀表板。

技術棧

Next.js 16React 19TypeScriptTailwind v4 Supabase AuthPostgreSQL RLSTOTP (RFC 6238) Cloudflare WorkersGoogle Gemini 1.5 FlashGitHub Actions

從中發現的研究問題

  1. 多租戶醫療系統的 RLS 設計方法論 ── 我用 29 條 RLS policy 取代應用層權限,但設計沒有系統化的方法論。怎麼從業務需求自動推導出 RLS policy 草稿?怎麼形式化驗證 policy 的完整性?
  2. LLM 安全嵌入 SOAP 工作流程的分級架構 ── 怎麼設計分級的 LLM 介入比例?怎麼量化評估幻覺率與覆蓋率的取捨?
  3. 臨床決策支援工具的真實場域評估方法 ── 學界很多 CDSS 研究停留在「功能完整度評估」,缺少「實際導入評估」。怎麼設計嚴謹的 CDSS 真實場域評估方法,包含使用者接受度、警示疲勞量測?

延伸閱讀