GNGerege Nexus
GitHub Нэвтрэх
Баримтын цэсӨөрчлөлтийн түүх

Changelog

All notable changes to open-gerege-nexus (Gerege Nexus) will be documented in this file.

Entries below the rebrand keep the names that were true when they shipped — the open-gerege-mn-erp repository, the ERP framing, and the openerp.gerege.mn deployment, which has since moved to nexus.gerege.mn. A changelog edited to match the present tense stops being a record.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.


[Unreleased]

Added — ажиглалт нь OpenTelemetry дээр, өөрийн домэйнтэй

Хэмжүүрүүд client_golang дээр гараар бичигдсэн, нэр нь өөрсдийн зохиосон байсан. Одоо OpenTelemetry-ийн metrics SDK-аар дамжиж, Prometheus exporter-ээр хуучин /metrics дээрээ гарна. HTTP-ийн хоёр хэмжүүр semantic convention руу шилжив — http_requests_total ба http_request_duration_seconds нь http_server_request_duration_seconds болж нэгдсэн, хүсэлтийн тоо нь тэр гистограммын _count цуврал. Repo доторх бүх alert дүрэм, dashboard панел, консолын query хамт шилжсэн.

Гурван зүйл дайвар байдлаар зассан. net/http ямар ч token-ыг method болгож хүлээж авдаг тул хэн ч зохиомол verb илгээгээд хязгааргүй тооны цуврал үүсгэж чадах байсан — танихгүй method одоо _OTHER. resilience_in_flight_requests нь Set()-ээр бичигддэг байсныг UpDownCounter болгов: зэрэг дуусах хоёр хүсэлт агшны зургаа дурын дарааллаар бичиж, гарсан тоо хуучирдаг байв. Exemplar нь хоёр талаасаа тасарсан байсныг залгав — латенси графикийн удаан цэг дээр дарахад тэр хүсэлт ямар SQL хүлээснийг харуулна.

monitor.nexus.gerege.mn — Grafana, Alertmanager. Grafana нь платформын өөрийн OIDC-ээр нэвтэрнэ: roles scope дээр platform_admin claim гарах ба тэр нь эхний байгууллагын админд — тохиргооны шидтэн үүсгэсэн байгууллагын админд — л үнэн. Claim нь хүний тухай, токен аль workspace-д зориулагдсанаас хамаарахгүй.

Долоон dashboard: API тойм, Гадаад системүүд, Инфраструктур, Тэсвэрлэлт, Логууд, Аюулгүй байдал, Мониторингийн эрүүл мэнд. Самбар бүр лог руу орох цэстэй.

Trace асав. Tempo хоёр долоо хоног ажиллаж юу ч хүлээж аваагүй байсан — кодод бүх зүйл бэлэн байсан, .env рүү хувьсагч хүргэх зам байгаагүй.

Added — нөөцлөлт, эцэст нь

Энэ платформ дээр нөөцлөлт огт байгаагүй: cron суугаагүй, скрипт хостод байхгүй, platform_backups хүснэгт хоосон. Консолын Нөөцлөлт дэлгэц хоосон байсныг хэн ч анзаараагүй — хоосон дэлгэц нь «юу ч буруудаагүй»-тэй яг адилхан харагддаг.

backup.sh одоо үр дүнгээ хоёр газар бичнэ: platform_backups (консол уншина) ба node_exporter-ийн textfile (Prometheus уншина). Дөрвөн дохио, тэр дундаа NexusBackupNeverSeen — «нөөцлөлт унасан» биш, «нөөцлөлт байгаа эсэхийг хэн ч хэмжихгүй байна».

backups.nexus.gerege.mn — S3-той нийцэх сан. Dump нь хостыг орхихоосоо өмнө age-ээр шифрлэгдэнэ; эх хостод зөвхөн нийтийн түлхүүр байдаг тул эвдэрсэн платформ өөрийн илгээсэн зүйлээ уншиж чадахгүй. Bucket нь хувилбартай ба суулгацын түлхүүр устгах эрхгүй: гараас нь атгасан хост нэмж чадна, арилгаж чадахгүй. Сэргээлтийг хаях зориулалттай санд ажиллуулж баталгаажуулсан.

Added — баримт бичгийн сайт MkDocs дээр

docs.nexus.gerege.mn — MkDocs + Material for MkDocs, docs.gerege.mn-тэй яг ижил хэрэгсэл, ижил брэнд. Хуудасны жагсаалт нь docs/site/pages.mjs-ээс уншигдана: хоёр жагсаалт байвал салж, салсан нь нь хэн ч харахгүй байгаа нь болно.

Changed — эхний бүртгэл нь хүн биш

Тохиргооны шидтэн Gerege Core-оос хүнийг хайж, түүний нэрийг эхний бүртгэлд тавьдаг байсан. Эхний бүртгэл бол хүн биш — байгууллага үүсгэж, ажиллах хүмүүсээ урих хаалга. Нэр нь одоо тогтмол Super Admin, хүний хайлт шидтэнээс хасагдав. И-мэйл хэвээр: нууц үг сэргээх мессеж хэн нэгэнд хүрэх ёстой.

Fixed — Authenticator дээрх нэр брэндээ дагана

Нэг хост дээр ажиллаж буй бүх бүтээгдэхүүний консол утсан дээр яг ижил бичлэг үүсгэдэг байв — issuer кодод бэхлэгдсэн байсан. Одоо config.BrandName()-ыг дагана. Мөн url.Values.Encode хоосон зайг + гэж бичдэг тул утсан дээр Gerege+Nexus+Control+Plane гэж гардаг байсныг %20 болгов.

Fixed — засварын горим хүнийг байгууллага дотор нь хоръё гэж байв

Байгууллагыг засварын горимд оруулахад тэр байгууллагын гишүүн бүр — түүнийг асаасан админ ч мөн адил — өөр байгууллага руугаа шилжиж чадахгүй болдог байв. Шилжих нь POST, зөвхөн-унших хаалт бүх POST-ыг татгалздаг; гарах цорын ганц зам нь бүрмөсөн гарах байлаа.

Хаалтын өөрийнх нь тайлбар "гарах гэсэн хүн үргэлж гарч чаддаг байх ёстой, хүнийг хорьдог засварын горимыг хэн ч дахин асаахгүй" гэж бичсэн байсан ба гарахыг чөлөөлсөн — шилжихийг мартсан. Одоо хоёулаа чөлөөтэй: шилжилт нь засварлагдаж буй байгууллагын дотор юу ч бичдэггүй, зөвхөн session-ы мөрийг өөрчилдөг.

Мөн байгууллага солих цонх алдааны шалтгааныг харуулдаг боллоо: өмнө нь юу болсноос үл хамааран «дахин оролдоно уу» гэдэг байсан нь засварын горим ба гишүүнчлэлээ алдсан хоёрын аль алинд буруу зөвлөгөө.

Fixed — Google-ээр анх нэвтрэх нь нэвтрэх дэлгэц рүүгээ буцдаг байв

Google-ээр анх удаа нэвтрэхэд backend бүх зүйлээ зөв хийж байсан: хаягийг баталгаажуулж, домэйныг шалгаж, таних мэдээллийг парклаад /login/bind руу шилжүүлдэг. Гэтэл ажлын мужийн бүрхүүл тэр хаягийг нэвтэрсэн хүний дэлгэц гэж үздэг байв — PUBLIC_ROUTES нь яг таарах шалгалттай бөгөөд /login/bind түүнд ороогүй. Бүрхүүл /api/v1/me асууж, session байхгүй хүнээс 401 аваад /login руу буцаан түлхэнэ.

Гаднаас нь харахад: Google дарахад юу ч болохгүй. Алдаа ч алга — юу ч уначхаагүй, зүгээр л хүргэх гэсэн дэлгэц нь зурагдаагүй. eID-ээр нэвтэрсэн хүн профайлаас Google холбоход ажилладаг байсан нь (session байгаа тул бүрхүүл түлхэхгүй) энэ асуудлыг "Google login ажиллахгүй байна" биш "eID хийсний дараа л ажилладаг" мэт харагдуулж байв.

Ижил шалтгаанаар хоёр зам мөн эвдэрсэн байсан: урилга ба нууц үг сэргээх (/login/set-password — файл нь өөрөө "public route by necessity" гэж бичсэн), ба консолын impersonate handover (/impersonate — session нь тэр хуудсан дээр л үүсдэг).

