This walkthrough shows how to build, deploy, and operate the Notion-backed logging app in this repo. It is based on the commit log, README.md, and PLAN.md, and it explains why the architecture evolves as features are added.
You will end up with three Val Town scripts:
- A weekly blood sugar report that emails a summary and persists rollups to SQLite.
- A weekly food log enricher that estimates macros with OpenAI and writes them back to Notion.
- A monthly report page that renders HTML from the stored weekly rollups.
The design is intentionally layered so the system stays fast and predictable as data grows.
The code is split by responsibility:
collectors/: scheduled jobs that pull the last 7 days of Notion data and compute a weekly rollup.shared/: reusable parsing and rollup logic shared across collectors.storage/: SQLite persistence so reports do not have to re-scan Notion.services/: HTTP endpoints for reporting (monthly report page) and optional rollup ingestion.
Why this architecture works as the app grows:
- Short Notion queries stay fast. Each collector only asks Notion for a one-week window. This keeps API calls cheap and predictable even as your Notion database grows.
- Rollups make reporting cheap. Once a weekly rollup is computed, it is stored in SQLite (
weekly_rollups_1). Monthly pages and future yearly summaries can use SQLite instead of Notion. - Idempotent writes avoid duplicates. Rollups use a deterministic
runId(for exampleblood_sugar-YYYY-MM-DD-YYYY-MM-DD) and SQLite has a unique constraint onrun_id, so re-running a week does not double-count. - Collectors stay focused. Each collector normalizes its own Notion database, then hands off common logic to
shared/and storage tostorage/. - HTTP services scale reporting. The monthly report page is just a read-only HTTP val that reads SQLite and renders HTML. This keeps interactive reporting separate from scheduled ingestion.
Create two Notion databases (table view recommended).
Properties (exact names expected):
Entry(Title)Created Time(Created time) orCreated Time(Text)Measurement Date(Date)Blood Sugar Level(Number)
Properties (exact names expected):
food(Title or Text)Created time(Created time)calories(Number)protein(Number)carbs(Number)fats(Number)fiber(Number)sugar(Number)sodium(Number)
Only food and Created time are required to read; the rest will be written by the food enricher.
- Go to https://www.notion.so/my-integrations and create a new integration.
- Copy the Internal Integration Token.
- Open each database, click Share, and invite the integration.
- Copy each database ID (the 32-character ID in the URL).
Create vals for each script:
vt create blood_sugar_report vt create food_report vt create monthly_report_page
Replace the contents of the new vals with these repo files:
collectors/blood_sugar_report.cron.tsxcollectors/food_report.cron.tsxservices/monthly_report_page.http.tsx
Push everything:
vt push
Shared:
NOTION_TOKEN
Blood sugar report:
NOTION_BLOOD_SUGAR_DB_ID
Food report:
NOTION_FOOD_DB_ID
Notes:
- On the free tier, the blood sugar report always emails the account owner; sender options are not configurable.
- The food val uses Val Town's
std/openaiproxy withgpt-5-nano, so you do not need an OpenAI API key.
Schedule both collectors in Val Town. A typical schedule is Tuesday at 9:30am EST.
30 9 * * 2
Cron fields:
minute hour day-of-month month day-of-week
- Blood sugar val should send an email and write a rollup row to SQLite.
- Food val should enrich any missing macros and write a rollup row to SQLite.
- Monthly report page should render without errors.
If you need to manually trigger a week, run the vals once in Val Town. The rollup table is idempotent because run_id is unique.
- Weekly rollups:
storage/rollups.tsstores them inweekly_rollups_1. - Monthly report page:
services/monthly_report_page.http.tsxreads from SQLite and renders the month.
The monthly report page supports query parameters:
?month=YYYY-MM(defaults to previous month)?partial=trueto include weeks that overlap the month boundary
The project grew in stages:
- Weekly Notion email: the first goal was a friendly weekly report without heavy infrastructure.
- Tests and logic extraction: core rollup logic moved into
shared/with tests, keeping collectors small. - Macro enrichment: a second collector was added for food logs, reusing the same weekly-window approach.
- SQLite persistence: weekly rollups were persisted so monthly/annual summaries could be built without re-reading Notion.
- Monthly report page: an HTTP val renders a full HTML report from SQLite data.
Each step keeps the system simple while expanding capabilities:
- We never need to scan all of Notion.
- Scheduled jobs do a single week of work.
- Reports are built from stored rollups, not raw entries.
To add a new tracker:
- Create a new Notion database and define its properties.
- Add a new collector val in
collectors/that:- pulls a weekly window from Notion,
- maps it into a shared rollup format,
- persists to SQLite using
upsertWeeklyRollup.
- Add new aggregations in
shared/and extend the monthly report page if desired.
This keeps the system consistent and avoids one-off logic when more categories are added.
Run tests:
deno test --allow-import --reporter=dot
Run lint:
deno lint
