Ovumcy
Privacy-first, self-hosted menstrual cycle tracker
Alternative to: flo, clue

v1.8.0
2026-07-11Adds three opt-in notification/subscription surfaces (webhook reminders, an in-process daily reminder scheduler, and a read-only calendar .ics feed), per-user timezone and week-start preferences, Italian localization, and an OIDC response-mode switch, alongside auth hardening. Six additive database migrations (026–031, SQLite and Postgres). The only breaking change is the swelling export-shape change noted under Changed.
Added
- Webhook reminder notifications (opt-in, off by default). A new per-owner webhook subsystem delivers period/ovulation reminders to an owner-configured endpoint (migration
027). The URL is a write-only secret — encrypted at rest and never rendered back into the settings page (only a configured/not-configured status and at most the hostname are shown). Delivery is request-free and owner-scoped — one owner’s predicted health data never reaches another owner’s endpoint — and the outbound envelope is hardened: bounded timeout, no keep-alives, zero redirects,http/httpsonly, and a size-capped response read. Operators can opt into an off-by-default private-address block (WEBHOOK_BLOCK_PRIVATE_ADDRESSES) that resolves the host and refuses private/loopback/link-local targets — including RFC 6598 CGNAT (100.64.0.0/10) and the RFC 6052 NAT64 prefix (64:ff9b::/96) — and dials the validated IP to defeat DNS rebinding. Configurable from Settings or theovumcy webhook <show|set>operator command. (#124) - Built-in daily reminder scheduler (opt-in, off by default). An optional in-process pass (
REMINDER_SCHEDULER_ENABLED, defaultfalse;REMINDER_SCHEDULER_HOUR, default9, in the server’sTZ) evaluates upcoming period/ovulation reminders once a day. When disabled, no scheduler goroutine, timer, or outbound component exists in the running process at all. It is backed by a minimal app-level key/value store (migration028) that holds process runtime markers only — never per-owner health data. A per-owner “reminder lead days” control (0–14) and an in-app dashboard reminder banner surface the same window in the UI. (#123, #125) - Read-only calendar
.icsfeed (opt-in). Each owner can generate an opaque, tokenized.icssubscription URL for external calendar clients. The feed token is stored hashed (migration029), has an explicit lifecycle in Settings, is one-click-revocable, and is force-cleared when the recovery code is regenerated.VEVENTUIDs are a stable function of(kind, date), and every feed carries the medical-safety disclaimer. (#181, #176, #184) - Per-user timezone. An account’s timezone is persisted from the request (migration
026) and used for DST-safe cycle-day counting, predictions, reminders, and the.icsfeed. (#159) - Per-user week-start preference. The calendar grid and weekday header can start on Sunday or Monday (migration
030, valuessunday/monday). (#225) - Italian (it) localization. Italian is added to the supported languages and the required-locale boot guard, with server-rendered dates localized. (#218)
OIDC_RESPONSE_MODEsetting. Selects the OIDC authorization response mode; defaults toform_post, withqueryavailable as an explicit opt-in (the query-mode Authorization Code is inert without the PKCE verifier held in the sealed state cookie). (#210)
Changed
-
Removed the local pre-push patch-coverage hook.
scripts/hooks/pre-push,scripts/setup-hooks.sh, andscripts/patch-coverage-local.share removed — the hook reran the full test suite on every push with a*.gochange, which madegit pushslow. Patch coverage is still enforced, exclusively by CI’spatch-coveragejob.scripts/patchcov(the gate CI calls directly) is unchanged. -
Breaking (export shape): the built-in
swellingsymptom (added 2026-03-09) now has its ownswellingboolean flag in the JSONsymptomsobject and its ownSwellingCSV column, instead of falling through toother_symptoms/Otherindistinguishably from an owner-created custom symptom. TheSwellingCSV column is inserted afterConstipationto stay adjacent to the other symptom columns, so every column fromCycle factorsonward shifts one position to the right in files generated after this change; consumers reading CSV columns by position (not by header name) must account for the shift.docs/export.mdanddocs/openapi.yamlupdated; import accepts both the new flag and legacy files that still carryswellingonly viaother_symptoms.
Fixed
- SQLite writes now open with
BEGIN IMMEDIATE, ending intermittentSQLITE_BUSY500s when two writes raced for the write lock. (#155) - Next-period projection uses the median (not mean) cycle length consistently across the dashboard, webhook reminders, and the
.icsfeed, and calendar-day differences use a DST-safeCalendarDaysBetween. (#213, #212) - Calendar cycle-start confirmation modal now opens in the summary view. (#238)
show_historical_phasesis reset when an owner clears their data. (#229)- Day-note trimming no longer splits multi-byte UTF-8 runes. (#222)
- The request-free webhook
notifypass sets the pass user’sRoleso the cycle baseline applies, and parsesWEBHOOK_BLOCK_PRIVATE_ADDRESSESconsistently on that path. (#214, #215) - Accessibility: the medical disclaimer renders on the stats and calendar pages; mobile touch targets, tab-bar height, and heading order were tightened; and the language pills stay on a single line down to 390px.
- Postgres example stacks (
docs/examples/postgres,caddy-postgres,nginx-postgres) mounted the data volume at/var/lib/postgresql/data, which thepostgres:18image no longer uses as its data directory — database contents ended up in an anonymous volume and were lost when the container was recreated. The examples now mountpostgres_data:/var/lib/postgresql. (#200)
Migration for existing deployments: if you deployed from one of these examples, dump your database while the old container is still running, before pulling this change: docker compose exec postgres pg_dumpall -U $POSTGRES_USER > backup.sql. Then apply the updated compose file, recreate the stack (docker compose down && docker compose up -d), and restore: docker compose exec -T postgres psql -U $POSTGRES_USER -d postgres < backup.sql. If the container was already recreated and the database looks empty, your data may still be in an orphaned anonymous volume — list candidates with docker volume ls and inspect them before deleting anything.
Security
- Go toolchain bumped to 1.26.5. Clears GO-2026-5856 (Encrypted Client Hello privacy leak in
crypto/tls, fixed upstream in go1.26.5), reachable via the outbound HTTP client used for webhook delivery and OIDC. The runtime image’s builder stage moves togolang:1.26.5-alpine3.24(thealpine3.22variant of this patch release was not published); the finalalpine:3.24.1runtime-assets stage is unchanged. - Daily-log writes are scoped to
user_id, tightening cross-owner isolation on the write path so a write can only land on the session owner’s rows. (#239) - Symptom writes are scoped to
user_id.SymptomRepository.Updatemoved off a primary-key-only save to the sameuser_id-scoped update path as daily logs, so a mutatedUserIDcan never reassign or overwrite another owner’s symptom row (defense-in-depth; callers already load viaFindByIDForUser). (#244) - Account erasure now clears
oidc_logout_states. These rows previously carried nouser_id, so account deletion left them to age out via their ~7-day TTL — a right-to-erasure gap. A nullableuser_idcolumn (migration031) is populated on the OIDC login-callback and session-rotation paths and deleted inside the account-erasure transaction; rows minted before031keep aNULLuser_idand still age out via TTL. (#244) - bcrypt work factor raised to cost 12, with stale lower-cost hashes transparently rehashed on the next successful login; the timing-equalization placeholder hashes are aligned to the same cost so login timing does not distinguish existing from missing accounts.
Internal
-
go-118-fuzz-buildpinned instead of floating on@latestin.clusterfuzzlite/build.sh(OpenSSF Scorecard “Pinned-Dependencies”). The module has no tagged releases, so it’s pinned to a pseudo-version (v0.0.0-20250520111509-a70c2aa677fa) instead — same effect as a commit-SHA pin..clusterfuzzlite/Dockerfile’s unpinnedbase-builder-gobase image is unchanged (documented exception, now cross-referenced correctly indocs/SECURITY_INVARIANTS.md → CI). -
Continuous fuzzing via ClusterFuzzLite on GitHub Actions, a
gitleakssecret-scanning gate with a fixture allowlist, and agolangci-lintv2 configuration with curated linters were added; the unreachable GO-2026-5932 (x/crypto/openpgp) is suppressed viaosv-scanner.toml. -
CI wall-clock reduced without changing coverage. The
testjob splits into paralleltest-go(staticcheck, golangci-lint, vet,go test+ coverage) andtest-frontend(lint, unit tests, build, stale-bundle guard) jobs — they never depended on each other’s output, so running them in parallel instead of as sequential steps in one job cuts wall-clock roughly in half. A thintestjob (needs: [test-go, test-frontend]) keeps the branch-protection required-check name intact.e2e,e2e-postgres-smoke, ande2e-cross-browsernow cache Playwright’s downloaded browser binaries (~/.cache/ms-playwright, keyed bypackage-lock.json) across runs; on a cache hit only the (fast, apt-based) OS dependencies are reinstalled, skipping the multi-hundred-MB browser download.golangci-lint’s own analysis cache (~/.cache/golangci-lint, previously uncached) is now restored across runs too, as isnpm’s package cache ine2e/e2e-postgres-smoke/e2e-cross-browser(test-frontendalready had it; the three e2e jobs’ ownnpm cicalls didn’t).ci.yml/security.ymlgained aconcurrencygroup so a rapid string of fixup pushes on the same PR cancels superseded runs instead of queuing every one to completion (push tomain/release/workflow_dispatchruns are never cancelled). Every check still runs the same commands against the same code — nothing skipped, nothing narrowed. -
Redundant dashboard
daily_logs/symptom_typesreads were removed on the dashboard render path, and the cycle-prediction multi-bool tuples were replaced with named structs.
Dependencies
- The Go
go-minor-patchgroup, the GitHub Actions group, thecompose-imagesgroup,eslint/npm dev-dependency minor-patch bumps, and a Tailwind CSS 4.3.2 bundle rebuild.