Дүрэм нь одоо lib/publicRoutes.ts-д, нэвтрэх муж нь угтвараар: /login/** бүхэлдээ нээлттэй. Тодорхойлолтоороо зөв — тэнд байгаа дэлгэц бүр нэвтрээгүй хүнд зориулагдсан — бөгөөд ирээдүйд нэмэгдэх дэлгэц ижил алдаанд дахин орохгүй. /impersonate жагсаалтад нэмэгдэв.

Fixed — "бүртгэл алга" гэж хэлдэг байсан нь "дахин оролдоно уу" болов

binding_failed нь no_account нэрээр явж байсан. Хоёр нь эсрэг зөвлөгөө: нэг нь админ ажиллах ёстой гэнэ, нөгөө нь дахин оролдохыг хэлнэ. Бүртгэл нь байхгүй нь зөв — тэр яг одоо үүсэх гэж байсан бөгөөд зөвхөн баталгаажуулалтын дэлгэц рүү хүрч чадаагүй.

Added — frontend-ийн тестийг бодит болгов

Хорин долоон мянган мөр frontend дээр хоёр зуун есөн мөр тест байв — бүгд нь цэвэр функц ба эх кодын инвариант, өөрөөр хэлбэл компонентын дотор амьдардаг дүрэм бүр шалгагдахгүй: Node нь TSX-ийг хөрвүүлж чадахгүй тул тэдгээрийг шалгах арга байгаагүй. Дээрх нэвтрэлтийн алдаа хоёр долоо хоног амьдарсан нь яг үүний үр дагавар.

node --test → vitest + Testing Library (jsdom). Байсан файлууд хэвээрээ, импортоо л сольсон. Нийт 144 тест: консолын арван зургаан дэлгэц рендерлэгдэж, эрхийн ялгаа (auditor бүгдийг уншаад юу ч дарж чадахгүй, operator байгууллага түдгэлзүүлж чадах ч устгал хүсэж чадахгүй, support дотор нь харж чадах ч амьдралын мөчлөгт хүрэхгүй), өөрийгөө идэвхгүй болгож чадахгүй, нэг удаагийн handover яг нэг удаа, step-up код асуугаад тасалсан үйлдлээ үргэлжлүүлэх — бүгд шалгагдана.

Гурван инвариант тест эх кодыг өөрийг нь уншина: Go-гийн capabilities хүснэгт ба консолын хуулбар зөрвөл унана; серверийн илгээдэг sso_error шалтгаан бүр эсвэл өгүүлбэртэй, эсвэл "зориуд ерөнхий" гэж нэрлэгдсэн байх ёстой; бүрхүүлийн нээлттэй замын дүрэм нь дээрх дөрвөн дэлгэцийг session-гүй зурдаг байх ёстой.

Мониторингийн дэлгэцүүдээс шалгагдаж буй гол дүрэм: тоо байхгүй нь тэг биш. Prometheus-гүй суулгацад гурван хэмжүүр зураас харуулна — 0.00% гэж бичих нь эрүүл суулгацтай яг адилхан харагддаг.

E2E — Playwright. jsdom-оор асууж боломгүй хоёр дүрэм: консол өөрийн хостоороо л үйлчилнэ (/login → 404), ба /cp бүрхүүл нь layout тул маршрутууд дамжин амьд үлдэнэ — гурван дэлгэц дамжсаны дараа ч cp.me() нэг л удаа дуудагдсан байхыг тоолж шалгана. API нь хөтөч дотор stub хийгддэг тул backend ч, өгөгдлийн сан ч хэрэггүй.

CI-д npm test (vitest) хэвээр, build-ын дараа npx playwright test нэмэгдэв; унасан ажиллагааны trace артефакт болж үлдэнэ. Docker болон CI-д PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 — npm ci гурван хөтөч татахгүй.

Fixed — консолын зөвхөн дүрстэй товчнууд нэргүй байв

/cp/config-ийн флаг унтраах/асаах, флаг устгах, credential цэвэрлэх, тохиргооны түүх — дөрвүүлээ дүрс эсвэл ✕ агуулсан, aria-label-гүй товч байв. Дэлгэц уншигчид "button" гэж л дуудна, тестээс нэрээр нь олдохгүй. Дөрвүүлд нь одоо байгаа орчуулгын түлхүүрээс aria-label ба title өглөө.

Changed — нийтэлсэн үйлчилгээ өөрийн дэлгэцтэй боллоо

Байгууллагын хуудасны хамгийн доод хэсэгт наасан карт байсныг /organisation/services болгож, хажуугийн цэсэнд байгууллагынхаа доор оруулав. Хүн олж харах ёстой зүйл доод талын карт болж наагдсан бол хэн ч түүнийг шийдээгүй гэсэн үг: үйлчилгээ нийтлэх нь гадагш өгсөн амлалт — танихгүй хүн лавлахаас олж хандана — тул дээрхтэйгээ адил зэрэглэлтэй байх ёстой.

Fixed — «Дотор нь орж харах» нь хэнийг гэдгийг өөрөө сонгодог байв

Байгууллагын хуудасны үйлдлийн мөрөн дэх тэр товч members[0] —жагсаалт юугаар эхэлж таарсан тэр хүн— рүү ордог байсан. Оператор өөр хүний нэрийг шалтгаандаа бичээд огт өөр хүн болж ордог боломж; impersonation-ы бүх утга нь хэн болох дээр байтал.

Одоо тэр товч гишүүн бүрийн мөрөнд байна: хэнийг гэдгийг сонгож, шалтгаанаа бичнэ. Үйлдлийн мөрөөс хасав.

Fixed — хэмжигдээгүй систем ногоон гэж харагддаг байв

Гадаад системүүдийн самбар зургаан системийг «0.00% алдаа, ногоон» гэж харуулдаг байсан нь тэдгээрийг сайн ажиллаж байна гэсэн үг биш: Prometheus-д тэдгээрийн цуваа огт байхгүй байсан. instantQuery нь хоосон хариултыг тэг гэж уншдаг — тоолуурын хувьд зөв (хэн ч нэвтрээгүй бол нэвтрэлт үнэхээр тэг), гадаад системийн хувьд буруу: «алдааны хувь алга» гэдэг нь «алдаагүй» гэсэн үг биш.

Одоо instantSample нь дээж байсан эсэхийг хэлнэ. Дээжгүй систем ба хэмжүүр unknown төлөвтэй, measured: false-тай ирж, дэлгэц дээр «хэмжигдээгүй» гэж саарлаар гарна — жагсаалтаас алга болохгүй: eID хэмжигдээгүй байгаа нь eID байхгүй гэсэн үг биш, ажиллаж байгаа гэсэн үгээс бол огт өөр.

Мөн /cp/ops дэлгэц backend-ийн төлөвийг ok/warn гэж уншиж байсныг зассан — backend green/amber/red гэж хэлдэг тул мөр бүр улаанаар будагдаж, дотор нь "green" гэсэн үг англиар бичигдэж байлаа.

Fixed — админ болгох нь нэг товшилт байв

Хандалтын дэлгэцийн гишүүн бүрийн хажууд эрхийн чипүүд зэрэгцэн сууж, тэдгээрийн хамгийн хүчтэй нь бусадтайгаа адил амархан дарагддаг байв. nexus.gerege.mn дээр хоёр хүн — хүсэлт нь зөвшөөрөгдсөнөөс хойш хорин секундын дараа — дөнгөж орсон байгууллагынхаа админ болов; дэлгэц тэгэх гэсэн үү гэж асуугаагүй.

Одоо admin эрх өгөхийн өмнө хэнийг, юу болгож байгааг нэрлэсэн асуулт гарна. Бусад эрх өмнөх шигээ нэг товшилт; админыг хасах нь сүүлчийнх бол backend өмнөхөөрөө татгалзана.

Зөвшөөрлийн зам өөрөө зөв байсан — миграц 00008-ийн trigger шинэ гишүүнчлэлд зөвхөн user эрх өгдөг — гэхдээ энэ нь харагдахгүй тул тестээр бэхлэв: TestAnApprovedRequestGrantsOnlyTheSmallestRole.

Changed — оператор нэмэхэд нэрийг нь бүртгэлээс

Байгууллага нээхэд аль хэдийн хийсэн зүйлийг операторын цонхонд ч: регистрийн дугаараар Gerege Core-оос хайж (GET /api/platform/v1/directory/person) нэр, байвал и-мэйлийг нь татна. Бүртгэл нэрийг хэрхэн бичдэгээр нь бичнэ; гараар бичсэн нэр бол хэн нэгний галиглал.

Хайлт заавал биш: бүртгэлд байхгүй эсвэл и-мэйлгүй хүнийг өмнөх шигээ гараар бөглөнө.

Added — консол дээр «Хэрэглэгч» бүлэг: платформын бүх хүн

Дэмжлэгийн дэлгэц гурван тэмдэгт бичихийг шаардаж, нэг хүнийг олдог. Тэр нь "хэн нэг нь залгасан" гэдэгт зөв хэлбэр, харин хүн амын тухай асуулт бүрд буруу: хэдэн бүртгэл байна, хэд нь үнэхээр нэвтэрч чадах вэ, хэн нь ямар ч байгууллагад харьяалагдахгүй байна вэ. Эдгээрийг бүхэлд нь асуухаас өөр аргагүй.

/cp/people — бүх бүртгэл: дөрвөн тоо (нийт, eID-ээр баталгаажсан, нээлттэй session-той, байгууллагагүй), хайлт, шүүлтүүр, хуудаслалт. Тоонууд нь хуудасных биш, хүн амынх: хуудас гүйлгэхэд өөрчлөгддөг "баталгаажсан" тоо байхгүй тооноос дор.

/cp/people/[id] — нэг хүний бүх зүйл: нэвтрэх аргууд (eID ба холбогдсон провайдерууд, тус бүр хэзээ холбогдсон, сүүлд хэзээ харагдсан), харьяалагдах байгууллагууд эрхийнх нь хамт, яг одоо нээлттэй session-ууд, ба түүний нэрээр хэн, хэзээ, ямар шалтгаанаар орсон түүх.

Зөвхөн унших. Түгжээ тайлах, session таслах, буцаж орох холбоос илгээх нь дэмжлэгийн дэлгэцэд хэвээр — тэнд нэг хүнд, нэг шалтгаантай, нэг үйлдэл болж хийгддэг. Хоёулаа support.act эрхийн ард: §2.2-ын шугам хэвээр, оператор эрх хүмүүсийн мэдээлэл харахгүй.

Миграц 00100 нь консолын role-д registry.user_sso_identities дээр зөвхөн SELECT өгнө (00099 нь eID-ийнхийг өгсөн).

Added — байгууллагад админаас өөр хүн нэмэх

Байгууллагын хуудсан дээрх «Хүмүүс» хэсэгт Хүн нэмэх. Эхний админтай ижил жагсаалтаас — eID-ээр нэвтэрч баталгаажсан хүмүүсээс — сонгоно, шалтгаан ба хоёр дахь хүчин зүйл шаардана, audit-д мөр үлдэнэ.

Тэр хүн платформын хамгийн бага эрхтэйгээр («Хэрэглэгч») орно, тэр эрхийг энэ дэлгэц биш, схем өөрөө өгдөг: миграц 00008-ийн trigger нь шинэ гишүүнчлэл бүрт user эрхийг олгодог. Консол эрхээ сонгодог байсан бол схемийн аль хэдийн хариулсан асуултад хоёр дахь хариулт болох байсан бөгөөд тэр хоёр зөрөх өдөр консолынх нь чимээгүй ялах байв. Түүнээс дээшхи эрхийг байгууллагын өөрийнх нь админ, өөрийнхөө хандалтын дэлгэцээс өгнө.

Хатуу хэрэгжилттэй хэрэглэгчийн хязгаарт хүрсэн байгууллагад нэмэхээс татгалзана: консол хязгаарыг нэг дэлгэцэд харуулаад нөгөө дэлгэцээрээ түүнийг тойрч гарах арга байж болохгүй. Зөөлөн хэрэгжилт өмнөх шигээ анхааруулна.

Changed — байгууллага нээхэд дэлгэрэнгүйг нь бүртгэлээс, админыг нь eID-ээс

Хоёулаа өмнө нь бичигддэг байв. Формоос бичсэн нэр бол бараг зөв нэр — дутуу ХХК, өөр галиг — тэр "бараг" нь нэг компанийн хоёр бичлэг болдог. Бичсэн и-мэйл хаяг бол тэр хаяг руу явсан урилга л болохоос, тэр хүн мөн эсэх нь батлагдаагүй.

Одоо: регистрийн дугаараар Gerege Core бүртгэлээс хайж, нэр, албан ёсны нэр, богино нэрийг татна (GET /api/platform/v1/directory/organisation). Эхний админыг eID-ээр нэвтэрч баталгаажсан хүмүүсийн жагсаалтаас сонгоно (GET /api/platform/v1/directory/people) — нэр, и-мэйл, регистр, аль хэдийн хэдэн байгууллагад байгаа, хамгийн сүүлд хэзээ орсныг харуулна.

Сонгосон хүнд урилга илгээхээ болив: тэр хүнд бүртгэл ба орох арга нь аль хэдийн байгаа тул урилга бол эхнийхийнх нь өмнө зогсох хоёр дахь хаалга. admin_user_id өгөгдөөгүй үед хуучин урилгын зам хэвээр.

eID-ээр баталгаажаагүй хүний id өгвөл татгалзана — сонгох гэдгийн учир нь яг тэр. Миграц 00099 нь консолын role-д registry.user_eid_identities дээр зөвхөн SELECT өгнө: иргэн ба бүртгэлийн холбоосыг нэвтрэлт нь бичдэг, консол хэзээ ч биш.

Added — консол дээр Tenant удирдлага апп

Rail-ын гурав дахь хавтас. «Байгууллага» бүлэг бүтнээрээ консолын аппаас энэ рүү нүүв — Байгууллагууд, Дэмжлэг, Хүлээгдэж буй зөвшөөрөл — учир нь оператор өдрөө тэнд өнгөрөөдөг, консолын бусад бүлгүүд нь очиж үздэг зүйл.

Хоёр шинэ дэлгэц, хоёулаа нэг байгууллагын хуудас хариулж чаддаггүй асуултад:

Fixed — операторын тест бусад тестийн бүртгэлийг унтраадаг байв

TestTheLastSuperadminIsKept нь сүүлийн ерөнхий админы хамгаалалтыг батлахын тулд бусад бүх ерөнхий админыг үнэхээр идэвхгүй болгодог байсан — тэр сан нь бүх багцын тесттэй хуваалцсан тул тэдний бүртгэлийг ч хамт унтраана. Одоо буцаагддаг транзакц дотор хийгдэж, гадаад ертөнцөд хүрэхээ болив.

Fixed — дахин үүсгэсэн өгөгдлийн сан эрхээ буруу role дээр авдаг байв

Role нь кластерынх, өгөгдлийн сан нь тийм биш. 00079 нь gerege_nexus_app → gerege_nexus_tenant нэршилтийг зөвхөн tenant role байхгүй үед хийдэг тул нэг кластерт хоёр дахь сан үүсгэхэд 00079-өөс өмнөх бүх grant ба RLS policy хуучин role дээр буудаг; платформ нь gerege_nexus_tenant-аар SET ROLE хийдэг. Нэвтрэлт болно, дараагийн дэлгэц бүр 500: permission denied for table users.

Онолын зүйл биш: nexus.gerege.mn-ийг 2026-08-29-нд шинэ суулгацын урсгал турших зорилгоор хоосноос босгоход яг ийм болов — хуучин role дээр 220 эрх, шинэ дээр 11.

Миграц 00098 нь хуучин role-ийн барьж байгаа бүхнийг (хүснэгт, схем, sequence, RLS policy, default privilege) шинэ рүү зөөж, дараа нь хуучин role-ыг кластераас гаргана — тэгснээр дараагийн сан "эхнийх" болно. Мөн:

Гурван guardrail тест нэмэв: tenant role нь бүрхүүлийн асуудаг хүснэгтүүдийг уншиж чадах, өгөгдлийн сан search_path-аа барьж байгаа, консол миграцын хувилбарыг уншиж чадах.

Fixed — GEREGE_CORE_TOKEN deploy-д огт дамждаггүй байв

Регистрийн дугаараар байгууллага, хүн хайх нь энэ токеноор ажилладаг ба credentials.keys-д, клиент код, шидтэний тайлбар дотор бүгд нэрлэгдсэн байсан ч байрлуулалтын гурван газрын нэгд нь ч байгаагүй: compose нь backend рүү дамжуулдаггүй, deploy нь .env-д бичдэггүй, жишээ файлд ч алга. Гараар .env-д нэмсэн ч compose-оор дамжихгүй, дараагийн deploy-д арчигдана — өөрөөр хэлбэл шидтэн production дээр регистрээр хайх боломжгүй, шалтгаан нь харагдахгүй.

Гурвуулаа зассан: compose-ийн passthrough, deploy-ийн env/envs/.env heredoc, ба deploy/.env.prod.example дахь тайлбартай мөр.

Added — консолоос оператор нэмэх (CP-2)

Хоёр дахь операторт хүрэх цорын ганц зам нь production хост дээрх shell — cmd/operator-bootstrap, өгөгдлийн сангийн эзний эрхээр — байв. Эхний account-д зөв хяналт, дөрөв дэх нь буруу: платформ ба түүнд нэгдэх хүн бүрийн хооронд серверийн shell зогсдог, ийм шалтгаанаар өгсөн shell буцаж цуглуулагддаггүй.

/cp/operators дэлгэц: оператор нэмэх, эрх солих, идэвхгүй болгох/эргүүлэх, ба өөрийн нууц үгээ солих. Нэмэлт бүр ерөнхий админ (operator.write), хоёр дахь хүчин зүйл, шалтгаан шаардана, audit-д мөр үлдээнэ.

Шинэ account нь нэвтэрч чадахгүй төлөвт үүснэ: консол нууц үг ба authenticator-ийн түлхүүрийг нэг удаа харуулж, шинэ оператор кодоо оруулж баталгаажуулах хүртэл totp_confirmed_at хоосон. Хулгайлагдсан консолын session account үүсгэж чадах ч, тэр account нэвтэрч чадахгүй.

Миграц 00097 нь консолын DB role-д INSERT өгнө — 00049 үүнийг "CP-2-д хэрэгтэй болно" гэж тэмдэглэн зориуд түдгэлзүүлсэн байсан. DELETE хэвээр өгөгдөөгүй: явах ёстой операторыг идэвхгүй болгоно, audit-ийн мөрүүд оршсоор буй мөр рүү заасаар байна.

Хамгаалалт: сүүлийн (нэвтэрч чадах) ерөнхий админыг идэвхгүй болгож ч, эрхийг нь бууруулж ч болохгүй; хэн ч өөрийгөө идэвхгүй болгож, эрхээ өөрчилж чадахгүй.

Fixed — консол цэс дарах бүрд бүхэлдээ дахин зурагддаг байв

<Console> хүрээ хуудас бүрийн дотор байсан тул /cp дотор шилжих бүрд бүхэл бүрхүүл дахин mount хийгдэж: ачаалалтын төлөв анивчиж, cp.me() дэлгэц тутамд дуудагдаж, толгой/rail/самбарын төлөв (нээлттэй бүлгүүд ч) тэглэгдэж байв.

Хүрээ нь одоо app/cp/layout.tsx дотор. Layout нь доорх маршрутуудын хооронд шилжихэд амьд үлддэг тул зөвхөн ажлын муж дахин зурагдана — Next-ийн App Router энэ зорилгоор layout-тай. Арван долоон хуудас өөрсдийн агуулгаа л буцаадаг боллоо.

Added — консол дээр System Operations апп

Консолын rail хоёр дахь хавтантай боллоо. Эхнийх нь платформ дээр юу байгааг удирддаг (байгууллага, тохиргоо, аудит); хоёр дахь нь байрлуулалтыг өөрийг нь ажиллуулна — 3 цагт асуудаг гурван асуулт: ажиллаж байна уу, юу үйлдвэрлэгдэж байна вэ, юу хадгалагдаж байна вэ.

Хяналт — /cp/ops (API-ийн гурван тоо, дэд бүтцийн хэмжүүрүүд, гадаад системүүд), /cp/ops/alerts (асаж буй сэрэмжлүүлэг ба тохиргооны анхааруулга — хоёр өөр зүйл, нэг мөчид уншигддаг), /cp/ops/jobs (өөрөө ажиллах ёстой ажлууд ба давтан алддаг байгууллагууд).

Тайлан — /cp/ops/usage (бүх байгууллагын энэ сарын хэрэглээ; тоолуур бүр өөрийн утгаараа: тоологддог нь нийлбэр, хадгалалт нь сүүлийн заалт, идэвхтэй хэрэглэгч нь сарын оргил — өдрийн идэвхтэй хэрэглэгчийг нэмбэл нэг хүнийг гуч удаа тоолно), /cp/ops/schedules (аль хуваарь унасныг нэрлэнэ; нүүр хуудас тоог нь л хэлдэг).

Нөөцлөлт — /cp/ops/backups (нөөц ба сэргээлтийн туршилтын түүх, гараар сэргээлт бүртгэх). Нүүр хуудас сүүлийнхийг л харуулдаг нь "өнөө шөнө болсон уу" гэдэгт хариулдаг; түүх нь "унаж байсан уу" гэдэгт хариулна.

Fixed — консол товлосон тайлангуудыг огт уншиж чаддаггүй байв

observability.backgroundJobs нь workspace.report_schedules-ыг асуудаг ч консолын DB role-д тэр хүснэгт дээр эрх байгаагүй. Алдаа нь warning болж залгигдаж, нүүр хуудсанд эрүүл харагддаг самбар үлдэж байв — яг тэр самбарын барихаар бүтээгдсэн уналт. Миграц 00096 нь SELECT эрх ба operator policy өгнө.

Fixed — хуваалцсан тест сангийн улмаас CI санамсаргүй улаан болдог байв

TestARequestUsesCachedControlDecisionsWhenTheirTablesAreUnavailable нь долоон хүснэгт дээр ACCESS EXCLUSIVE авдаг — тэр нь тестийн санаа. Гэвч go test ./... багцуудыг зэрэг ажиллуулж, бүгд нэг Postgres руу заадаг тул өөр багц registry.tenants-ийн мөр барьж байхад deadlock үүсдэг, Postgres хоёрын нэгийг алдаг — 2026-08-29-нд SQL огт өөрчлөөгүй хоёр салбар дээр яг үүнээс болж CI улаан болов (нэг нь main дээр deploy-г зогсоов).

Түгжээг lock_timeout-той болгож, алдвал транзакцаа дахин эхлүүлнэ (таван оролдлого). Тестийн баталгаа хэвээр: хүснэгтүүд үнэхээр хүрэшгүй болсныг шалгасаар байна.

Changed — AI туслах ба и-мэйл баталгаажуулалт консол руу нүүв

Хоёулаа платформын хөрөнгө байсаар атлаа ажлын муж дотор, байгууллага тус бүрд харагдаж байв. Туслахын нийтлэг prompt ба мэдлэгийн сан нь бүх байгууллагад үйлчилдэг — нэг tenant админ түүнийг засах нь бусад бүх байгууллагын хариултыг засаж байсан гэсэн үг. Баталгаажуулалтын үйлчилгээ нь нэг түлхүүр, нэг провайдертай тул "ажиллаж байна уу", "хэнд бичсэн бэ" гэсэн хоёр асуулт нь платформынх; tenant админ хариултын дөрөвний нэгийг л хардаг байв.

Одоо консол дээр: /cp/assistant (нийтлэг prompt, мэдлэгийн сан — бичилт бүр шалтгаантай, audit-д), /cp/email-verification (бүх байгууллагын бүртгэл, үйлчилгээний төлөв — зөвхөн унших). Ажлын мужаас /settings/ai, /settings/email-verification хуудсууд ба /api/v1/admin/ai/*, /api/v1/admin/email-verification/overview маршрутууд устав.

Миграц 00095 нь консолын DB role-д тухайн хүснэгтүүд дээр эрх ба RLS policy өгнө: prompt/мэдлэгийн хувьд зөвхөн tenant_id IS NULL мөрүүд (тиймээс оператор нэг байгууллагын өөрийн prompt-ыг уншиж ч, бичиж ч чадахгүй), бүртгэлийн хувьд зөвхөн SELECT. Мөн нийтлэг prompt түлхүүр бүрт нэг мөр байхыг баталгаажуулсан partial unique index нэмэв — UNIQUE (tenant_id, prompt_key) нь NULL-ыг ялгаатай гэж үздэг тул хоёр "глобал scope" зэрэгцэн орж болдог байсан.

Changed — толгойн нэр бүтэн, аватар хоёр үсэгтэй

UserMenu-ийн дугуй тэмдэг нэг үсгээс хоёр болов (хоёр үгтэй нэрээс үсэг тус бүр, нэг үгтэй нэрээс эхний хоёр үсэг), нэр нь 9rem дээр таслагдахаа больж бүтнээрээ харагдана. Ажлын муж ба консол хоёулаа ижил цэс хэрэглэдэг тул хоёуланд нь хүрнэ.

Changed — консолын толгойд бүтээгдэхүүний өөрийнх нь бүртгэлийн цэс

"Гарах" нь бар дээр ганцаараа зогсох товч байхаа болиод, ажлын мужийн UserMenu-ийн дотор оров: нэр, и-мэйл, доор нь эрх, дараа нь хэл ба загвар (гэрэл / харанхуй / систем), хамгийн доор нь Гарах. Операторт өмнө нь консол дотроос хэл, өнгөө солих арга огт байгаагүй.

UserMenu нь гурван нэмэлт заавал бус prop авна — showTenants, links, subtitle. Консол нь showTenants={false} (байгууллагын session байхгүй тул жагсаалт ч, түүний API дуудлага ч утгагүй), links={[]} (консолд /profile, /settings хуудас байхгүй), subtitle-ээр эрхээ өгнө. Анхдагч утгууд нь ажлын мужийн өмнөх зан төлөв яг хэвээр.

Changed — операторын консол admin.nexus.gerege.mn дээр нүүв

cp. гэдэг товчлолыг хүн уншиж ойлгодоггүй: консолын хаяг өөрөө юу болохоо хэлэх ёстой. Хост admin.nexus.gerege.mn болов — vhost, гэрчилгээ, CONTROL_PLANE_HOST репо хувьсагч, prod .env гурвуулаа шинэ нэр дээр.

cp.nexus.gerege.mn нь гэрчилгээгээ хадгалж, зөвхөн 301-ээр шинэ нэр рүү заана — хуучин bookmark ажиллах боловч консол тэр хостоос үйлчлэхээ болив. API-ийн HostGate нь хуучин нэрээр ирсэн хүсэлтэд 404 өгнө, өөрчлөлт шаардлагагүй: тэр хаяг ердөө CONTROL_PLANE_HOST-той таарахаа больсон.

Changed — консол ажлын мужийн бүрхүүлийг яг хуулж авав

Толгой хэсэг мөр мөрөөрөө ижил боллоо: брэндийн нүд, контекст, цэсний товч + бүх бүлгийг хумих, session цэг дээр операторын нэр, хайлт, баруун талд эрх ба гарах. Зүүн цэс ажлын муж шиг хоёр хуваалттай — 4rem-ийн апп rail ба 14rem-ийн модулийн самбар, хумигддаг бүлгүүдтэй, гар утсан дээр drawer болдог. Ижил class-ууд тул design-ий өөрчлөлт хоёуланд нь хүрнэ.

Layout.tsx-ийг хуваалцаагүй: тэр нь mount дээр /api/v1/me асуудаг — операторт байхгүй session. Хуваалцаж буй зүйл нь stylesheet.

Fixed — модулийн байнгын ажил миграцынхаа өмнө эхэлдэг байв

StartBackgroundJobs нь v1.15.0-д бүртгэгдсэн модуль бүрийн StartHousekeeping-ийг дууддаг болсон боловч жагсаалтын эхэнд байрлаж байв — модулийн миграцыг ажиллуулдаг ApplyCatalogToInstallations нь тэр функцийн төгсгөлд. Хүснэгтээ энэ хувилбараар авсан модуль эхний шүүрдэлтээ байхгүй схем дээр хийнэ.

client.gerege.mn-ий v1.15.0 rollout дээр Өртөөгийн суваг эхний дамжлагадаа relation "urtuu_peers" does not exist гэж зургаан удаа бичив, каталогийн шүүрдэлтээс 600мс өмнө. Өөрөө эдгэрдэг — дараагийн тик ажиллана — гэхдээ логоос харахад deploy унасантай ялгагдахгүй, ба энэ нь суулгац бүрийн эхний ачаалалт дээр давтагдана.

Дараалал зассан; TestAModulesHousekeepingStartsAfterItsMigrations нь өөрийн миграцтай модуль бүртгээд StartHousekeeping дотроосоо хүснэгтээ асуудаг.


[1.15.0] - 2026-08-27

Нэг сэдэв, хоёр хагас: нэршил ба хил. Эхнийх нь кодоос хоёр үгийг гаргав — platform, tenant — хоёр дахь нь цөмөөс хоёр бүтээгдэхүүнийг. Хооронд нь хүний хувийн муж («гэр») суув: гишүүнчлэлгүй хүн нэвтэрч, байгууллагаас хүсэлтийнхээ төлөвийг уншиж, өөрөө нэгдэх хүсэлт гаргадаг болов.

⚠️ v1 дотор эвдэрсэн өөрчлөлт. pkg/nexus-ийн экспортолсон гадаргуугаас нэрс хасагдаж, дахин нэрлэгдсэн. Semver-ээр бол энэ нь major боловч Go нь v2+ дээр модулийн замыг .../backend/v2 болгохыг шаарддаг — тэр нь найман distribution бүрийн бүх import-ыг өөрчилнө. Тиймээс v1 дотор гаргаж, distribution бүр go.mod-ын нэг мөр ба нэршлийн mechanical rename хийнэ. Wire format (tenant_id гэх JSON тэмдэглэгээ) өөрчлөгдөөгүй: web shell, дөрвөн native клиент, гадны OAuth2 клиент бүр хуучин шигээ уншина.

Changed — ⚠️ нэршил: platform ба tenant гэдэг үг кодоос гарлаа

platform гэдэг нэр гурван өөр зүйлийг нэрлэж байв — операторын урсгал, өгөгдлийн сангийн schema, процессыг угсардаг host. tenant нь байршлын үг бөгөөд байгууллагыг заах домэйний үгийн байрыг эзэлж байв (ADR 0001). Хоёулаа одоо зөвхөн прозод үлдэв:

Юу Байсан Болсон
Операторын урсгал internal/platform internal/operator
Процесс угсрагч pkg/platform pkg/host
Байгууллагын урсгал internal/tenant internal/workspace
DB schema platform registry + operator (27 хүснэгтийг хоёр эх сурвалжаар хуваав)
DB schema tenant workspace
SDK TenantID, RequireTenant, AllowedTenants, TenantOf … WorkspaceID, RequireWorkspace, AllowedWorkspaces, WorkspaceOf …

api.txt-ийн 554 мөрөөс 65 нь дахин бичигдсэн, нэг ч тэмдэглэгээ нэмэгдээгүй, хасагдаагүй. Нэрлэлтийг gopls rename-ээр хийсэн тул TenantID нэртэй боловч SDK-гийнх биш дотоод бүтцүүд хөндөгдөөгүй. pkg/platform нь эхлээд дамжуулагчаар үлдэж, дараа нь хасагдав.

Distribution-д хийх ажил: go.mod-ыг энэ хувилбар руу зөөж, nexus.RequireTenant → nexus.RequireWorkspace маягийн mechanical rename. client-gerege-nexus дээр 72 дуудалт байв.

Added — хүн өөрийн мужтай болов («гэр»)

Өмнө нь гишүүнчлэлгүй хүн нэвтэрч чаддаггүй байв: схем нь хүнийг байгууллагаас хамааралгүй гэж үздэг атал нэвтрэлтийн код хориглодог — схемийн зөвшөөрснийг код хориглож буй цорын ганц цэг. EID_JIT_TENANT_SLUG гэсэн тойрч гарах зам нь иргэнийг хэн ч шийдээгүй байгууллагад гишүүн болгодог байв.

Removed — ⚠️ Өртөө ба гадаад холбогч цөмөөс бүрмөсөн гарлаа

Хоёр бүтээгдэхүүн, нэг шалтгаан. Аль аль нь өмнө нь хагасаараа гарсан байв: дэлгэц, амьдралын мөчлөг нь client-gerege-nexus руу явж, «рельс» гэж нэрлэгдсэн хэсэг нь цөмд үлдсэн. Тэр заагийг барьсан үндэслэл нь CORE_BOUNDARY_PLAN.md §4.1-ийн шалгуур: «нэг суулгац дотор үүнээс хоёр зэрэг оршиж чадах уу». Суваг ч, шифр ч чадахгүй — тиймээс рельс.

Шалгуур зөв, гэхдээ дутуу байсан. «Хоёр байж болохгүй» гэдэг нь «цөмд байх ёстой» гэсэн үг биш: хэрэглэгч нь ганц апп бол «ганц байх» гэдэг нь тэр аппын дотор ганц байна гэсэн үг. Гурван сарын дотор nexus.Link ба nexus.PeerDirectory-г Өртөөгийн самбараас өөр юу ч дуудсангүй, экспортыг esign рельсээс өөр юу ч дуудсангүй, nexus.MeetingBooker-ыг юу ч дуудсангүй. Тиймээс шалгуурт хоёр дахь асуулт нэмэгдэв: хоёр өөр бүтээгдэхүүн үүнийг өнөөдөр дуудаж байна уу?

Өртөө (00087) — тээвэр (internal/workspace/urtuu), дугтуйн гэрээ (pkg/urtuu), nexus.Link, nexus.PeerDirectory болон сувгийн зургаан хүснэгт бүгд явлаа. Цөмд Өртөөгийн юу ч үлдсэнгүй: маршрут ч, /.well-known/urtuu.json ч, URTUU_*/RING_* хувьсагч ч, операторын консолын «Өртөө» гэрэл ч. Аппын хувилбар: io.gerege.nexus.urtuu 1.1.0.

