Date: April 10, 2026 Phase: Phase 8 (Testing & Bug Fixes) — Continued Previous Log: Day 11 (FCM Integration, Notification Action Buttons, Undo, Inactive Schedule Cleanup)


✅ Session Progress

Task Description Status
QStash Auto-Scheduling Auto register/remove QStash cron on medication create/update/delete/toggle ✅ Done
KST → UTC Conversion Convert scheduled_time (KST) to QStash cron expression (UTC) ✅ Done
QStash JWT Signature Verification Fix webhook signature verification from HMAC to JWT ✅ Done

1. QStash Auto-Scheduling

Background

Previously, push notifications had to be triggered manually. Introduced QStash (Upstash) cron-based scheduling to automatically send push notifications at medication times.

Implementation

New file: app/services/qstash_service.py

medication_service.py Changes

Converted sync methods to async and added QStash sync logic to each CRUD operation:

async def create(self, user_id: str, req: MedicationCreate) -> dict:
    # ... save medication + schedules to DB ...
    await self._qstash.sync_schedules(medication_id, user_id)

async def update(self, medication_id: str, user_id: str, req: MedicationUpdate) -> dict:
    # ... soft delete old schedules + insert new ones ...
    await self._qstash.sync_schedules(medication_id, user_id)

async def delete(self, medication_id: str, user_id: str) -> None:
    await self._qstash.delete_all_for_medication(medication_id)
    # ... soft delete medication ...

async def toggle(self, medication_id: str, user_id: str) -> dict:
    if new_state:
        await self._qstash.sync_schedules(medication_id, user_id)
    else:
        await self._qstash.delete_all_for_medication(medication_id)

DB Migration

ALTER TABLE schedules ADD COLUMN IF NOT EXISTS qstash_schedule_id TEXT DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_schedules_qstash_id ON schedules (qstash_schedule_id)
WHERE qstash_schedule_id IS NOT NULL;

Flow