TremorZen
אפליקציית בריאות מלאה לiOS ואנדרואיד עם שרתי ענן ובינה מלאכותית גנרטיבית: הקמת פרופיל קולית, צ׳אט שיחתי בזמן אמת, ניתוחים חודשיים וסנכרון מקומי.
13 פרקים · ~15 דק׳ קריאה
הפרוייקט מלווה מטופלי רעד מהותי שזקוקים למעקב יומי אחרי תרופות, בדיקות מוטוריות וטריגרים התנהגותיים. המוצר מספק חוויית "מטפל אישי בכיס": כל אינטראקציה היא בשיחה טבעית, רוב הזמן בקול בלבד, וכל מה שנאמר נשמר, מנותח ומתורגם לתובנות קליניות.
בניתי את הפרויקט מההתחלה ועד הסוף: צד הלקוח, צד השרת, אינטגרציות בינה מלאכותית, אבטחה ופריסה.
הנדסת ההקשר עבור הבינה המלאכותית
איך לבחור מה להגיש למודל בכל בקשה
מודלי בינה מלאכותית גנרטיביים הם כלי אוניברסלי שאיננו מכיר את המשתמש הספציפי, את הרקע הרפואי שלו, את התרופות שלו, או את ההיסטוריה של השיחה. הנדסת הקשר היא האומנות של לבחור מה להגיש למודל בכל בקשה: באיזה סדר, באיזה פורמט, ומה להשאיר בחוץ. זה ההבדל בין מודל שמרגיש "כללי" לבין מודל שמרגיש שהוא ממש מכיר אותך.
המערכת מנהלת עשרה פרומפטים שונים בשמונה מצבי פעולה. כל מצב (הקמת פרופיל, צ׳אט יומיומי, יומן בוקר, הערכה רבעונית, סיכום חודשי, חיפוש רפואי) מקבל פרומפט מותאם עם בדיוק המידע הדרוש לו. בנוסף, לכל בקשה מורכב דינמית "הקשר משתמש": פרופיל, תרופות פעילות, מצב יומן נוכחי, היסטוריית שיחה. כל זה נשלח למודל לפני הבקשה עצמה. המערכת אף מאפשרת לאפליקציה לשלוח את ההקשר מראש מהמכשיר, וחוסכת בכך שלוש קריאות לבסיס הנתונים בנתיב החם של כל הודעה.
בינה מלאכותית שמבצעת פעולות אמיתיות
שימוש של המודל בכלים
מודל שיחה רגיל יכול רק לדבר, לא לעשות. שימוש בכלים הוא היכולת של המודל להחליט בעצמו: "כדי לענות לשאלה הזאת, אני צריך לקרוא את היומן של המשתמש", "צריך לשמור את התרופה החדשה שלו", "צריך לחפש מידע על תופעות לוואי". המודל לא רק יודע מה להגיד, הוא יודע מה לעשות, מתי לעשות זאת, ובאיזה סדר.
למנוע השיחה יש גישה לעשרים ושניים כלים שונים, מקובצים לשש קטגוריות: שליפת נתונים, שמירה, ניווט בין מסכים, חיפוש רפואי, זיכרון שיחה, ומידע. כשמשתמש שואל "מה לקחתי השבוע?", המודל מחליט לקרוא לכלי שליפת היומנים, מקבל את הנתונים, ואז בקריאה שנייה ממיר אותם לתגובה טבעית. כל זה קורה תוך פחות משלוש שניות, בלי שהמשתמש יודע שהיו שלוש קריאות שונות ברקע.
ביצוע פעולות מקומי על המכשיר במקום בשרת
לא כל פעולה שהמודל מחליט עליה צריכה להתבצע בשרת. בניתי מערכת תיווך בצד הלקוח שמקבלת מהשרת את תיאור הפעולה שהמודל בחר, ומריצה אותה ישירות על המכשיר. למשל, כשהמודל מחליט "המשתמש שאל איך להתחיל בדיקה, אני אעביר אותו למסך הבדיקות", השרת לא עושה כלום מבחינה פיזית. הוא פשוט מחזיר בתגובה "פעולה: נווט למסך הבדיקות", והלקוח מבצע. אותו עיקרון לכתיבות אופטימיסטיות מקומיות, עדכוני ממשק, פתיחת חלונות בהקשר השיחה, ושמירות שעוברות גם דרך מסד הנתונים המקומי.
היתרון הוא כפול. ראשית, פחות סבבי תקשורת. פעולה שהייתה דורשת שלוש קפיצות רשת (שאלת המשתמש ← שרת ← תשובה ← בקשת ניווט ← שרת ← אישור ← ניווט בפועל) מבוצעת בשתיים. שנית, פעולות מסוימות חייבות להיות בלקוח: אי אפשר לנווט מסך מרחוק, ואי אפשר להציג חלון מרחוק. במקום מנגנון יציאה מוזר שבו השרת "מסמן ברקע שגם צריך לנווט", בניתי מנגנון אחיד שבו כל פעולה היא פעולה. חלקן מתבצעות בשרת (כתיבה לבסיס נתונים בענן, קריאה למודל), חלקן בלקוח (ניווט, ממשק, מסד מקומי), וההפצה אוטומטית לפי סוג הפעולה. כל פעולה גם עוברת ולידציה בצד הלקוח לפני ביצוע, עם סכמה ברורה של אילו פעולות מותרות עם אילו פרמטרים, כדי שמודל שמתבלבל לא יוכל לשבור את האפליקציה.
ניתוח השוואתי בין משתמשים בקנה מידה רחב
המלצות מבוססות עדויות שמכלילות מהניסיון של מיליונים
יש הבדל עצום בין מודל שיודע לענות למשתמש על השאלות שלו, לבין מודל שיודע ללמוד מהניסיון של מיליוני משתמשים אחרים ולהמליץ למשתמש החדש מה כדאי לו. אם שנים עשר אלף משתמשים עם פרופיל דומה לשלך הפסיקו קפה וראו שיפור משמעותי בחומרת הרעד, זה מידע חזק שהמודל לא יכול לגזור מהשיחה שלך לבדה. כדי לעשות את הקפיצה הזאת, צריך תשתית שיודעת לעבד מיליוני שורות נתונים ולחפש דפוסים.
עיצבתי את מבנה הנתונים מההתחלה כך שיהיה מוכן לקנה המידה הזה, אפילו לפני שהמשתמשים הראשונים נכנסו. סיכומים חודשיים מצרפיים, אוספים נפרדים לכל סוג בדיקה, מזהים מובנים על אירועי התנהגות, וכל אלה לא רק עוזרים למודל לענות מהר על משתמש בודד, אלא גם הופכים את כל המסד לבר שאילתה רוחבית בין משתמשים. הנתונים מוזרמים אוטומטית למחסן נתונים אנליטי בענן, אחרי שעברו אנונימיזציה והסרת מזהים אישיים. השירות הזה מיועד בדיוק לסוג הזה של ניתוחים, ויודע להריץ שאילתות על מיליארדי שורות בשניות במקום בשעות.
זיהוי דפוסים בין משתמשים
שאלות כמו "מצא את כל המשתמשים עם פרופיל דומה לפלוני (גיל, חומרה, תרופות), שאצלם חומרת הרעד ירדה ביותר מחמישה עשר אחוז בשישה חודשים, ובדוק אילו שינויי התנהגות הם עשו בתחילת התקופה". זאת שאלה שאי אפשר לענות עליה בלי לחבר את כל הנתונים של כל המשתמשים יחד.
המלצות מבוססות עדויות
במקום שהמודל יגיד "אולי כדאי לנסות לישון יותר", הוא יכול להגיד "אצל שבעים ושלושה אחוז ממשתמשים בפרופיל דומה לשלך, הוספה של שעת שינה אחת הביאה לשיפור ממוצע של 1.2 נקודות בחומרת הרעד תוך שלושה שבועות". זה הבדל ענק במשמעות הקלינית של אותה ההמלצה: מעבר מהשערה לעדות מספרית.
מאגר תצפיתי בעולם האמיתי
למרות שמחקרים קליניים פורמליים נעשים בדרך כלל על מאות בודדות של מטופלים, פלטפורמת ייעוץ עם רבבות משתמשים יוצרת אוטומטית מאגר תצפיתי עצום. בזהירות הראויה ובכפוף לפיקוח מקצועי, אפשר לחשוף ממנו השפעות שמחקרים מסורתיים קצרים לא היו רואים.
הכל בכפוף לאישור משתמש מפורש, לאנונימיזציה, ולהפרדה מוחלטת בין שכבת המוצר לשכבת האנליטיקה. שכבת ההזרמה ושכבת הניתוח כבר משולבות בארכיטקטורה במקביל לזרימת המוצר, בלי להאט אותה אף לרגע.
הזרמת תגובה בזמן אמת עם הקראה קולית מקבילית
משפט שנגמר מתחיל מיד להתקרא
כשאתה כותב למודל שיחה, אתה רואה את התשובה מופיעה מילה אחר מילה. זוהי הזרמה. עכשיו תוסיף קומפליקציה: אתה רוצה שהתשובה גם תיאמר בקול. הבעיה היא שלא יעיל לחכות שכל התשובה תיגמר לפני שמתחילים לקרוא אותה. הפתרון: כל משפט שמסתיים מתחיל מיד להפוך לאודיו, ומנגן ברגע שהוא מוכן, בזמן שהמשפט הבא עוד נכתב.
בניתי שירות ייעודי שפותח ערוץ דו כיווני מתמשך עם האפליקציה. בזמן שהמודל מייצר תשובה, כל מקטע טקסט נשלח מיד למסך וגם נצבר. ברגע שמתגלה משפט שלם, נשלחת בקשה מקבילית למנוע הסינתוז הקולי. ברגע שהאודיו חוזר, הוא מנוגן בסדר הנכון, גם אם משפט שלישי חזר לפני משפט שני. התוצאה: המשתמש שומע את ההתחלה של התשובה תוך פחות משנייה מהרגע שהפסיק לדבר, במקום לחכות חמש עד שבע שניות לכל התגובה.
עיגון תשובות במקורות אמת
בלי הזיות. במיוחד בתחום רפואי.
הבעיה הגדולה של בינה מלאכותית גנרטיבית היא "הזיות": המודל ממציא עובדות שנשמעות סבירות אך אינן נכונות. זה מסוכן במיוחד בתחום רפואי. הפתרון הוא הגשה למודל של מקורות אמיתיים לפני שהוא עונה: "ענה רק על בסיס המסמכים האלה, לא ממה שאתה זוכר מהאימון". כך התשובה תמיד מעוגנת במידע בדוק.
כשמשתמש שואל שאלה רפואית, הזרימה היא כדלהלן. ראשית, שאלת המשתמש מומרת לשאילתת חיפוש מקצועית באמצעות קריאה קלה למודל. שנית, המערכת מחפשת במנוע חיפוש מקצועי על מסד ידע רפואי ייעודי. שלישית, התוצאות מוחזרות למודל עם הוראת מערכת מחמירה: "ענה רק מתוצאות החיפוש, לעולם אל תטען טענות רפואיות מהזיכרון שלך". רביעית, התשובה תמיד כוללת הצהרת אחריות רפואית חובה. בנוסף, יש כלי שמתשאל בזמן אמת את הממשק הציבורי של רשות התרופות האמריקאית למידע מעודכן על תרופות.
שיקוף מבנה הענן על המכשיר
קריאות מהירות וחיסכון משמעותי בעלויות שרת
כל פעם שאפליקציה רגילה מציגה מסך, היא צריכה לפנות לשרת ולשאול "מה הנתונים שלי?". כל גלילה, כל פתיחת מסך, כל רענון הם קריאה לשרת. זה גורם לשלוש בעיות: המשתמש מחכה, השרת עובד קשה ועולה לנו כסף, ואם הרשת מתקטעת לרגע, האפליקציה נשברת. הפתרון: לשמור עותק מלא של הנתונים של המשתמש על המכשיר עצמו, באותו מבנה בדיוק שיש להם בשרת.
אותו מבנה עץ, שני מיקומים
המסד בענן מאורגן כעץ של מסמכים ואוספים. תחת כל משתמש יש את היומנים שלו, את התרגילים שלו, את היסטוריית השיחה שלו, את הסיכומים החודשיים ועוד. עיצבתי את המסד המקומי על המכשיר כך שיהיה לו בדיוק אותו מבנה לוגי: אותן ישויות, אותן יחסים, אותו ארגון היררכי. כשמפתח עובד באפליקציה, הוא לא צריך לחשוב "האם זה מקומי או מרוחק", הוא פשוט שולף נתונים מהמבנה. ההבדל היחיד הוא שהמסד המרוחק שומר על מבנה של מסמכים מקוננים, והמקומי שומר על שורות וטבלאות עם תרגום אוטומטי בין השניים.
ממשק המשתמש לא מדבר עם השרת
כל קריאה של נתונים בממשק המשתמש מתבצעת תוך מילישניות בודדות מהמסד המקומי. גלילה ביומן של החודש האחרון לא יוצרת אפילו קריאת רשת אחת. פתיחת היסטוריית שיחה מתבצעת מקומית. צפייה בסיכום החודשי מתבצעת מקומית. השרת בכלל לא יודע שהמשתמש פתח את המסכים האלה.
חיסכון מסיבי בעלויות שרת
בלי הגישה הזאת, אפליקציה פעילה הייתה גוררת מאות קריאות לבסיס הנתונים בענן לכל משתמש בכל יום. עם הגישה הזאת, השרת נשמע רק כשמתבצעת כתיבה (תוספת יומן חדש, הוספת תרופה) או כשמגיע שינוי מהענן (סנכרון בין מכשירים). ההבדל יכול להיות פי עשרים ויותר בעלות החודשית של שירות הענן עבור אותה תנועה.
סנכרון פסיבי ברקע
עשרה מאזינים על מבני הענן השונים נשארים פתוחים כל זמן שהאפליקציה רצה. ברגע שמשהו משתנה בענן (למשל המשתמש שמר נתון ממכשיר אחר, או עבודת רקע יצרה סיכום חודשי), השינוי זורם אוטומטית למסד המקומי. המשתמש רואה את העדכון בלי לרענן ובלי ללחוץ. הכל קורה ברקע.
הצפנה מלאה של המסד המקומי
כל הנתונים המקומיים מוצפנים. מפתח הצפנה אקראי בן 32 בייטים נוצר בהפעלה הראשונה ונשמר במאגר ההגנה של מערכת ההפעלה עם הרשאת גישה רק כשהמכשיר פתוח. גם אם מישהו מוציא את הקבצים פיזית מהמכשיר, הם רק רעש מוצפן.
כתוצאת לוואי, גם עובד בלי אינטרנט
ברגע שיש לך את כל הנתונים מקומית, האפליקציה ממשיכה לעבוד גם ברכבת תחתית, במטוס, או באזור עם קליטה גרועה. המשתמש יכול לרשום יומן, לראות היסטוריה ולהתחיל בדיקה, הכל גם במצב טיסה. כשהאינטרנט חוזר, מה שנכתב מסונכרן אוטומטית. זה לא היה היעד הראשי של הארכיטקטורה, אלא בונוס שצומח חינם ממנה.
ארכיטקטורת נתונים מבוססת זמן לניתוח על ידי AI
הערך האמיתי הוא במגמות, לא בנתון בודד
באפליקציה רפואית קלינית, הערך האמיתי לא נמצא בנתון בודד, הוא נמצא במגמות. כמה השתפר המשתמש מאז התחיל תרופה חדשה לפני שנה? איך חומרת הרעד שלו ביוני 2026 משתווה ליוני 2024? כדי שמודל בינה מלאכותית יוכל לענות על שאלות כאלה, הנתונים צריכים להיות מאוחסנים בצורה שמתחילה מהזמן, לא בצורה ש"זורקים את הכל לקופסה אחת ומקווים שהמודל ימצא".
עיצבתי את מבנה הנתונים סביב עיקרון אחד: כל שאלה היסטורית שהמודל יכול לשאול צריכה להיות זולה לענות עליה. זה מתבטא בכמה החלטות:
ארגון לפי זמן עם מזהים מבוססי תאריך
כל יומן יומי נשמר במסמך נפרד שהמזהה שלו הוא התאריך עצמו. אין צורך לסנן ולחפש, שליפת היומן של 15 במרץ 2025 היא קריאה ישירה אחת. אותו עיקרון לסיכומים חודשיים (מזהה לפי חודש) ולהערכות רבעוניות. הזמן הוא מפתח האינדקס הראשי, ולא שדה משני שצריך לחפש לפיו.
אגרגציה בכמה שכבות מראש
במקום שהמודל יצטרך לקרוא שבע מאות שלושים מסמכי יומן יומיים כדי לחשב "ממוצע חומרת רעד בשנתיים האחרונות", המערכת מייצרת אוטומטית סיכומים חודשיים מצרפיים בעבודה מתוזמנת בסוף כל חודש. כך השאלה "איך הרעד השתנה בשנתיים האחרונות" הופכת לקריאה של עשרים וארבעה מסמכים קטנים במקום שבע מאות שלושים. העיקרון הוא חישוב פעם אחת, שימוש אינסוף.
אוספים ייעודיים לכל סוג בדיקה
בדיקות מוטוריות שונות (ספירלה, הקלדה, מעקב קו) נשמרות באוספים נפרדים, ולא מערבבות עם תרגילי הרגעה. השאלה "איך ציון הספירלה השתנה מאז ינואר" הופכת לשאילתה חד משמעית על אוסף אחד מסונן לפי זמן.
ציר זמן של אירועי התנהגות
כשמשתמש אומר משהו משמעותי בשיחה ("הפסקתי קפה לפני שבועיים", "התחלתי להתאמן בבקרים"), המערכת מזהה זאת באמצעות קריאה לסיווג קצרה, ושומרת את האירוע כנקודת ציון בציר זמן ייעודי. בשיחה הבאה שעוסקת בשינוי בחומרת הרעד, המודל קורא לכלי שמחזיר את כל נקודות הציון מהחודשים האחרונים ועושה מתאם זמני: "אתה הפסקת קפה בתאריך 15 במרץ, וחומרת הרעד שלך ירדה ב 22 אחוז בששת השבועות שאחרי".
אות מוח ממתין
כשמשתמש מסיים בדיקה מוטורית עם ציון חריג, התוצאה נשמרת לא רק באוסף הבדיקות שלה, אלא גם כשדה "ממתין" על מסמך המשתמש. בפעם הבאה שהוא פותח את הצ׳אט, המודל רואה את האות הזה ויוזם שיחה רלוונטית מיד: "ראיתי שהרגע סיימת ספירלה עם ציון שגבוה ב 1.2 מהממוצע השבועי שלך. רוצה לדבר על זה?".
שמירה ארוכת טווח בענן, סנכרון מוגבל למכשיר
המסד שבענן שומר את כל ההיסטוריה ללא הגבלה, שנים אחורה. המסד שעל המכשיר מסונכרן רק עם תשעים הימים האחרונים. כשהמודל זקוק לנתונים ישנים יותר, הוא קורא לכלי ייעודי שמתשאל את הענן ישירות, מקבל את הנתונים מצומצמים לשדות הרלוונטיים בלבד, ומחזיר אותם כבר מסוכמים.
ארכיטקטורת שירותים מבוזרים
שינוי בלוגיקה אחת לא מפיל את כל המערכת
האלטרנטיבה לשירותים מבוזרים היא "מונוליט": שרת ענק אחד שעושה הכל. כשהוא נשבר, הכל נופל. כשמשנים פיצ׳ר אחד, צריך לפרוס את הכל מחדש. גישת המיקרו שירותים מפצלת את הלוגיקה ליחידות עצמאיות: כל אחת עם השם שלה, התלויות שלה, הפריסה שלה. שינוי באחת לא משפיע על האחרות.
צד השרת מחולק לשבע קבוצות פונקציות עצמאיות, וכל אחת מהן יחידת פריסה נפרדת לחלוטין עם ניהול תלויות משלה. שתיים מתוכן הן שירותים מבוססי קונטיינר, משום שהן דורשות תקשורת מתמשכת והזרמה שלא נתמכת בפונקציות סטנדרטיות. שינוי בלוגיקה של תרופות לא מחייב פריסה מחודשת של מנוע השיחה. זה חוסך זמן פריסה, מבודד תקלות, ומאפשר עבודה מקבילה של מפתחים שונים על אזורים שונים של המוצר.
אינפרנס מקומי על המכשיר
בחירה דינמית בין מנוע ענן למנוע מקומי לפי ההקשר
זיהוי דיבור והפיכת טקסט לקול אפשר לעשות בשתי דרכים: לשלוח את האודיו לשרת ענן (איכותי אך איטי, עולה כסף, דורש אינטרנט) או לעשות זאת מקומית במכשיר (חינמי, מיידי, פרטי, אבל פחות מדויק). אסטרטגיה חכמה משלבת בין השתיים, לפי הצורך.
בניתי שכבת תיווך בצד הלקוח שיודעת לבחור בין מנוע ענן (איכותי) למנוע מקומי לפי ההקשר. למשל, בזרימת הקמת פרופיל שדורשת דיוק מקסימלי שולחים לענן. בהאזנה רציפה ארוכה במהלך שיחה מנגנים מקומית. בנוסף, נכסי קול קבועים (כמו הקדמות לבדיקות) נוצרים מראש בכלי שורת פקודה בזמן בניית האפליקציה, ונארזים לתוכה כקבצי שמע, כך שאין עליהם שום עלות בזמן ריצה.
אופטימיזציית זמני תגובה
כל מה שהמשתמש לא צריך לחכות לו נדחה
בכל אינטראקציה עם מודל בינה מלאכותית יש פיתוי לחכות שהכל יסתיים לפני שמראים תוצאה למשתמש. זאת טעות. כל מה שהמשתמש לא צריך לחכות לו צריך לדחות לאחרי שהוא ראה תשובה. שמירה של ההודעה? אחרי. סטטיסטיקה על השימוש? אחרי. אפילו תוצאת השמירה עצמה, אם המשתמש לא מסתכל עליה, מחזירים "סינתטית" שהצליחה ובאמת שומרים ברקע.
המערכת מיישמת תבנית של "כתיבות נדחות": כל פעולה שאיננה קריטית להצגת התשובה (שמירת לוג, שמירת היסטוריית שיחה, מעקב שימוש) נדחפת למאגר שמבוצע אחרי שהתשובה כבר נשלחה למשתמש. בנוסף, מטמון פרומפטים ברמת המנוע מונע שליחה חוזרת של הוראת המערכת בת שלושת אלפי הטוקנים בכל בקשה, וחוסך כשבעים וחמישה אחוז מעלויות הטוקנים. מטמון בזיכרון של מופע השרת משמר את ההרכבה של הפרומפט בין בקשות. שליחת הקשר המשתמש מהאפליקציה חוסכת שלוש קריאות לבסיס נתונים בכל הודעה. התוצאה: זמן תגובה ראשונה של פחות משנייה וחצי, כשהמטמון פעיל.
אבטחה ופרטיות מהיסוד
הרשאות מינימליות בלי קיצורי דרך
אפליקציית בריאות חייבת לפעול לפי עיקרון "הרשאות מינימליות": כל משתמש, וכל רכיב בקוד, יקבלו רק את ההרשאות המינימליות שהם באמת צריכים. הפיתוי הגדול הוא קיצורי דרך: לחשוף נקודת קצה ציבורית כי "אנחנו היחידים שיודעים את הכתובת". בפועל, כל נקודת קצה ציבורית היא נכס תקיפה.
הוסרו לחלוטין נקודות קצה ציבוריות שהיו רגישות. למשל, נקודת קצה ציבורית להפקת קול הייתה חשופה לכל אחד באינטרנט שמצא את הכתובת. אפילו נקודת קצה מוגנת באימות נחשפה לכל משתמש מחובר, כולל אלמוניים שנוצרו אוטומטית. שתיהן הומרו לכלי שורת פקודה מקומיים שרצים רק מהמחשב של המפתח עם הרשאות אישיות. החוק שאני עובד לפיו: אם פעולה היא "אדמין בלבד", לעולם לא נקודת קצה ציבורית, תמיד כלי מקומי. אם בכל זאת צריך נקודת קצה, חוסמים גישה ברמת ההרשאות בענן, לא רק בקוד.
בנוסף, רכיב הגנה מובנה מאמת שהבקשות באמת מגיעות מהאפליקציה הרשמית (ולא מסקריפט מזויף), כל הנתונים נשמרים תחת מזהה משתמש פרטי עם חוקי גישה בענן, ובסיס הנתונים המקומי מוצפן.
ניהול גרסאות סכמה ומיגרציות
תאימות לאחור גם למשתמשים שדילגו על עשר גרסאות
האפליקציה שלך הותקנה אצל אלפי משתמשים בגרסה 1.0. עכשיו בגרסה 1.1 אתה מוסיף שדה חדש לטבלה. מה קורה למשתמשים שמתעדכנים? בסיס הנתונים המקומי שלהם לא מכיר את השדה, והאפליקציה תקרוס. מיגרציות הן סדרת שינויים מסומנים לפי גרסה, שמתבצעים אוטומטית כשהאפליקציה עולה למשתמש.
קובץ סכמה מגדיר את המבנה הנוכחי, וקובץ מיגרציות נפרד מנהל את כל ההיסטוריה: לכל שינוי (הוספת עמודה, יצירת טבלה, שינוי טיפוס) יש מספר גרסה ופונקציית שדרוג. כשמשתמש פותח את האפליקציה אחרי עדכון, המנוע משווה את הגרסה הנוכחית של בסיס הנתונים שלו לסכמה ומפעיל את כל המיגרציות החסרות בסדר. תאימות לאחור מובטחת לכל המשתמשים, גם אלה שדילגו על עשר גרסאות.
מערכת התראות רב ערוצית
לא כל ההתראות שוות
לא כל ההתראות שוות. חלקן דורשות התראת פוש מיידית (תזכורת לקחת תרופה), חלקן יכולות לחכות לפתיחה הבאה של האפליקציה ("הסיכום החודשי שלך מוכן"), וחלקן צריכות להתבצע רק בתנאים מסוימים. מערכת התראות מקצועית מנהלת את כל הקטגוריות האלה במשולב.
קבוצת פונקציות ייעודית מנהלת את מערכת ההודעות עם שולחים נפרדים לכל מקרה: שולח לסיכומים חודשיים שמתפעל בסיום עיבוד, ומשימה מתוזמנת לתזכורות רצף שבודקת מתי המשתמש שמר ביומן בפעם האחרונה. בצד הלקוח, שירות מקומי שומר את כל ההתראות בהיסטוריה מקומית (לא רק "פתח קופץ ועף"), כך שהמשתמש יכול לחזור לקרוא אותן. שירות הגדרות מאפשר למשתמש לכבות קטגוריות ספציפיות בלי לכבות את הכל. כל ההתראות עוברות הצפנה ומאומתות בצד הלקוח.