Гадаад системийн интеграц ба webhook (00088) — менежер, провайдерын бүртгэл, OAuth солилцоо, илгээх давталт, гурван хүснэгт бүгд явлаа. nexus.MeetingBooker гэрээ устлаа. Аппын хувилбар: io.gerege.nexus.integrations 1.1.0.

Added — nexus.SecretSealer

Цөмд үлдсэн ганц зүйл нь шифр: INTEGRATION_ENCRYPTION_KEY ба түүний AES-GCM нь internal/kernel/security-д хэвээр бөгөөд гадагш энэ гэрээгээр гарна. Энэ нь шалгуурын хоёр асуултыг хоёуланг нь давсан цорын ганц хэсэг: нэг суулгацад хоёр шифр байж болохгүй, бөгөөд итгэмжлэл хадгалдаг аль ч модуль түүнийг дуудна. Хувьсагчийн нэр өөрчлөгдөөгүй — тэр нь ижил түлхүүр.

Мөн модуль өөрийн байнгын ажлаа зарлах зам нэмэгдэв: бүртгэгдсэн модуль StartHousekeeping(context.Context) метод агуулбал платформ түүнийг эхлүүлнэ. Өмнө нь apps.Bootstrap-ийн жагсаалт л энэ боломжтой байсан бөгөөд тэр нь цөмийн дотоод — Өртөөгийн солилцооны давталт гадаад репод очиход энэ нь зайлшгүй болов.

Changed — ⚠️ зан төлөв өөрчлөгдсөн гурван зүйл

  1. Гарын үсэг зурсан баримт гадагш автоматаар илгээгдэхээ болив. POST /api/v1/esign/documents/{id}/export алга болов. Экспортын код нь холбогчийн аппын дотор байгаа тул хэрэгтэй өдөр нэг дуудлагын зайд байна.
  2. Апп суулгаагүй суулгац Өртөөгийн дугтуй хүлээж авахаа болив. Өмнө нь аппгүй ч дугтуй ирж, шалгагдаж, хадгалагдаж, апп суулгагдмагц уншигдаж байсан.
  3. Демо суулгацын integrations апп жагсаалтаас хасагдав (pkg/host/seed.go).

Changed — CI нь downstream repo-г clone хийхээ болив

Платформ өөрийн эзэмшдэггүй репог шинэчилж push хийх хүртэл merge хийж чадахгүй байсан — хамаарлын чиглэл урвуу. Гэрээний эвдрэлийг api.txt ба testdata/canary (тусдаа Go модуль тул go build ./... түүнд хүрэхгүй, CI л хүрнэ) хоёр барина.

Migration — амьд суулгац дээр юу хийх вэ

Энэ хувилбар нь арван нэгэн миграц (00079–00089) агуулна, тэдгээрийн хоёр нь бүх хүснэгтийг schema хооронд зөөнө (platform → registry+operator, tenant → workspace) ба нэг нь DB role-ыг gerege_nexus_app → gerege_nexus_tenant болгож нэрлэнэ. Аль аль нь ажиллаж буй суулгац дээр шалгагдсан (nexus.gerege.mn, 2026-08-27).

Модулийн миграц бичдэг distribution бүр анхаараарай: role-ыг нэрээр нь бичсэн бол цэвэр өгөгдлийн сан дээр role "gerege_nexus_app" does not exist гэж унана. pg_roles-оос хайж, gerege_nexus_tenant-ыг эхэнд тавь.

00087 ба 00088 нь есөн хүснэгтийг CASCADE-аар хаяна. 2026-08-27-нд гурван суулгац дээр тоолоход бүгд 0 мөртэй байв (nexus.gerege.mn дээрх хоёр холбогч нь INACTIVE, хэзээ ч холбогдож байгаагүй). Өгөгдөлтэй хост дээр appstore-ийн store_* хүснэгтүүдэд ажилласан алхмыг давт: миграцаас өмнө CREATE TABLE <нэр>_keep AS SELECT * FROM workspace.<нэр>, rollout, дараа нь гадаад түлхүүрийн дарааллаар буцааж INSERT. Модулийн миграц нь хүснэгтүүдийг IF NOT EXISTS-ээр дахин зарлах бөгөөд платформ суулгацын үед бус ачаалахдаа модулийн схемийг шүүрддэг (v1.10.1-ээс хойш), тиймээс гар засвар шаардлагагүй.

Distribution-ууд: pkg/urtuu, pkg/nexus.Link, PeerDirectory, MeetingBooker дөрөв алга болсон нь эвдэрсэн өөрчлөлт. Эдгээрийг хэрэглэдэг цорын ганц repo нь client-gerege-nexus бөгөөд тэр нь энэ өөрчлөлттэй хамт шинэчлэгдсэн.


[1.14.1] - 2026-08-26

Fixed — өөр байгууллагын клиентэд өгөх анхны зөвшөөрөл бүтдэггүй байв

Зөвшөөрлийн дэлгэцийн ард байх /api/v1/oauth2/consent нь клиентээ дуудагчийн байгууллагад уягдсан холболтоор уншиж байсан тул мөрийн түвшний хамгаалалт өөр байгууллагын бүртгэсэн клиентийг нуудаг байв. Үр дүнд нь unauthorized_client: unknown or disabled client буцаж, байгууллага хоорондын анхны нэвтрэлт хэзээ ч гүйцэддэггүй: клиентээ бүртгэсэн байгууллагын гишүүд ороод, бусад нь орж чаддаггүй.

Клиент нэг байгууллагынх, нэвтэрч буй хүн өөр байгууллагынх байх нь зохиомжийн санаа — issueAuthCode нь кодод клиентийнх биш хэрэглэгчийн байгууллагыг хадгалдаг нь яг үүний төлөө. Нэвтрэлтгүй ажилладаг /oauth2/auth аль хэдийн платформын замаар уншдаг байсан; одоо consent хоёр ч мөн адил уншина.

TestConsentPromptFindsAnotherTenantsClient нь dbguard-тай усан сан дээр хоёр байгууллага үүсгэж, засваргүйгээр production-ий яг тэр 400-г давтдаг.


[1.14.0] - 2026-08-25

Changed — ⚠️ PlatformVersion-ийг стампдах -X зам өөрчлөгдөв

