Case study · Web app

Ayİzi

("ay" means month, "iz" means trace)

A private cycle tracker for one person. Logging takes one tap, reminders arrive by email, and the data stays in one Google account.

  • Google Apps Script
  • Google Sheets
  • Gmail
  • Plain JavaScript
Home screen on a calm day: a cover picture, a countdown ring saying 4 days left and a Started today button
Home screen on a period day: day 3 ring, flow and pain buttons and a calendar
Cycle summary sheet with average cycle length, range, pain and the last four cycles
75automated tests, all passing
18versions, 3.0.0 to 3.7.1
18days between the first and the last of them
0server calls to outside services

Why I built it

Tracking apps want your data. This one does not

Most cycle apps ask for an account, store health data on someone else's servers and push ads or upsells. I wanted something small for one person, on the phone home screen, where the data sits in a spreadsheet that belongs to her.

So it runs on Google Apps Script. It costs nothing, needs no server of my own and keeps everything inside one Google account. The rules I set: ready in one tap, no third parties, and health details never appear where other people can see them.

Today

One tap, then close the app

The app is opened rarely, so the main card does the job. On a period day it asks two questions with three buttons each: how heavy the flow is and how strong the pain is. One tap saves it to today.

  • When no period is on, the card shows a countdown to the estimated start and a single Started today button
  • Pain and flow buttons write to today's record without opening a form
  • A wrongly entered date can be fixed later from the list of past cycles

The app is in Turkish. "Regl sürüyor" = period ongoing. "Hafif / Orta / Yoğun" = light / medium / heavy. "Ağrı" = pain. "Bugün başladı" = started today.

Period day card with flow and pain buttons
A period day
Calm day card with a countdown to the estimated start
A calm day

Calendar

Estimates, labelled as estimates

The calendar colours logged period days, estimated days and estimated fertile days. Estimates come from the average of the logged cycles, with a margin of a few days written next to them.

  • Fertile days carry a line next to them: a rough guess, not to be relied on for contraception
  • A "back to today" button appears when you page to another month
  • Every day is at least 44 px tall, so it is easy to hit on a small phone

"Regl" = period. "Tahmin" = estimate. "Doğurgan" = fertile. "Bugün" = today.

Calendar for October with period days in rose, fertile days in green and a note that they are a rough guess
Estimates and a plain warning

Report card

One chart, with the numbers written on it

The cycle report card shows the average cycle length and the last period length. Under it a single chart has one bar per cycle: the bar is the cycle length, the rose part at the bottom is the period.

  • The value is written above each bar, so no colour has to carry the meaning
  • Tapping a bar shows the details
  • Most frequent complaints appear as small tags under the chart

"Döngü karnesi" = cycle report card. "Geçmiş turlar" = past cycles. "Şikâyet" = complaint.

Cycle report card with average cycle 30 days, last period 5 days, a bar chart of four cycles and a list of past cycles
The report card

Summary

An answer ready for the doctor's question

At an appointment the questions are always the same: how often does it come, how long does it last, how bad is the pain. The cycle summary answers them on one sheet, calculated on the phone from the records.

  • Numbers only. The sheet says in writing that it contains no interpretation or diagnosis
  • Sharing is the user's own action, with a screenshot or Safari's print sheet, and a warning says the information goes to whoever receives it
  • Pain and complaints are counted only from finished cycles

"Döngü özeti" = cycle summary. "Ort. döngü" = average cycle. "Yoğun akışlı gün" = heavy-flow days.

Cycle summary sheet in the light theme
Light
The same sheet in the warm dark theme
Evening theme

Privacy

Locked, and quiet on the lock screen

Opening the link shows only a PIN screen. The PIN lives on the server and never reaches the page. Reminders go out by email, and by default the email says almost nothing.

  • After the sixth digit the app signs in by itself. Only the number of digits is sent to the page, never the PIN
  • Too many wrong tries lock the screen for a while
  • The default reminder reads "Ayİzi" with a one-line text. Day numbers and complaints appear only if she turns details on