internal/platform/server.go-д байсан PlatformVersion нь v1.13.0-ийн дараа internal/kernel/config-т нүүсэн (5471576). Distribution-ий Dockerfile-ууд хуучин замаар стампддаг бол юу ч болохгүй: Go нь байхгүй тэмдэгт рүү чиглэсэн -X-ийг чимээгүй үл тоомсорлодог тул образ өөрийгөө 1.1.0 гэж хэлээд, өөрийнхөө каталогийн manifest бүрийг татгалзаж, асахаа болино.

-X .../backend/internal/platform.PlatformVersion=${version}        # үхмэл
-X .../backend/internal/kernel/config.PlatformVersion=${version}   # зөв

v1.14.0 бол энэ нь мэдрэгдэх эхний хувилбар. Бумп хийхийн өмнө deploy/Dockerfile-аа засах хэрэгтэй — appstore-gerege-nexus, sso-gerege-nexus хоёр 2026-08-25-нд яг үүнийг хийсэн.

Removed — SSO клиентийн апп цөмөөс гарч, App Store-д очлоо

internal/apps/sso_clients нь энэ репогийн сүүлчийн апп байв. Одоо appstore-gerege-nexus-д modules/ssoclients болж, каталогоор дамжин аль ч distribution түүнийг апп стороос татаж суулгана — эхнийх нь sso-gerege-nexus.

Үүний дараа catalog/apps.json ба internal/apps хоёулаа хоосон. Энэ нь дутуу байдал биш: ECOSYSTEM_GIT_STRATEGY-ийн тавьсан "бизнес апп огт байхгүйгээр асдаг платформ" шалгуур ингэж биеллээ. Аппын дэлгэц, migration history нь shell-д үлдсэн бөгөөд module-гүйгээ амьгүй (§2.3-ын дүрэм, contacts-тай адил) — иймд цөмийн frontend image-ийг хуваалцдаг distribution бүр дэлгэцээ хэвээр авна.

Рельс нь цөмийнх хэвээр. OAuth2 сервер — код олгох, токен солих, id_token гарын үсэглэх, клиент хадгалах, redirect шалгах — бүгд internal/tenant/ssoprovider-т үлдэв. Апп нь тэр рельс рүү хорин экспортолсон нэрээр биш, шинэ гэрээгээр хүрнэ.

Added — nexus.SSOClientRegistry

Модулийн SDK-д OAuth2 клиентийн бүртгэлийн гэрээ нэмэгдэв: SSOClientRegistry interface ба SSOClient, SSOScope, SSOClientActivity, SSOConsent, SSOSigningKey төрлүүд, ErrSSOClientNotFound sentinel.

Цөм нь nexus.Provide[nexus.SSOClientRegistry](ssoprovider.AsClientRegistry(...)) гэж нийтэлнэ. Ямар ч distribution nexus.Capability[nexus.SSOClientRegistry]() гэж асуугаад өөрийн суулгацын провайдерыг администрацлах апп агуулж чадна.

Нууц үгийн digest гэрээгээр гардаггүй: CreateClient/RotateClientSecret нь задгай нууцыг авч, hash-ыг цөм өөрөө хийнэ — модуль тухайн суулгацын hash функцийг мэдэх шаардлагагүй.

Changed — Консолыг хаягаар хязгаарлах нь зөвхөн платформ ХААЛТТАЙ үед

Консол нь nginx-ийн deny all allowlist-ын ард байсан бөгөөд тэр хаалт нь платформын өөрийнх нь тохиргоог харж чаддаггүй: нээлттэй (public) горимд ажиллаж буй, хэн ч бүртгүүлж болдог суулгац операторуудаа хаанаас нэвтрэхийг нь зааж, өөрчлөх бүрд secret + deploy + nginx reload шаарддаг байв.

Одоо шийдвэрийг платформ өөрөө, хүсэлт бүр дээр гаргана:

хаалттай (private) → хаяг жагсаалтад байх ёстой (жагсаалт өгсөн бол)
нээлттэй (public)  → хаягаар огт хязгаарлахгүй

Үнэ нь ил: нээлттэй суулгац дээр танихгүй хүн консолын нэвтрэх дэлгэц хүртэл хүрнэ. Ард нь хэвээрээ — хостоос гараар үүсгэсэн оператор бүртгэл, нууц үг, баталгаажсан хоёр дахь хүчин зүйл, өөрчлөлт бүрд дахин асуух step-up. Хаягийн жагсаалт бол таван давхаргын хамгийн гадна талынх бөгөөд платформын өөрийнх нь «энэ суулгац хэнийх вэ» гэсэн хариулттай зөрчилддөг цорын ганц нь байлаа.

nginx-ийн snippet хэвээр — хүсвэл ирмэг дээр давхар хаалт болно. CONTROL_PLANE_ALLOWED_CIDRS-д open гэж бичвэл түүнийг өргөнө. 0.0.0.0/0 гэж бичих боломжгүй хэвээр: буруу бичсэн prefix консолыг чимээгүй нээхээс сэргийлнэ, харин open гэдэг үгийг санамсаргүй бичихгүй.

Changed — open-gerege-core-оос бүрмөсөн салав

Гурван багц үлдсэнийг nexus дотроо бичив:

Хуучин Шинэ Юу
pkg/eid internal/kernel/eidrp eID-гийн RP клиент (QR/push нэвтрэлт, poll, төлөөлөл)
pkg/gemini internal/kernel/gemini generateContent + WAV туслах
core/business/usecases/sign internal/kernel/eidsign PDF/digest-д PIN2 гарын үсэг, PAdES

go.mod-оос github.com/gerege-systems/open-gerege-core бүрэн хасагдав. pdfcpu, digitorus/pdf, digitorus/pdfsign гурав indirect байснаа шууд хамаарал болов — өөрчлөлт нь тэднийг ХЭН эзэмшиж байгаад л, аль хувилбар ажиллаж байгаад биш.

Юуг нь авчирсангүй: gemini-гийн embedding клиент, streaming, model fallback — энэ репод хэн ч дуудаагүй. eID-гийн гарын үсэг зурагчид, PKI самбар, төлөөллийн бичилт — өөр бүтээгдэхүүнийх.

Wire protocol гурвуулаа өөрчлөгдөөгүй. Гарын үсгийн багц дээр нэг зүйл сайжрав: өмнө нь алдааны утгыг («represent» гэсэн үг агуулж буй эсэх) хайж HTTP статус шийддэг байсныг одоо errors.Is-ээр яг таарууллаа — үг солиход статус өөрчлөгддөггүй болов.

Гарын үсгийн урсгалын хамгаалалтууд хэвээр бөгөөд одоо тестээр барьцаалагдав: өөр иргэний session «олдсонгүй» гэж хариулна (IDOR), хэрэглэгчийн өгсөн зургийн URL дотоод сүлжээ рүү хүрэхгүй (dial түвшний SSRF шалгалт, redirect дагахгүй), production-д түр зуурын Document-Signer хориотой, displayText кирилл тэмдэгтээр таслагдана.

Changed — eID-ийн RP клиентийг nexus дотроо бичив, person блокийн шинэ нэршилд оруулав

eID нь person блокоо core.gerege.mn-ий нэршилд оруулж, birthDate, gender, familyName, geID нэмсэн. Гурван нэр нь гурван ӨӨР ойлголт бөгөөд одоо тэрийгээ хэлдэг болов:

firstName  = нэр            (өөрийн нэр)
lastName   = ЭЦГИЙН нэр     (сертификатын subject-д ордог)
familyName = УРГИЙН овог    (сертификатад ОРОХГҮЙ)

Өмнө нь givenName/surname хоёрыг уншиж байсан бөгөөд «surname» гэдгийг эцгийн нэр рүү тулгадаг байсан нь англиар зөв уншигдаж, монголоор ил буруу байдаг төрлийн алдаа.

open-gerege-core/pkg/eid-ийг ашиглахаа больж, RP клиентээ internal/kernel/eidrp-д бичив. Хоёр шалтгаан, хоёр дахь нь чухал:

Wire protocol өөрчлөгдөөгүй: ижил зам, ижил ACSP_V2 body, ижил Bearer. Хуучин givenName/surname талбарыг fallback болгож уншина — нэг тал шинэчлэгдээд нөгөө нь хоцорсон өдөр иргэн бүрийн нэр алга болохоос сэргийлнэ.

Changed — eID-ээр нэвтэрсэн иргэний хаяг утгатай болов, geID нь доош дамжина

eID-гийн session хариу COMPLETE+OK үед person.geID — иргэний core.gerege.mn дахь дугаар — буцаадаг болсон. Түүнээс өмнө энэ платформ eID-ээр анх нэвтэрсэн иргэнд eid+3854e516490de4f6184ac3af2d8cc8b8@identity.invalid гэсэн зохиомол хаяг үүсгэдэг байв: давхцахгүй, тогтвортой, гэхдээ хүн харахад утгагүй — тэр дундаа энд холбогдсон RP-үүд түүнийг жинхэнэ хүний email claim болгож хүлээж авдаг байсан.

Одоо geID мэдэгдэж байвал хаяг нь 10000263@gemail.com. Хуучин хаягтай бүртгэлүүд дараагийн нэвтрэлтдээ шинэчлэгдэнэ — тэр хаягаар хэн ч нэвтэрч байгаагүй тул алдах зүйл алга. Өөрийн жинхэнэ хаягаар бүртгүүлээд дараа нь eID-ээ холбосон хүний хаягийг хөндөхгүй: нэвтрэх арга нэмэх нь нэр солих шалтгаан биш.

geID нь platform.users.ge_id баганад тусад нь хадгалагдана (NULL зөвшөөрнө — eID-д заавал байх талбар биш) бөгөөд нэг дугаар = нэг хүн гэдгийг unique index барина. Доод урсгал руу хоёуланг нь өгнө: email ба ge_id нь id_token, userinfo хоёуланд гарч, discovery-гийн claims_supported-д зарлагдав. Хаягнаас нь дугаарыг нь салгаж авдаг RP байх ёсгүй — тэр нь хаягийн хэлбэр өөрчлөгдөх өдрийг хүртэл ажилладаг код юм.

Тайлбар: open-gerege-core/pkg/eid-ийн RP клиент нь person блокоос geID-г задлахгүй тул тэр замаар ирэхгүй байна. Түүнийг зөв уншдаг болтол дугаарыг Gerege Core бүртгэлээс регистрээр нь авна — geID гэдэг нь тодорхойлолтоороо core.gerege.mn-ий users.id тул үр дүн нь ижил. Токен байхгүй, бүртгэл хүрэхгүй, иргэн бүртгэлд байхгүй — гурвуулаа адилхан «geID алга» гэж дуусах ба аль нь ч eID зөвшөөрсөн нэвтрэлтийг унагаах шалтгаан биш.

Fixed — Консолын нүүр хуудас цагаан дэлгэц болдог байсныг зассан

cp.<домэйн>/cp нь «This page couldn't load» гэж унадаг байв. Шалтгаан нь нэг мөрөнд: observability.New(...) нь Warnings callback-ыг огт дамжуулаагүй тул warnings талбар JSON-д null болж, нүүр хуудас түүнийг .map хийхэд бүх дэлгэц уначихдаг байлаа. Go-д nil slice нь null болдог бөгөөд жагсаалт гэж зарласан талбарт null ирэх нь «богино жагсаалт» биш, өөр төрөл.

Хоёуланг нь зассан: callback-ыг холбож (нүүр хуудас платформын өөрийн анхааруулгуудыг дахин харуулна), мөн Overview буцахдаа бүх жагсаалтаа хоосон жагсаалт болгодог болов — найман талбарыг найман газарт санахын оронд нэг дор.

Added — Тохиргоог консолоос: нууц бус утга нэг дэлгэцэд, түлхүүрүүд нөгөөд

.env засаад контейнер дахин асаах гэдэг нь тохиргоо өөрчлөх ганц зам байв. Одоо /cp/config дээр:

Түлхүүрүүд нь тохиргооны хүснэгтэд ОРООГҮЙ бөгөөд орох ч ёсгүй: internal/kernel/settings нь нууц Kind байхгүй, нууц мэт нэрийг panic-аар татгалздаг гэдгээрээ тэр хилээ кодоор барьдаг. Оронд нь тусдаа хүснэгт, тусдаа дүрэмтэйгээр:

Хэрэглэгч талдаа: copilot болон дуут боломжийн клиент нь түлхүүр солигдвол дахин баригддаг болов (өмнө нь зөвхөн загвар солигдоход), тайлангийн SMTP relay нь илгээх үедээ уншигддаг болов — өмнө нь ачаалах үед л шийддэг байсан тул консолоос тавьсан утга дахин ачаалтал үйлчлэхгүй байх байлаа.

Added — Байгууллагын мэдээллийг регистрээс нь шинэчлэх

/organisation дээр «Core-оос шинэчлэх» товч. Регистрийн дугаараар Gerege Core-оос албан ёсны нэр, хаяг, утас, и-мэйлийг татна. Регистрд байхгүй талбарыг хөндөхгүй — байхгүй утгаар дарж хоослох нь огт ажиллаагүйгээс дор.

Товч болохоос товлосон ажил биш: буцаж ирсэн утга нь админы нүдний өмнө байгаа талбаруудыг дарж бичдэг тул хүнгүйгээр хийвэл Мягмар гаригт хийсэн засвар Лхагва гаригт чимээгүй алга болно.

Added — Анхны тохиргооны шидтэн: байгууллагаа регистрээр нь татаж авдаг

Тушаалын мөр бүх хүнд тохирохгүй. Шинэ deployment дээр одоо /setup гэсэн гурван алхамт дэлгэц гарна: байгууллага, админ, нууц үг. Байгууллагын болон хүний регистрийн дугаараар Gerege Core-оос нэр, албан ёсны нэр, и-мэйлийг татна — суулгасан хүний гараар бичсэн нэр нэхэмжлэх дээрхтэй таарахгүй байдгийн эсрэг.

Вэб дээрх first-run хуудсыг аюулгүй болгодог хэсэг нь ихэвчлэн орхигддог: нээлттэй /setup бол хамгийн түрүүнд хүрсэн хүнийх. Тиймээс энэ нь токеноор зэвсэглэдэг — 256 бит, зөвхөн байгууллагагүй үед, зөвхөн санах ойд, ачаалах үед лог руу нэг удаа бичигдэж, байгууллага үүссэн даруйд устана. Токенгүй үед эндпойнтууд 401 биш 404 буцаана: "буруу токен" гэж хэлэх нь таах токен байгааг хэлж байгаа хэрэг.

tenant-bootstrap команд хэвээрээ — терминалтай хүнд, хөтөчгүй сервер дээр.

Added — Шинэ deployment-д эхний байгууллагаа нээх команд

Шинэ deployment дээр орох зам байсангүй: бүртгүүлэх дэлгэц байхгүй, demo бүртгэл production дээр үүсэхгүй, консол нь өөрийн vhost, оператор бүртгэл, TOTP шаарддаг. Өмнөх хоёр бүтээгдэхүүн гараар бичсэн SQL-ээр орсон бөгөөд тэр SQL нь илэрхий биш гурван зүйлийг зөв хийх шаардлагатай — admin эрхийг tenant мөрийн trigger үүсгэдэг, админ эрх platform.users.is_admin дээр биш гишүүнчлэлийн эрх дээр тогтдог, гишүүнчлэлд эрхийн мөр нь тусад нь хэрэгтэй — бөгөөд эхний оролдлогод гурвуулаа буруу хийгдсэн.

Одоо tenant-bootstrap команд образ дотор явна, консолын operator-bootstrap- тай яг ижил зарчмаар: DATABASE_URL эзэмшдэг хүн ажиллуулна, нууц үгийг TTY-ээс асууна, ард нь юу ч үлдээхгүй. Байгууллага аль хэдийн байвал татгалзана — анхдагч нууц үг ч биш, үүрд үлддэг env хувьсагч ч биш, ачаалахаас хойш онгорхой зогсох first-run вэб хуудас ч биш.

Асах үед байгууллагагүй deployment-ийг лог нь хэлж, яг ажиллуулах командыг зааж өгдөг болов — өмнө нь энэ төлөв чимээгүй байсан тул хоёр удаа "нууц үг буруу" гэсэн шинжээр дебаг хийгдсэн.

Fixed — Суулгаагүй аппын цэс хажуугийн самбарт үлддэг байсныг зассан

«AI тохиргоо» ба «Интеграцууд» хоёрыг бүрхүүл өөрөө бичдэг байв. Тэр хоёр өнөөдөр апп болсон тул тэдгээрийг суулгаагүй байгууллага ч гэсэн цэсэндээ харсаар, дарахад л 404 авдаг байв.

Одоо модуль өөрсдөө зарлана (Group: settings), тэгэхээр:

Бүрхүүл мөн бүх chrome аппын цэсийг зурдаг болов, эхнийхийг нь биш. Гурав нь тийм тугтай (байгууллага, туслах, холбогч) бөгөөд өмнө нь эхнийх нь л зурагддаг байсан тул нөгөө хоёрын дэлгэц утсан дээр хаанаас ч хүрэхгүй байв — доод таб мөр зөвхөн rail аппуудыг жагсаадаг.

Fixed — Суулгасан ч бинарь үүрдэггүй апп чимээгүй алга болдог байсныг хэлдэг болов

Апп өөр репод нүүхэд суулгацын мөр нь үлддэг. Тэр байгууллагын хувьд апп нь маршрут нь 404, цэсний API-д байхгүй — өөрөөр хэлбэл хаанаас ч харагдахгүй болно. Утсан дээр аппуудын хооронд явах цорын ганц зам нь доод таб мөр тул энэ нь «цэс эвдэрлээ» гэж уншигдана.

Суулгац буруу байгаа хэрэг биш — аппын явалтыг давж шинэчилсэн оператор тэр байдалд хууль ёсоор орно — харин юу ч дуугардаггүй байсан нь алдаа. Одоо асалт бүрт нэрсийг нь бичнэ:

catalog: organisations have apps installed that this binary does not carry;
their routes answer 404 and they appear in no sidebar.
Either deploy a distribution that compiles them, or uninstall them
apps=[io.gerege.nexus.documents io.gerege.nexus.egov io.gerege.nexus.organisation]

Гадаад апп үүнд ороогүй: тэдгээр нь өөр газар ажилладаг бөгөөд энэ бинарь тэднийг үүрэх ёсгүй.

Removed — Цэсний blueprint пакет устав; модуль өөрийн цэсээ бүрэн зарлана

internal/platform/menu/blueprints.go нь аппын id-гаар түлхүүрлэгдсэн хүснэгт байв: «энэ апп цаашид ийм дэлгэцүүдтэй болно» гэсэн жагсаалт, зөвхөн энэ репогийн аппууд өөрсдийгөө нэмж чадах. Тэр хүснэгт устлаа.

nexus.MenuDefinition.Group нэмэгдэв (MenuGroupModules, MenuGroupSettings). Аппын хоёр толгойн алинд нь дэлгэц харьяалагдахыг модуль хэлдэг болов — эцэг нь платформынх хэвээр, тэгэхгүй бол апп өөр аппын бүлэг рүү бичлэг оруулж чадна. Талбар нэмэх нь эвдэх өөрчлөлт биш (RELEASING §1).

sso_clients нь blueprint-ийн таван бичлэгээ өөрөө зарлав, замууд нь өөрчлөгдөөгүй (/module/sso-clients/<id>).

Хоёр шалгуур модулиудынхаа хажууд нүүлээ: цэсний бичлэг бүр жинхэнэ хуудастай байх, шошго бүр долоон хэлээ хамрах. Аль аль нь одоо nexus.List()-ийн оронд internal/apps-ийн модулийн хүснэгтээс асууна.

Removed — Тайлангийн апп цөмөөс гарав

internal/apps/reports ба domain/reports нь client-gerege-nexus руу нүүлээ. Гарах боломжтой болсон нь v1.12.0-ийн дөрвөн гэрээ (ReportEngine-ийн Form/RunConsolidated, ReportSchedules, ReportGrants) ба v1.13.0-ийн InstalledApps.

Хөдөлгүүр цөмд үлдэв — тайлангийн SQL ажиллуулах, экспорт бичих, гурван цагт шуудан илгээх, өөр байгууллагын мөрийг зөвшөөрлөөр унших. report_schedules ба report_grants хүснэгтүүд ч мөн адил: платформ өөрөө бичдэг тул аппын хамт явсангүй — Өртөөгийнхөөс ялгаатай тал нь энэ.

Тэгэхээр суулгац тайлангаа шуудангаар авсаар байна, дэлгэц нь байхгүй ч гэсэн. Хуваарь платформын мөр, sweep платформын давталт.