"PIN'ini gir, devam edelim" = enter your PIN, let's go on. "Gmail ile hatırlat" = remind by Gmail.

PIN screen with a flower mark and one input
The only thing a stranger sees
Settings sheet with a Gmail reminder option and a sign-out button
Settings

Lessons from real bugs

What actually went wrong

Deployment

A half-pasted file poured code onto the screen

Apps Script has no upload. The page file is pasted into an editor by hand. One paste stopped in the middle, and the live app showed raw JavaScript instead of the screen.

The fix: the page file now ends with a marker and carries its own version number. If either is missing, the app says it is updating instead of showing code, and a health check reports the half file and any version mismatch.

If a manual step can fail silently, make the failure detectable.

Email

A flower became question marks

The test email arrived in another mail provider with "??????" in the subject. The Gmail service of Apps Script writes four-byte characters such as emoji badly. Turkish letters were fine.

The fix: subject and plain text are stripped of emoji before sending, and the flower in the HTML part is written as a numeric code that every mail program understands.

Test with the receiver's mail program, not only your own.

Cache

A footer that blamed the wrong file

The settings footer said the screen was out of date. It was not. The server had kept a data package written by the previous version, and for up to an hour the new screen met old fields.

The fix: the version number is part of the cache key, so a new release never reads a package prepared by old code. The footer now also says which of the two files is the old one.

Put the version into every cache key.

Speed

"A bit slow" could not be measured from outside

The app felt slow to open, but the real delay inside Apps Script cannot be measured from a laptop. Instead of guessing, I followed the chain of waits in the code: start the data call before the page is drawn, read the settings in one call, skip needless compression.

To get real numbers from the phone, the settings sheet now shows one line: total open time, time spent on the server and where the data came from. It holds no health data.

When you cannot measure it, build the measurement in.

Under the hood

The parts that are not on screen

A free stack

One server file and one page file, with Google Sheets as the database, Google's cache for speed and Gmail for reminders. Nothing is paid for, and the server code makes no call to any outside service.

Security on the server

The PIN is stored on the server. A correct PIN returns a 90-day session key, and the server keeps only its SHA-256 digest, for at most five sessions. Every write is checked on the server, runs under a lock and carries a request id, so a repeated tap never writes twice.

No health data in logs

The operations log records the action and the result, never flow, pain, complaints or notes. Dates inside error messages are masked. Text written to a cell is neutralised against spreadsheet formula injection.

Data stays on the server

The phone stores only the session key. A copy of the data on the phone would make opening faster, but a lost phone would then show old data even after the PIN is changed. I chose safety over speed. For the same reason the app never moved to a hosted database.

Tests without Google

Apps Script does not run locally. 75 tests run in Node against hand-made fakes of the spreadsheet, cache, lock and Gmail services. Calculations on the page sit in one marked block that tests load on their own. A browser script checks sideways overflow and touch-target size at 375 and 390 px, in light and dark.

Design rules

Colours are defined once as variables, in a warm light theme and a warm medium-dark one that avoids pure black. Text contrast is checked by a test. The one entrance animation plays once and is skipped when the phone asks for reduced motion.

Timeline

September to 28 October, in three steps

  1. September to early October 2026The first versions: security fixes, Gmail reminders, a cache and a faster opening.
  2. 11 OctoberVersions 3.0.0 to 3.5.4 in one day: a calmer design, one-tap logging, PIN sign-in, automatic sign-in on the last digit, private reminders and in-app confirmation sheets.
  3. 20 and 28 OctoberA simpler home screen, a faster opening, the cycle summary, and a repository cleanup that found no secret or real data.

About

Who did what

Product decisions, the rules and testing on a real phone are mine. The code was written together with Claude.

All screens on this page use invented dates and entries. The app is made for one person and holds private health data, so it is not public. The interface is in Turkish, and the labels are explained next to each screen. The page carries no medical advice.