/api/v1/reports/* арван хоёр маршрут энэ бинариас алга. Хүснэгт устгасан миграц байхгүй.

Энэ бол цөмөөс гарсан зургаа дахь апп бөгөөд internal/apps дотор суулгацын өөрийн бүтээгдэхүүн үлдсэнгүй: одоо тэнд байгаа дөрөв — туслах, холбогч, ээлж солих PIN, SSO клиентийн бүртгэл — бүгд платформын рельсийн дэлгэц.


[1.13.0] - 2026-08-23

Added — nexus.InstalledApps: аль апп суусныг distribution ч асууж чадна

Тэр чадамж нь internal/apps.InstalledApps нэрээр нийтлэгддэг байв — repo-гийн гаднах модуль тэр төрлийг нэрлэж чадахгүй тул асууж ч чадахгүй. Бүртгэл нь төрлөөр түлхүүрлэдэг гэдгийг санавал энэ хоёр өөр түлхүүр байсан гэсэн үг.

Гэрээ экспортлогдож, nexus.AppsOf(ctx, tenantID) туслах нэмэгдэв. Энэ бол PDF-ийн рельс өнөөдөр давсан яг тэр алдаа, яг адилаар илэрсэн: цөмөөс гарсан апп платформын өгч байгаа чадамжийг авч чадахгүй.


[1.12.0] - 2026-08-23

Note — ReportEngine таван метод авсан нь v1 доторх шийдвэр

RELEASING.md §1 нь экспортолсон interface-д метод нэмэхийг эвдэх өөрчлөлт гэж нэрлэдэг: гаднаас түүнийг хэрэгжүүлж байсан бүх төрөл компиллогдохоо болино. Энэ хувилбар ReportEngine-д таван метод нэмж байгаа тул журмаараа major байх ёстой.

Minor-оор гаргаж байгаа шалтгаан, ил тод: ReportEngine бол платформ өгдөг чадвар — distribution нь дуудагч тал, хэрэгжүүлэгч нь энэ репогийн адаптер. Экосистемд өөр хэрэгжүүлэгч мэдэгдэхгүй байна. Хэрэв танай distribution nexus.ReportEngine-ийг өөрөө хэрэгжүүлдэг бол энэ хувилбар дээр компайл унана — тэр тохиолдолд бидэнд хэлээрэй, дараагийн major дээр гэрээг хуваана.

Added — Тайлангийн гэрээ бүрдэв: ReportSchedules, ReportGrants, Form, RunConsolidated

pkg/nexus/reportengine.go гурван цоорхойгоо нэрлээд «эдгээр буутал reports апп internal/platform/reporting-ийн импортоо хадгалж, энэ репод үлдэнэ» гэж бичсэн байсан. Гурвуулаа буув.

nexus.ErrReportScheduleNotFound, ErrReportGrantExists, ErrReportGrantNotPending, ErrReportGrantNotFound, ErrOrganisationNotFound нэмэгдэв — апп бүрийг өөр хариулт болгодог тул sentinel.

Changed — Тайлангийн sweep платформынх болов

Гурван цагт тайлан илгээдэг давталтыг reports апп эхлүүлдэг байсан нь дэлгэцийг суулгацын аж ахуйн ажилд хариуцлагатай болгож байв: аппыг устгасан байгууллага хуваариа чимээгүй хүлээж авахаа болино. Одоо платформ эхлүүлнэ.

Мөн internal/apps/reports нь internal/platform-ийн нэг ч импортгүй болов — domain/reports/postgres устаж, түүний бичилтүүд платформын адаптерт орлоо.

Removed — Өртөөгийн даалгаврын самбар цөмөөс гарав

internal/apps/urtuu ба domain/urtuu нь client-gerege-nexus руу нүүлээ. Гарах боломжтой болсон нь v1.11.0-ийн nexus.PeerDirectory: апп сувгийн хүснэгтийг арван дөрвөн газар шууд уншдаг байсныг гэрээ болгосон (ADR 0004-ийн Саад 2).

Суваг цөмд үлдэв — холбоос, гарын үсэг, дугтуйны дараалал, дахин оролдлого, кодын sync ба тэдгээрийн таван хүснэгт. Суулгац Өртөөний сүлжээнд байхын тулд даалгаврын самбар суулгасан байх шаардлагагүй: дугтуй ирж, шалгагдаж, хадгалагдаж, хүлээн авсан гэж хариулагдаад, тэр төрлийг уншдаг модуль компайллагдах өдрийг хүлээнэ — модулийн анхны тайлбар үүнийг бичсэн байсан.


[1.11.0] - 2026-08-23

Changed — Туслах апп болов

Copilot, ярианаас бичвэр, бичвэрээс яриа, орчуулга, prompt ба мэдлэгийн удирдлага — арван маршрут server.go-гоос гарч internal/apps/ai болов. Өмнө нь суулгац бүр туслахыг үүрдэг, аль нь ч устгаж чаддаггүй байсан; тэр бол энэ архитектурт апп гэдгийн тодорхойлолт (CORE_BOUNDARY_PLAN §4.2).

Суулгацад:

Added — nexus.PeerDirectory: Өртөөгийн сувгийг уншдаг гэрээ

Даалгаврын самбар нь сувгийн хүснэгтүүдийг арван дөрвөн газар шууд уншдаг байв: urtuu_peers-ыг ес удаа JOIN хийж (даалгавар, үйл явдал, дугтуй, гурван тайлан), urtuu_request_codes, urtuu_peer_codes, urtuu_deliveries-ыг тус бүр. Хажууд нь «энэ хоёр пакет бол давхаргаар хуваагдсан нэг бүтээгдэхүүн, нэг схем хуваалцдаг» гэсэн тайлбар бичээстэй байсан — тэр нь хоёул нэг репод байх хүртэл үнэн бөгөөд ADR 0004 энэ аппыг цөмөөс гаргах гол саад гэж нэрлэсэн.

Одоо дөрвөн метод:

Peers(ctx, tenantID) ([]Peer, error)                        // хэн нөгөө талд байна
RequestCode(ctx, tenantID, code) (RequestCode, bool, error) // код юу гэсэн үг
CodeOpenOn(ctx, tenantID, peerID, code) (bool, error)       // тэр холбоос дээр зарлагдсан уу
DeliveryLoad(ctx, tenantID, from, to) ([]PeerLoad, error)   // тухайн хугацаанд юу явсан

nexus.Directory-гийн хэв маягаар: хуудсанд нэг уншилт, ID-г нэр болгох нь санах ойд. Таван зуун даалгаврын самбар нь давтагддаг хариултын төлөө таван зуун JOIN байсан.

Тайлангуудын бүлэглэлт нэрээр биш ID-гаар болов: нэг нэртэй хоёр холбоос хоёр мөр хэвээр үлдэнэ, өмнө нь нийлдэг байсан.

Changed — Ээлж солих PIN апп болов, нэвтрэлт нь цөмд үлдэв

Кассын дэлгэц дээр ажилтан солигдоход бичдэг богино нууц үг — хүснэгт, түгжигдэлт, хэлбэрийн дүрэм, PIN тавих дэлгэц — internal/apps/staffpin болов. Дэлгүүргүй суулгац эдгээрийг үүрэх шалтгаангүй.

Нэвтрэлт өөрөө гараагүй, гарч ч болохгүй. POST /api/v1/devices/staff/pin нь платформын session нээдэг бөгөөд session нээх нь аппын эрх биш — SDK зориудаар ийм зам санал болгодоггүй. Тэр маршрут одоо nexus.StaffCredential-ыг хэрэгжүүлсэн аппаас «энэ хэн бэ» гэж асууна.

Added — nexus.StaffCredential

Хуваалцсан төхөөрөмж дээрх богино нууц үгийн гэрээ. Нууц үг нь юу байх — PIN, тэмдэг, карт — бүтээгдэхүүний шийдвэр; session нээхийг зөвхөн платформ хийнэ гэдэг нь тийм биш. ErrStaffCredentialRejected нь буруу нууц үг, түгжигдсэн бүртгэл, аппгүй байгууллага гурвуулангийн хариулт — гурвыг нь ялгаж чаддаг дуудагч кассын өмнө зогсох хэн бүхэнд гурвыг нь хэлж чадна.

Changed — Холбогчийн удирдлага апп болов

Zoom, Teams, Google, Dropbox холболт бүртгэх дэлгэц, OAuth-ийн урсгал, илгээлтийн бүртгэл — server.go-гийн ес маршрут internal/apps/integrations болов. Хурлын өрөө захиалдаггүй, гадагш юу ч файлддаггүй суулгац тэднийг үүрэх шалтгаангүй.

Рельс нь цөмд үлдэв — internal/platform/integration: менежер, провайдерын бүртгэл, OAuth солилцоо, нууцлалын түлхүүр, илгээх давталт. Хоёр зүйл түүнээс хамаарна: PDF гарын үсгийн рельс баримтаа түүгээр илгээдэг, nexus.MeetingBooker нь түүний адаптер.

Changed — httpx.DecodeLimited

Хүсэлтийн биеийг хэмжээтэйгээр уншдаг туслах нь ai_handlers.go дотор амьдардаг байсан бөгөөд платформын есөн handler түүнийг хэрэглэдэг байв. Гурав дахь хуулбар үүсэхийн өмнө httpx рүү гарлаа — энэ пакет нь яг ийм хүсэлтийн үгсийн санд зориулагдсан.

Added — nexus.QuotaGate ба nexus.Quota

Сарын квотыг модуль нэрээр нь гуйдаг болов:

rr.Use(nexus.QuotaGate("ai"))

Хязгаарыг control plane зардаг тул модуль өөрөө тоог нь мэдэх ёсгүй. Квот хэмждэггүй суулгац, эсвэл хэмждэггүй төрөл дээр бүх хүсэлтийг нэвтрүүлдэг middleware буцна — RateLimit-ийн адил, дутуу тоолуур маршрутыг унагаах ёсгүй.

Fixed — nexus.RateLimit анх удаа үнэхээр хуваалцсан төсөв болов

SDK нь модульд «суулгац даяар хуваалцсан төсөвтэй хязгаарлагч» амлаж байсан бөгөөд өнөөдрийг хүртэл нэг репликийн IP хувин буцаадаг байв: нэг gateway-ийн ард гурван реплик гурван өөр төсөв барьж, суулгац нийтдээ юу ч барихгүй. Платформын өөрийн үнэтэй маршрутууд — нэвтрэлт, poll, туслах — Redis тоолуурыг server.go-д гараар хажууд нь холбож байсан, яг тэр нь модулийн хийж чадахгүй зүйл. Одоо чадамж нь хоёуланг нь өгнө; Redis-гүй суулгац хуучин локал хувиндаа үлдэнэ.


[1.10.1] - 2026-08-23

Fixed — Модулийн схем суулгацын дараа ирвэл хэзээ ч ажиллахгүй байв

Модулийн өөрийн миграц нь ганцхан газраас — суулгах замаас — дуудагддаг байсан. Тиймээс аль хэдийн суусан аппын схем нь дараа нь модуль руу нүүвэл тэр миграц хэзээ ч ажиллахгүй: app_installations дээр «installed», маршрут нь mount хийгдсэн, харин хүсэлт бүр relation ... does not exist гэж хариулна.

Онолын биш: v1.10.0-д гурван апп энэ репогоос гарч схемээ авч явахад 00077 нь тэдний үлдээсэн хүснэгтийг устгасан, тэдгээрийг өмнө нь суулгасан суулгац дээр модулийн миграц ажиллах зам байгаагүй. Стек эрүүл асаж, бүрхүүл зурагдаж, нэвтрэлт ажиллана — зөвхөн тэр аппын дэлгэц л 500 өгнө.

Одоо applyCatalogToInstallations бүрт (асалт дээр нэг удаа, дараа нь каталогийн синк бүрт) бинарь дотор компайллагдсан модуль бүрийн схем ажиллана. Суулгацаар биш модулиар: модулийн хүснэгт бол тенантынх биш суулгацынх — runModuleMigrations анхнаасаа гүйлгээнээс гадуур байгаа шалтгаан яг тэр. Хэрэглэгдэхгүй схем бол ямар ч суулгац өөрийн үүрдэггүй апп бүрийн хувьд байдаг төлөв.

goose-ийн ачаар идемпотент; нэг модулийн алдаа нөгөөг нь нуухгүй; өгөгдлийн сан хараахан босоогүй байхад асалтыг зогсоохгүй — дараагийн синк дээр дахин оролдоно.


[1.10.0] - 2026-08-23

Removed — Цахим засаг, баримт бичиг, байгууллага цөмөөс гарав

Гурван апп — io.gerege.nexus.egov, io.gerege.nexus.documents, io.gerege.nexus.organisation — нэг өдөрт client-gerege-nexus руу нүүлээ. Цөмд үлдсэн нь тэдний ашигладаг рельсүүд: internal/platform/esign (PDF гарын үсэг), gerege (ХУР-ын клиент), eid, dan, directory. Гарах боломжтой болсон нь өмнөх PR-ууд: nexus.StateRegistry/AuditReader (#184), SigningRails/EIDSigner/DANAuthenticator (#185), ReportEngine (#186), Directory (#187).

Схем нь аппуудтайгаа хамт явав — nexus.Migrations (Үе 3) — ба 00077 нь цөмөөс арван нэгэн хүснэгтийг устгав: document_* ес, departments, organisation_people.

Суулгацад юу өөрчлөгдөх вэ:

Changed — Distribution-ийн модуль чадамжуудын дараа баригддаг болов

platform.Options.ExtraModules-ийн callback нь NewServer бүх чадамжаа Provide хийсний дараа дуудагдана — тэр дотор nexus.SigningRails, Link, Directory, ReportEngine, StateRegistry, AuditReader, EIDSigner, DANAuthenticator, Signer, RateLimiter, MeetingBooker. Distribution-ийн модуль конструктор дотроо тэдгээрийг гуйж чадна гэсэн үг.

PDF-ийн рельс одоо ганц түлхүүртэй — экспортолсон nexus.SigningRails. Хажууд нь *esign.Rails-ыг нийтэлж байсан нь internal/-ийн төрлөөр арын үйлчилгээний давталтыг түлхүүрлэдэг байсан: distribution нэрлэж ч, солиж ч чадахгүй, санамсаргүй устгасан Provide нь цэвэр эхэлж таван минутын дараа nil дээр panic хийдэг байв.

Added — Чадвар нэмэх нь SDK-гийн засвар байхаа болив

nexus.Provide[T](impl) ба nexus.Capability[T](). Чадвар нь өөрийнхөө төрлөөр түлхүүрлэгдсэн нэг бүртгэлд амьдардаг болсон тул distribution энэ репо сонсож ч байгаагүй чадвар нийтэлж чадна:

nexus.Provide[mydist.Pricing](myPricing{db})   // main()-д
p, err := nexus.Capability[mydist.Pricing]()   // модульд

nexus.Meetings() нэмэгдэв. MeetingBooker гэрээ 2026-08-15-нд зарлагдсан, адаптер нь тэр өдрөө бичигдсэн, авах арга нь хэзээ ч нэмэгдээгүй — зургаан хоног хэрэглэгдэх боломжгүй байсан.

nexus.ReportSink нь нэрлэгдсэн төрөл болов (өмнө нь нэргүй func(Report)). Чадвар төрлөөрөө хайгддаг тул нэргүй төрөл нь ижил гарын үсэгтэй бүхнийг нэг түлхүүр дээр буулгана.

internal/apps.Bootstrap нь есөн параметрийн оронд nexus.Platform нэгийг авдаг болов. Энэ нь internal/ тул экспортолсон гадаргууд өөрчлөлт биш, гэхдээ дээрх бүртгэл яагаад байгааг тайлбарладаг: тэр гарын үсэг 2026-08-09-өөс 08-20-ны хооронд 4-өөс 9 болж долоон удаа өөрчлөгдсөн.

Deprecated — Дөрвөн Use*

nexus.UseLink, nexus.UseDocumentFiler, nexus.UseAuditSink, nexus.UseReportSink нь Provide[T]-ийн нимгэн бүрхүүл болов. Зан төлөв яг хэвээр, Use*(nil) нь чадварыг хураах нь ч хэвээр.

Тус бүрийг nexus.Provide[<төрөл>](impl) -ээр солино:

Хуучин Шинэ
nexus.UseLink(l) nexus.Provide[nexus.Link](l)
nexus.UseDocumentFiler(f) nexus.Provide[nexus.DocumentFiler](f)
nexus.UseAuditSink(s) nexus.Provide[nexus.AuditSink](s)
nexus.UseReportSink(s) nexus.Provide[nexus.ReportSink](s)

Нэг major цикл үлдэнэ, дараагийн major дээр устана (docs/RELEASING.md §1). Ring(), Documents(), Audit(), RegisterReport() нь deprecated биш — дотроо Capability-ээр уншдаг болсон, гарын үсэг нь хэвээр.

Added — Баримт гарын үсэг зурагдах зүйлээ өөртөө авч явдаг болов

documents апп өнөөг хүртэл агуулгагүй байсан — гарчиг, төрөл, төлөв — тиймээс түүний «гарын үсэг» нь агуулгад холбогдоогүй зөвшөөрөл байв (ADR 0002). Одоо баримт файл авч явна, гарын үсэг нь түүнийг хамарна.

Гурван хэлбэр, ямар файл авч явж байгаагаас нь шалтгаална: PDF-д pades (гарын үсэг баримтын дотор ордог, энэ платформгүйгээр шалгагддаг), бусад файлд detached (SHA-256 дээрх баталгаажсан гарын үсэг), хавсралтгүй баримтад approval — 0002-ын тодорхойлсон зүйл, одоо гуравны нэг. Хэлбэрийг баримт шийднэ, дуудагч биш: сонголт өгвөл PDF-ээ сулаар зуруулах болно.

POST /documents/{id}/file (оруулах, documents.manage), GET /documents/{id}/file (татах, documents.read). Төрлийг байтууд шийднэ; зурагдсаны дараа файл хөлддөг; татахдаа digest шалгагдана.

Ёслол дуусахад юу зурагдсаныг шалгана: зам нь илгээснээс өөр digest баталгаажуулбал татгалзана. Бүртгэлд format ба covered_digest нэмэгдэв (миграц 00070, 00071).

ADR 0002-ын 3-р үе шат (eid.StartSignature устгах) цуцлагдав: хавсралтгүй баримтын зөвшөөрөл нь нэвтрэлтийн ёслол бөгөөд нэвтрэлтийн клиентийнх. Хоёр зам зэрэгцэн үлдэнэ.

Шийдвэр: docs/adr/0003-a-document-carries-what-is-signed.md.

Added — Гарын үсгийн зам нэг болж, SDK-д нийтлэгдэв

nexus.Signer — суурилуулалтын баталгаажсан гарын үсгийн зам, модуль харах хэлбэрээр. eidmongolia дээр хэрэгжсэн.

Шалтгаан нь энэ репод «гарын үсэг» гэдэг үг хоёр өөр зүйлийг нэрлэж байсан: internal/platform/esign нь агуулгын дээрх гарын үсэг үйлдвэрлэдэг бол internal/apps/documents нь иргэний баталгаажсан зөвшөөрлийг бүртгэж, signature_hash талбарт сессийн дугаар хадгалдаг байв. Хоёр дахь нь утгатай бичлэг ч баримтын агуулгад холбогдоогүй — зөвшөөрлийн дараа баримт өөрчлөгдвөл хадгалагдсан ямар ч утга түүнийг үгүйсгэхгүй.

Гэрээ нь нарийхан: digest дээрх гарын үсэг, төлөв, юу гарын үсэг зурагдсаныг шалгах. ДАН энэ гэрээний ард байхгүй — тэр нь зөвшөөрлийн зам бөгөөд апп нь аль нь болохыг хэлэх ёстой.

Баримтын бичлэг үнэн болов. documents апп нь баримтын агуулга хадгалдаггүй (мөрөнд гарчиг, төрөл, төлөв — PDF нь esign-ы өөр хүснэгтэд, холбоосгүй), тиймээс агуулгын гарын үсэг үйлдвэрлэж чадахгүй. API одоо proof талбар нийтэлж, бичлэг нь баталгаажсан зөвшөөрөл гэдгийг хэлнэ; signature_hash нэр хэвээр (клиентүүдийг эвдэхгүй) ч миграц 00069 нь баганад тайлбар бичив. Дэлгэц батламжийн сурвалжийг үргэлж харуулдаг болов — өмнө нь гэрчилгээ байвал түүнийг нуудаг байсан нь гэрчилгээг агуулгын гарын үсэг мэт уншуулж байв.

Ажиллахгүй суваг санал болгохоо болив. Гарын үсгийн цонх E-ID ба ДАН хоёрыг үргэлж санал болгодог байв — байгууллагын бодлого ч, суурилуулалт тухайн сувагтай эсэх ч дэлгэц дээр хүрдэггүй байсан. ДАН дээр тэр нь бүх deployment: амьд клиент байхгүй тул сонгоход л 503. Одоо GET /documents/{id}/signing-rails хоёр баримтыг хамтад нь хариулж, цонх ажиллахгүй сувгийг идэвхгүй болгож шалтгааныг нь хэлнэ — «байгууллага зөвшөөрөөгүй» (админ засна) ба «суурилуулалтад тохируулаагүй» (оператор засна) хоёр өөр асуудал, өөр хүнд.

Шийдвэр ба үе шатууд: docs/adr/0002-one-signing-rail.md.

Changed — Үлдсэн аппууд ч дүрмээ платформоос салгав

egov — ХУР клиент ба төрийн холболтууд одоо порт: лавлагаа, холболтын жагсаалт, асуултын түүх нь ХУР-гүйгээр, өгөгдлийн сангүйгээр ажиллана.

sso_clients — OAuth2 клиент бүртгэлийн шалгалтууд (фрагмент, wildcard, userinfo, loopback биш plain HTTP, нууц үгийн бодлого) домэйн болов. Authorization server бүтнээрээ платформд үлдэв. Энэ аппад урьд нь тест байгаагүй; татгалзал бүрийн өгүүлбэр одоо хүснэгтэд.

documents — зөвшөөрлийн гинжний дүрмүүд домэйн болов: нэг иргэн нэг удаа гарын үсэг зурдгаас урган гарах бүхэн, регистрийн дугаарыг Go-д том үсэг болгож руне-ээр тоолох шийдвэр, гарын үсгийн бодлого. Гарын үсгийн төмөр зам, PDF, retention нь байрандаа.

urtuu — Өртөө самбарын шийдвэрүүд домэйн болов: бүртгэлийн дугаарын хэлбэр, гарчиг ба хугацааг хэнээс тоолох, аль шугам өргөдөл эзэнтэй байхыг шаардах, мөчлөгийг таних, доод байгууллагын хоцорсон мэдээ дууссан ажлыг ухраахгүй байх. Суваг (холбоос, гарын үсэг, дараалал) платформд, гүйлгээт SQL адаптерт үлдэв. Энэ аппын дүрмүүдийг ажиглахад урьд нь хоёр суурилуулалт, түлхүүр, миграцлагдсан схем хэрэгтэй байсан.

Changed — Тайлангийн апп ба түүний хөдөлгүүр хоёр өөр зүйл болов

backend/domain/reports үүсэв: байгууллага аль тайланг үзэж чадах, хуваарь хадгалагдахаасаа өмнө юуг хангах, өөр байгууллагад тайлан үзүүлэхийг хэн зөвшөөрөх — эдгээр дүрэм HTTP handler-аас гарч, PostgreSQL ч, тайлангийн хөдөлгүүр ч байхгүйгээр ажилладаг боллоо. Хөдөлгүүр (internal/platform/reporting) байрандаа: тайлан ажиллуулах, экспортлох, товлон илгээх нь бүх модулийн хуваалцдаг платформ.

Апп нь өмнө нь нэг ч тестгүй байсан тул эхний алхам нь route-уудын зан төлөвийг барих гурван тест байв; тэдгээр өөрчлөгдөөгүйгээр ногоон.

Хоёр хариулт зориудаар засагдав: суулгасан аппын жагсаалт уншигдахгүй үед хоосон 200 биш 500 болов, мөн хуваалцах хүсэлт бүртгэхэд гарсан ямар ч алдааг «аль хэдийн хүсэлт байна» (409) гэж хэлэхээ болив — зөвхөн жинхэнэ давхардал 409.

Changed — Аппын дүрмүүд платформоо мэдэхээ болив

backend/domain/organisation үүсэв: сүүлчийн администратор үлдэх, нэгж өөрийн удмынхаа доор орохгүй байх, архивлагдсан эцэгтэй нэгж сэргэхгүй байх — эдгээр өгүүлбэрүүд HTTP handler дотроос гарч, pkg/nexus ч, chi ч, pgx ч импортлодоггүй пакет болов. Тэдгээрийг ажиглахын тулд өмнө нь миграцлагдсан PostgreSQL, HTTP хүсэлт хэрэгтэй байсан; одоо go test ./domain/... секундын дотор хариулна.

internal/apps/organisation нь адаптер болов: handler бүр декодлож, домэйнийг дуудаж, домэйний хариуг статус болгоно. Route, JSON талбар, HTTP статус, алдааны текст, эрхийн код, module ID — нэг ч байт өөрчлөгдөөгүй, аппын зургаан интеграцийн тест өөрчлөгдөөгүйгээр ногоон.

esign нь internal/platform/esign болов (Rails, Mount). Тэр нь 00058-аас хойш апп байгаагүй бөгөөд internal/apps/boundaries_test.go-д ганц онцгой тохиолдол шаардаж байсан шалтгаан нь зөвхөн хавтасны байрлал байв. Онцгой тохиолдлын хүснэгт одоо хоосон.

Шийдвэр ба түүний хил: docs/adr/0001-domain-first.md.

[1.9.1] - 2026-08-17

/api/v1/oauth2/consent sent already_granted: null when the user had granted that client nothing, because a Go nil slice is null and the handler set the field straight from a lookup that finds no row. The consent screen asks that list whether it includes each requested scope, so the page threw before it rendered: a relying party's very first authorization — the only kind a new client ever sees — ended at "This page couldn't load", with the grant itself never offered.

The handler sends [] now, through the same list helper the client writes already use, and the screen tolerates a null besides: a shell talks to deployments of several ages, and it should not be the half that breaks when one of them answers with an older shape.

[1.8.0] - 2026-08-17

Added — The installation ring and the platform's clock are SDK capabilities

Two things a module could only reach by living in this repository are published now, and the Өртөө app — the first caller of both — imports nothing from internal/ as a result.

nexus.Link is the Өртөө ring: enqueue a signed message for another installation, in the caller's transaction, and register a reader for what arrives. The transport stays in the platform for the same reason document filing does — one signing key, one outbox, one retry schedule and one set of peers per installation, not per app. Two apps growing their own would stop the ring being one ring the first time they disagreed about retries. What is deliberately absent: peering. Who this deployment is linked to is an operator's decision, and a module that could add a peer could arrange its own audience.

nexus.Location, with Now, Today, TimezoneName and DefaultTimezone, moved out of internal/platform/config, which now delegates to them. A module makes calendar decisions of its own — a register number carries a year, a report covers days, a schedule fires at an hour — and one reaching for time.Now() in the process's zone would put a Mongolian office's Monday morning on Sunday.

The Өртөө app took *urtuu.Service, a platform type, and used five of its methods; that was the whole of what stood between it and a repository of its own. It takes nexus.Link now.

Added — The core's shape is a test now, not a paragraph

Four apps have left this repository, and every one of those decisions was made in prose. The paragraphs are right and they were also the only thing holding the line: nothing failed when an app reached into another app's package, or when the platform reached into an app's. The split just got quietly more expensive, and the price was paid by whoever tried it next.

internal/apps/boundaries_test.go asserts the two properties that make a split possible — no app imports another app, and no platform package imports an app — and it found a real violation of each on the first run:

One deliberate exception is recorded in the test with its reason: documents imports esign, because the PDF rails moved inside that app when the two store cards became one. Adding another entry is a decision, and it should feel like making one.

Changed — The commerce distribution is Gerege Business

commerce-gerege-nexus is business-gerege-nexus, the product is Gerege Business, and it answers at business.gerege.mn. The repository, the Go module path, the deployment names and the domain all moved together; the module ids did not, because an id is what an installation row, a manifest and a menu key are all keyed by, and a product changing its name is no reason for a database to lose track of an app.

Two names were deliberately left as they were, both for the same reason — they are not labels for the product, they are records of what has already happened:

Nothing in the platform depends on either name. This entry is here because the next distribution to be renamed will face both.

Added — A deployment can supply its own app icon

BRAND_ICON_URL and BRAND_MASKABLE_ICON_URL join the brand: the tab favicon, the Apple touch icon and the manifest's icon all follow the first, and the second is published only when a deployment has artwork drawn for cropping.

When the brand work landed, icons were the one thing deliberately left in the image — "the logo is an address, the icons are files". That was half true. The files cannot be handed to a built image; the addresses can, exactly as the logo's is, and a deployment serving its own logo already has somewhere to put one. Nothing is resized or generated here: the platform points at what it is given.

Fixed — Гарын үсэг зурагдсан мөр наносекундаа алдаж байсныг зассан

Гарах дугтуйн created_at нь TIMESTAMPTZ баганад хадгалагдаж, илгээхээр уншихад дахин дүрслэгддэг байв. Go-гийн time.Now() Linux дээр НАНОсекунд өгдөг, Postgres МИКРОсекунд хүртэл хадгалдаг тул буцаж гарсан мөр нь гарын үсэг зурагдсан мөрөөс өөр болж, дугтуй бүр нөгөө талд унаж байсан.

Хөгжүүлэгчийн macOS дээр цаг ихэвчлэн микросекундээр зогсдог тул харагдалгүй, CI (Linux) дээр Өртөөгийн бүх тест улаан болж илэрсэн. Хамгийн муу хэлбэр: бүх зүйл зөв харагдаж, сувагт юу ч хүрэхгүй.

payload-ыг яагаад JSONB биш TEXT болгосон шалтгаан яг энэ байсан бөгөөд created_at дээр хийгдээгүй байжээ. Одоо гарын үсэг зурагдсан мөрийг яг тэр чигээр нь хадгална (миграци 00067) — дахин дүрслэгдэхгүй тул дахин зөрөх боломжгүй. Тэр багана SQL-д ашиглагддаггүй байсан: цорын ганц уншигч нь дугтуйг эргүүлэн угсрах код.

Тест нь цагаас хамаарахгүй: наносекундтай тогтмол агшин бичиж, хадгалаад буцааж уншаад гарын үсгийг нь шалгана.

Fixed — Платформын календарь Улаанбаатарын цагаар

Агшинг өдөр болгож бууруулах бүх газар нэг цагийн бүсээр: Asia/Ulaanbaatar (PLATFORM_TIMEZONE-оор дарж болно). Хадгалалт өөрчлөгдөөгүй — багана бүр timestamptz, агшин хадгалсаар байна; энэ нь зөвхөн түүнийг ХЭРХЭН УНШИХЫГ шийднэ.

Илэрсэн алдаа. Хэрэглээний тоолуур өдрийг Go талд Format("2006-01-02")-оор гаргаж, нэгтгэлээ created_at::date буюу өгөгдлийн сангийн бүсээр хийж байв. UTC+8 машин дээр шөнө дундаас өглөөний 8 хүртэл хоёр нь зөрж, бүх тоо тэг гарч, шалтгааныг хэлэх зүйл байхгүй байсан. Консол дээр "энэ байгууллага юу ч хийгээгүй" гэж харагдана — квота мөн адил тэр тоог уншина.

Засвар нэг л газар. dbguard.Install connection бүрт цагийн бүсийг өгдөг болов. Тэр нь энэ репо дахь ЦОРЫН ГАНЦ бүх pool тохируулагддаг газар — production болон өгөгдлийн сантай тест бүр — тул хоёр дахь дуудлага мартагдах боломжгүй. Үүнээс хойш ::date, CURRENT_DATE, date_trunc бүгд платформын цагаар уншина.

Go талд: тоолуурын хуваарь (шөнө дунд), тайлангийн огнооны муж (хэрэглэгчийн бичсэн «2026-08-16»), товлосон тайлангийн cron цаг, Өртөөгийн бүртгэлийн дугаарын он, баримтын загварын {date} — бүгд config.Location()-оор.

Нэг нарийн зүйл тэмдэглэв: $1::date гэж бичвэл Postgres параметрийг шууд date гэж таамаглах тул драйвер Go-гийн бүсээр огноог бууруулж, алдаа зүгээр л нүүдэг. $1::timestamptz::date нь параметрийг агшин байхад хүргэж, бууралт нь session-ий бүсэд явна. Тестээр батлав: алдааг буцаахад унана.

time/tzdata компайлд шингээв — суурь image-д tzdata байхгүй бол LoadLocation унаж, чимээгүйгээр UTC болох байсан.

Added — «Өртөө»: платформ хоорондын үйлчилгээний хүсэлт, даалгаврын суваг

Nexus дээр суурилсан платформууд дээд/доод холбоосоор хэлхэгдэж, урьдчилан бүртгэгдсэн хүсэлтийн кодоор даалгавар доошоо урсаж, биелэлт нь дээшээ буцна. Яамнаас гарсан «хагас жилийн тооллого» агентлагт ирж, аймгуудад задарч, сумдад буугаад, биелэлт нь шат шатандаа хуримтлагдан яаманд харагдана. Их Монгол Улсын өртөө шуудангийн зарчим: захиаг өртөөнөөс өртөөнд дамжуулж, хүрсэн эсэхийг нь буцааж мэдэгддэг.

Дизайны санал docs/URTUU_PROPOSAL.md, ажиллагааны заавар docs/URTUU.md.

Гурван давхарга, зааг нь тодорхой. pkg/urtuu — гэрээ (дугтуй, гарын үсэг, статусын машин), distribution бүр ижил ойлголттой байхын тулд pkg-д. internal/platform/urtuu — тээвэр (холбоос, дараалал, retry), платформын үйлчилгээ. internal/apps/urtuu — апп (io.gerege.nexus.urtuu), тенант өөрөө суулгана. Аппыг устгасан ч замд яваа ажил алга болохгүй: суваг нь доор нь үлдэнэ.

Доод тал л холбогдоно. Дээд тал доод руу хэзээ ч гарахгүй — доод суулгац галт ханын цаана, хувийн сүлжээнд, түр унтраатай байж болно. Доод тал long-poll-оор татаж, биелэлтээ түлхэнэ. Каталог sync яг ижил шалтгаанаар pull сонгосон.

Token нь "хэн ярьж байна", гарын үсэг нь "хэн бичсэн". Хоёр өөр асуулт, дугтуй бүр дээр хоёулаа асуугдана. Гарын үсэг нь pkg/catalog-ийн signed баримттай яг ижил хэлбэртэй (created_at + '\n' + түүхий payload), golden тесттэй. Дугтуйн payload нь өгөгдлийн санд JSONB биш TEXT-ээр хадгалагдана: JSONB нь түлхүүрийн дарааллыг эмхэлдэг тул хадгалаад буцааж уншихад гарын үсэг батлагдахаа болих байв.

Хоёр талын зөвшөөрөл, устгах биш цуцлах. Дээд тал урина (24 цаг, нэг удаагийн код) → доод тал буулгаж түлхүүр солилцоно → дээд тал баталгаажуулна. report_grants-ийн урсгал. Цуцлагдсан холбоос 401 буцаана, баталгаажаагүй нь 403 — цуцлагдсан тал нь өөрийгөө "хэзээ ч байгаагүй"-ээс ялгаж чадах ёсгүй.

Кодын бүртгэлийн стандартыг бид тогтоов. ring.dgov.mn-ий утасны форматыг хүлээхийн оронд түүнийг санал болгож бичив: docs/RING_STANDARD.md — гарын үсэгтэй баримт, ETag, Ed25519. Гарын үсгийн вход нь аппын каталог болон Өртөөгийн дугтуйтай ЯГ ИЖИЛ (generated_at + '\n' + түүхий байт): нэг дүрэм гурван газар, нэгийг нь уншсан хүн гурвуулангийнхыг ойлгоно. Хэрэгжилт нь бүрэн — нөхцөлт татах, гарын үсэг шалгах, хэмжээний хязгаар, нэг муу бичлэг бүх импортыг унагаахгүй.

Гурван зүйлийг ЗОРИУДААР хийгээгүй: диск кэш (импортлогдсон кодууд өгөгдлийн санд сууна — тэр нь өөрөө кэш), хуваарьт синк (код нь хугацааны нормтой ирдэг, норм нь ажилтны хэмжигдэх зүйлийг өөрчилдөг тул авах эсэх нь байгууллагын шийдвэр), гарын үсэггүй горим (RING_PUBLIC_KEY байхгүй бол импорт огт байхгүй — шалгагдаагүй бүртгэл нь улсын бүх суулгац дээрх нормыг өөрчлөх эрхтэй баримт).

Ring нь ЭРСДЭЛ БИШ, НЭМЭЛТ: RING_BASE_URL тохируулахгүй бол платформ бүрэн ажиллана — байгууллага local. кодоо зохиож, холбоос бүрд зарлана.

Даалгавар чөлөөт текстээр үүсэхгүй. Урьдчилан бүртгэгдсэн кодоор үүснэ: код нь юу бөглөхийг (JSON Schema) болон хэдий хугацаанд хийхийг (SLA) өөрөө хэлнэ. Эх сурвалж нь ring.dgov.mn — импортлогч интерфэйсийн цаана, бодит формат тохирогдоогүй тул parsing нь TODO хэвээр (таамгаар бичсэн parser өөрийн тестээ давж, бодит цэг дээр буруу байна). Дээд тал холбоос бүрд аль кодыг нээхээ шийдэж зарлана; локал код заавал local. угтвартай.

Мөчлөгөөс хамгаална. Даалгавар бүр дамжсан суулгацынхаа ID-г origin_chain-д авч явна. А→Б→А холбоос өөрөө хууль ёсны (хоёр яам өөр өөр төрлийн ажлаар бие биенийхээ дээд байж болно) — хамгаалагдах ёстой нь холбоос биш, даалгавар. Гинжинд өөрийн ID байвал шалтгаантай буцаана.

Хоёр шугам, хоёр амлалт. Суваг нь хоёр төрлийн ажил зөөнө. Үйлчилгээ: иргэн, байгууллагаас ирсэн хүсэлт доошоо явж, ХАРИУ нь заавал буцна — хүсэгч платформын гадна байгаа тул хариугүй хаагдсан хүсэлт гэдэг нь тэр хүний асуултыг зүгээр л алга болгосон хэрэг. Албан даалгавар: дээд байгууллага доод байгууллагадаа ажил өгнө, хүсэгч гэж байхгүй.

Хоёр дахь мөр нь Go-гийн шалгалт биш, СХЕМИЙН шалгалт: үйлчилгээний хүсэлт хариугүйгээр COMPLETED болж чадахгүй (urtuu_tasks_service_has_answer). Handler дээрх шалгалт нь хүнд ойлгомжтой өгүүлбэр буцаахын тулд; CHECK нь мартагдахгүйн тулд.

Шугам нь КОДООС ирнэ, үүсгэгч хүнээс биш: ring.dgov.mn-ээс импортлогдсон код бол төрийн үйлчилгээ, дотоод код бол даалгавар. Үүсгэгч сонгодог байсан бол нэг код хоёр өөр амлалттай хоёр газар хэрэглэгдэх байв. Задаргаа хийсэн хүсэлтийн хариу нь салбар бүрийн хариуг байгууллагын нэрээр нь нэрлэн эх мөр дээр цуглана.

Албан бичиг. Даалгаварт eID гарын үсэгтэй бичиг дагалдана. Дамждаг зүйл нь ЛАВЛАГАА — баримт нь бүртгэсэн байгууллагын Баримт бичиг апп дотор үлдэж, тэндээ гарын үсэг зурагдана; холбоосоор "ийм бичиг байгаа, ингэж нэрлэгдсэн, гарын үсэг хэд зурагдсан" гурав л явна.

Бүртгэлийн дугаар. Мөр бүр хүн уншихуйц дугаартай: Д2026-00412 (даалгавар), Ү2026-01875 (үйлчилгээ). Эхний тэмдэгт нь шугам — дугаарыг харангуут хариу заавал буцах эсэхийг мэднэ. Дараалал нь суулгац, шугам, он тус бүрд. Шат бүр ирсэн ажлыг өөрийн бүртгэлд дугаарлаж, илгээгчийн дугаарыг иш татна — цаасан бичиг яг ингэж ажилладаг.

Дугаарт платформын угтвар ЗОРИУДААР байхгүй: холбоосын мөр аль хэдийн name талбартай ("Ховд аймаг"), админ handshake хийхдээ өгсөн. Суулгац тусад нь богино код зарлавал нэг зүйлийн хоёр дахь, хэн ч баталгаажуулдаггүй нэр үүсэх байв. Дугаарыг нөгөө талын нэртэй нь хамт харуулна: Ховд аймаг · Д2026-0087.

Хяналт. urtuu_tasks_total{status}, urtuu_deliveries_total{result}, urtuu_peer_last_seen_seconds{role} — тенантын label аль нь ч дээр байхгүй. Операторын консол дээр «Өртөө» гэрэл: хүргэгдээгүй дугтуй, дуугүй холбоос. Оператор агуулгыг харахгүй — миграци 00064 нь зөвхөн urtuu_peers, urtuu_deliveries-ийг нээнэ.

Тайлан гурав: даалгаврын биелэлт, хугацаа хэтрэлт, сувгийн ачаалал. reports.schedule-тэй ажиллана. Гурвуулаа ScopeFull зарлана, ScopeCounterparty зарлахгүй: даалгаврын схемд counterparty_ref байхгүй бөгөөд холбоосыг түүн мэт үзвэл нэг байгууллагад өгсөн зөвшөөрөл чимээгүйгээр бүх доод платформын харагдац болох байв.

Миграцууд 00061–00067. Шинэ орчны хувьсагчид: URTUU_SIGNING_KEY (байхгүй бол Өртөө унтраалттай, платформ хэвийн асна), URTUU_ALLOW_INSECURE_PEERS, RING_BASE_URL, RING_API_KEY.

[1.7.0] - 2026-08-15

Changed — Contacts left for the commerce distribution, and the sidebar was rearranged

Three changes with one shape: putting each thing where it belongs.

The contact register is a product's, not a platform's. Migration 00059 folded Contacts into the Directory this morning, on the argument that who an organisation is made of and who it deals with are one subject. That was half right, and the wrong half is the half that decides where code lives: departments and staff are something every organisation has; customers are something a business has. So io.gerege.nexus.contacts is an app again — built and shipped by commerce-gerege-nexus — and io.gerege.nexus.organisation is the organisation again at 3.0.0, under the name it had before the merge.

The sidebar says where things are. Installed apps sat under Modules while its page is at /settings/apps, which asked somebody to hold two answers for where the same screen lives; it is under Settings now. The organisation moved the other way, up into Modules, because it is a thing you look at and edit rather than a switch that changes how the platform behaves — and its two screens, departments and people, are indented beneath it instead of listed beside the App Store as though the three were unrelated destinations.

[1.6.0] - 2026-08-15

Added — A distribution can read its own catalogue

catalog.LoadFile and the pieces under it — LoadEntries, Assemble, LoadManifest, LoadChronicleFile, ReleaseNotesFor — moved from internal/platform/appcatalog into the contract package.

The App Store found this the way these things are always found. A test wanting to check its bundled catalogue against the modules compiled beside it could not read the catalogue: the loader was in internal/, which is the rule that makes distributions possible, working exactly as designed and against the product it was designed for. The alternative was a second implementation of "how a catalogue is read" — a second answer to whether a manifest is valid, agreeing with the first until the day a deployment and the store it publishes to disagreed about an app nobody had touched.

The platform's own loader is now three lines over the public one, keeping the one thing that is genuinely this repository's: the deprecated app-id renames, which no distribution should inherit.

[1.5.0] - 2026-08-15

Added — Apps can be published to named platforms instead of to all of them

An app now declares a visibility: public, which every platform may be offered, or private, which only the platforms the registry names may be. Empty means public, so every manifest written before this marshals to the bytes it always did — the signed catalogue stays byte-reproducible across the two repositories that build it.

It is enforced by the registry, not by the platform, and that is the whole design rather than a shortcut. A private app is kept from a platform by not being in the catalogue that platform is served. The alternative — ship every platform the same document and ask each to hide what it should not see — leaks the names of private apps to everyone holding the catalogue, and asks the party with the motive to look to be the party that decides.

The other half of this lives in appstore-gerege-nexus: which platform may see which private app, and GET /catalog answering per deployment. One trap to carry over — the ETag has to vary per deployment too, or a platform whose entitlement is withdrawn keeps its old catalogue on a 304. See docs/ECOSYSTEM_GIT_STRATEGY.md §3.1.

Fixed — Installed apps listed four that this binary cannot run

The settings screen read straight from app_installations, so it showed nine rows under its own banner saying the catalogue has five. State Services, Products, Inventory and Billing were all listed as installed and active — months after their code left for gov-gerege-nexus and commerce-gerege-nexus — each with a button offering to disable an app that was not running. Nothing about them worked: no routes mounted, no menu entry, and the compile-time check refuses them at install. The row was all that was left, and a row that says "Active" about an app with no code is worse than no row.

The store already had this rule (runnableHere, added when State Services left); the list of what a tenant has never got it. Both screens ask the same question now: if the catalogue knows the app, it has to be runnable here; if the catalogue has never heard of it, a compiled module is enough — a distribution's own module is real from the moment the binary starts and may reach a catalogue minutes later or never.

The rows stay in the database. The apps went to distributions this deployment may yet run, and deleting an installation because a screen cannot render it is the wrong way round.

Changed — Two cards became one app: Contacts moved inside the Directory

"Organisation & People" and "Contacts" were one subject cut in half — who this organisation is made of, and who it deals with. An administrator who installed one and not the other had half a directory, and nothing in the store said which half was missing. So io.gerege.nexus.contacts is gone and its register is part of io.gerege.nexus.organisation, now called Directory (mn: Бүртгэл) at version 2.0.0.

The contacts table, the API path /api/v1/contacts and the screen at /contacts are all unchanged. What moved is the app the register belongs to.

Changed — Two cards became one app: PDF E-Sign moved inside Documents

The store carried "Digital Documents & Signatures" and "PDF E-Sign" side by side. They answered one question — where are my documents and who has signed them — and nobody adopts a signature on its own; they adopt it because something has to be signed. So io.gerege.nexus.esign is gone and its rails are part of io.gerege.nexus.documents, now called Documents (mn: Баримт бичиг) at version 2.0.0.

Nothing about how a document is signed changed. The eID and HSM rails, the signature log, batch signing, stamp placement, the HSM connection and the two reports are the same code reading the same tables.

Changed — The image no longer knows its own name

lib/apiBase.ts took the deployment's address out of the build. This takes its identity out, which is the second half of the same argument: one image, a different .env, a hundred deployments is only true when neither is baked in.

Five values are read from the environment per request — BRAND_NAME, BRAND_SHORT_NAME, BRAND_DESCRIPTION, BRAND_LOGO_URL, BRAND_THEME_COLOR — and all of them are optional. Unset is Gerege Nexus, so nothing about nexus.gerege.mn changes. They reach the document title, the PWA manifest (which is what an installed copy keeps under its icon), the header, the sign-in and consent screens, the workarea footer, the operator console and the landing page's chrome.

See docs/ECOSYSTEM_GIT_STRATEGY.md §2.3 and §6.

Removed — State Services left, and is a product of its own

The second distribution split. apps/gov_services now lives in gov-gerege-nexus.

No composition image, and nexus.gerege.mn simply stops offering the app. That was not the plan an hour before it shipped; the plan was a repository whose only job is to build core-plus-verticals into one image for this deployment. Production answered the question instead. It carries two tenants; the app is installed on one of them and disabled there; gov_services, gov_applications and gov_appointments hold zero rows between them. A repository, a pipeline and their maintenance, to keep showing an app nobody has switched on. The ecosystem strategy tells distributions to choose the lower level when in doubt (§1), and the rule reads the same when it is pointed at us.

The distribution exists and is green, so the day somebody wants State Services it is a deployment rather than a project.

[1.4.0] - 2026-08-15

Removed — Commerce left, and is a product of its own

The third and last of the planned vertical splits. apps/products, apps/inventory and apps/billing now live in commerce-gerege-nexus.

As with State Services, no composition image: production carries two tenants, and products, stock_levels and billing_invoices hold zero rows between them. A deployment that wants commerce runs the distribution.

[1.3.0] - 2026-08-15

Added — the reporting contract and the document capability, both so a module elsewhere can use them

Changed

[1.2.0] - 2026-08-15

Added — a module can now state its own access policy, and book a meeting

Both are the same discovery from two directions: the platform held things a module should have held, and a module could not be moved until it did.

gov_services now depends on pkg/nexus and nothing else from this repository, which is what makes the next split possible.

Removed — the App Store left, and is a product of its own

The first distribution split. apps/appstore_registry, apps/publisher_studio and apps/store_review now live in appstore-gerege-nexus, which takes this platform as a dependency by tag and adds nothing but three modules and the line that registers them. Every other deployment stopped carrying them as dead weight the day they left.

[1.1.0] - 2026-08-14

Added — pkg/platform, so a distribution can start the platform it compiles against

The SDK let somebody write a module and gave them nowhere to run it: booting is internal/platform.NewServer, which the language closes to every other repository. docs/ECOSYSTEM_GIT_STRATEGY.md §2.5 sketches a distribution's main.go as a call to platform.Run(); this is that function.

Added — pkg/catalog, the app-store contract, and a clean appstore boundary

Preparation for the first distribution split (gerege-appstore). The three store modules — the registry, the publisher studio and the review queue — were measured against the rest of the platform first, and the boundary turned out to be almost clean already: nothing in the core imports them, and the only thing holding them here was the catalogue schema, which lived in internal/platform/appcatalog where no other repository can reach it.

The three store modules now import pkg/nexus, pkg/catalog and each other, and nothing else from this repository. What still ties them here is three lines in internal/apps/runtime.go — the lines that become a distribution's main.go.

[1.0.0] - 2026-08-14

Эхний тогтвортой хувилбар: backend/pkg/nexus нь semver амлалттай нийтийн API болж, экосистемийн салгалтын 0-р (нэршил) ба 1-р (SDK) алхам дуусав. Энэ хувилбараас эхлэн distribution repo нь цөмийг fork хийхгүйгээр dependency болгон авч чадна — хувилбар гаргах журам.

Added — a release process, and the tests that make its promise checkable

Step 2 of the ecosystem split. A tag is the only way another repository can depend on this one, so a tag has to mean something; this is what makes it mean something.

Added — pkg/nexus, the SDK that makes a product possible without a fork

Step 1 of the ecosystem split (docs/ECOSYSTEM_GIT_STRATEGY.md §6), and the precondition for every step after it. The module contract lived in backend/internal/module.go, and Go forbids another repository from importing anything under internal/ — so the only way to build a product on this platform was to fork it, and one fork per product means every fix is applied once per product for ever.

Implementations stay in internal/. The SDK is a contract, and a contract that also carried the machinery would drag the machinery into the semver promise.

The service half followed, and with it §6 step 1 is complete: a module in another repository can now be written, not merely declared.

The surface was measured rather than designed. Across the fourteen modules the whole demand on the platform was: write a JSON response (420 call sites), name the organisation and the caller (94), refuse on a missing permission (24), record what happened (41), and query the database. The report engine (40 symbols), the catalogue, the state rails and the SSO provider are each a subsystem or a specialised rail, and none belongs in the first version of a contract that cannot be narrowed later. settings, flags and emailverify are in docs/ECOSYSTEM_GIT_STRATEGY.md §2.1's sketch of this package and are not here, because no module imports them.

Added — egov, the front door to the state's systems

The last of the three naming corrections, and the only one that creates a module rather than renaming one. The pieces existed and were scattered: the ХУР registry lookups were two handlers in the platform's own route table, whether the eID, ДАН and ХУР rails were even configured was knowable only from the deployment's environment, and what had been looked up sat in the audit log with nothing pointing at it.

What deliberately did not move into the app: the eID and ДАН sign-in flows, which run before anybody is signed in, and a person's own list of linked identities with the button that unlinks one. The second is the same reasoning profile_handlers.go has carried since before this module existed — an app is installed per organisation and an administrator can remove one, and somebody's ability to detach their own national identity is not their employer's to take away. The connections screen links to /profile rather than owning it.

Deprecated — to be removed in the next release

Changed — developer_portal becomes sso_clients

The second of the three naming corrections. developer_portal named the wrong thing twice: there is a real developer portal in this ecosystem — developer.gerege.mn, backed by apps/publisher_studio, where a third party submits an app to the store — and an administrator looking for that and landing here had nothing in the name to tell them they were in the wrong product. What this app actually is has no developers in it: CRUD over the OAuth2 clients registered against this platform's own OIDC provider, run by whoever looks after an organisation's integrations.

Deprecated — to be removed in the next release

Changed — core becomes organisation, and stops being undeletable

The first of three naming corrections made before the platform is published as an SDK, where a name becomes part of an import path and stops being cheap to change. core was the name of the app holding departments and people and the name of the platform underneath every app; one of the two had to give it up, and it is not the platform.

Deprecated — to be removed in the next release

Each of these is marked // DEPRECATED: remove in vNEXT at its definition.

Added — Control Plane Catalog Management & Migration Deprecation (CP-46)

Added — The break-glass account, and the storage limit that refuses

The last two things the design document asked for and the phases had not delivered.

Added — Counting what each organisation used, from the database rather than from the metrics

CP-5, the last phase of the control plane. usage_events holds one row per organisation per metric per day, written by a job that runs nightly and again during the day, so the console is never showing yesterday's picture to somebody looking at it after lunch.

Added — A front page that answers "is the platform well", and the platform's first backup

CP-4. The console's home screen is now the deployment's health — requests, errors, latency, the government systems' lights, what is alerting, the disk, which background jobs have quietly stopped, what version is running — and the organisation list moved to its own page. Every panel links into Grafana, and none of it tries to replace Grafana.

Added — A platform that can be told to behave differently, without a deploy

CP-3, and the setting it exists for: this platform is private by default. Until now, whether a stranger who could authenticate somewhere else became a user here was decided by which environment variables happened to be set — EID_JIT_TENANT_SLUG in one file, SSO_CLIENT_TENANT in another, read by two packages, with no single place that answered "can somebody get in".

Fixed

Added — Operating an organisation from the console, without being able to take it

CP-2 gives the console the buttons CP-1 deliberately withheld: creating an organisation, closing one, deleting one, helping somebody back into their account, and — with a reason and a banner — looking at the platform as they see it. Everything in it is shaped by one rule: an operator should be able to do their job without being able to do quiet damage.

Added — A console for operating the platform, kept away from the platform

Somebody has to be able to see which organisations exist, which apps they run and what has been done to them — and until now that somebody used psql. This is the first phase of the operator console described in docs/CONTROL_PLANE_PLAN.md: the foundation, on which suspension, support and configuration are built next. Guide in docs/CONTROL_PLANE.md.

Fixed

Added — One organisation seeing another's report, with their permission

A coal mine contracts a hundred transport companies. Each keeps its trips in its own tenant; the mine wants one consolidated "Transport" report. That request runs against everything this platform is built to prevent, so the answer is not to weaken the isolation but to add a separate, permissioned path beside it. §3.5 of the design; guide in docs/REPORT_SHARING.md.

Added — Reports, as a platform layer rather than a screen

io.gerege.nexus.reports. Every app that keeps data now has reports; the module serving them knows about none of them. Guide in docs/REPORTS.md.

Added — Traces, and errors that group themselves

The third pillar and the tool beside it, both env-gated and both off by default. Guides in docs/MONITORING.md §11 and §12.

Added — A monitoring stack that reads the platform

deploy/docker-compose.monitoring.yml: Prometheus, Alertmanager, Loki, Alloy, Grafana, node_exporter, cAdvisor, postgres_exporter, redis_exporter. Guides in docs/MONITORING.md and, for every alert, docs/RUNBOOKS.md.

Added — The platform can now be measured

/metrics carried two series: a request count and a request duration. That is the R and the D of RED and nothing else — no saturation, no business volume, no sign that a call to ХУР or eID had gone slow, and no way to tell a breach of the in-flight ceiling from any other 503. Everything a dashboard would need was missing before the dashboards were, which is why this lands before the stack that reads it (design: docs/MONITORING_AND_REPORTING_PROPOSAL.md).

Added — Signing in with Google

A "Google-ээр нэвтрэх" button beside eID on the platform's own sign-in screen, off unless GOOGLE_LOGIN_CLIENT_ID is set. It is an addition, not the federation added a moment ago: SSO_CLIENT_ISSUER closes this deployment's own sign-in paths and hands the question of who somebody is to a provider, while this is one more of its own answers and closes nothing. On a deployment that does federate, the button is withdrawn along with the rest — a front door nobody manages is exactly what federating was meant to remove.

Google is an ordinary OpenID Connect provider, so there is no second implementation: the same discovery, PKCE, code exchange and RS256 id_token verification serve both, and both land on the same (issuer, subject) account resolution. What is written separately is only what differs — which cookie the flow parks in, and who is allowed through.

Added — A deployment can now be an SSO client, not only a provider

The platform has always been an OpenID Connect provider: it could hand identities out and never take one in, so a group running several deployments had one sign-in per deployment and no way to make one of them the source of truth. This is the other half. Setting SSO_CLIENT_ISSUER makes a deployment a relying party of the provider named there — including of another Gerege Nexus — and the two halves are independent: an instance can be a provider, a client, or both, which is what a regional deployment federating upward while still issuing identities to its own installed apps needs. Full guide in docs/SSO_FEDERATION.md.

Added — RP-initiated logout at the provider (/oauth2/logout)

The discovery document has advertised end_session_endpoint since it was written, and nothing served it: a relying party that ended its own session and sent the person here — which is what a conformant client does — landed on a 404 while staying signed in. That is worse than not advertising it, because the next click on "sign in" looks like the logout was ignored.

Fixed — Two defects the new tests turned up

Without this, single sign-on is not single. A relying party signing somebody in sends the browser to /oauth2/auth, which is a top-level navigation arriving from another site, and a Strict cookie is not sent on one — so the authorization endpoint saw no session and showed a login screen to somebody who had signed in a minute earlier. It costs nothing in CSRF terms, because the cookie was never the defence: Lax adds exactly one thing over Strict, a cross-site top-level GET, and every state-changing request goes through security.CSRFMiddleware, which demands positive evidence that a page of ours made it.

Fixed — A lockout that never let go, and three silent truncations

Removed — Code that had stopped being reachable

Changed — The platform's apps stop calling themselves examples

io.example.* was placeholder vocabulary from the first week — the reverse domain of nobody, borrowed the way example.com is borrowed — and it had been the primary key of every app in the store ever since. These are Gerege Nexus's own apps and they now say so: io.gerege.nexus.*.

Added — An organisation to be about

The module Odoo calls base, as a core app: the organisation itself, the people in it, and how it is arranged. The platform had tenants, users and memberships carrying only what signing somebody in needs — a slug, a name, an email. A document that has to print a registration number, an approval that has to name a department, a deadline counted in some timezone: none of those had anywhere to come from, so each app either invented its own or went without.

Added — The App Store moved to appstore.gerege.mn

The catalogue now comes from a registry of its own, and the apps in it can be published by people who do not work here.

Added — Preparing the App Store to live at appstore.gerege.mn

The catalogue is on its way out of this repository and into a registry of its own (docs/APPSTORE_SEPARATION_PLAN.md). Everything here works today in file mode, which stays the default and the whole story for a self-hosted deployment; the registry is opt-in and this platform never depends on it.

Added — Switching between the organisations you belong to

Removed — The Swift macOS client (desktop-mac/)

Removed — the Swift macOS shell, in favour of one shell for all platforms

Fixed — the Tauri shell's bridge was dead on arrival

Found by running the app and signing in — none of it was visible to cargo build, clippy -D warnings, cargo test, or the three-platform CI, all of which stayed green throughout.

Added — Tauri v2 desktop shell (desktop-tauri/)

Added — CI for both desktop shells

Added — Native Shell + Web Work Area

Fixed — Security: the macOS shell's JavaScript bridge

Added — Email verification as a platform capability

Added — PDF E-Sign v2: eID Mongolia qualified remote signing

Fixed — PDF E-Sign

Added

Fixed — CI/CD pipeline

Fixed — Security

Fixed — App store & modules

Fixed — Reliability


[0.1.0] - 2026-08-05

Added


Inspirations & Acknowledgements

Authors & Contributors