Mimio'yu yazmaya başladığımda ergoterapinin ne olduğunu tam olarak bilmiyordum. Ürün fikri şuydu: terapistin seansta oynattığı oyunlar ölçülebilir veri üretsin, o veri danışanın dosyasına düşsün ve zamanla bir ilerleme çizgisi çıksın. Kulağa temiz geliyordu. İlk yaptığım şey yanlıştı.
Yanlış olan şuydu: seans notu için kendi alanlarımı tasarladım. "Gözlemler", "Sonuç", "Notlar" diye üç kutu koydum. Gayet mantıklı görünüyordu — bana. Meğer bu mesleğin zaten bir not formatı varmış ve onlarca yıldır kullanılıyormuş.
Mesleğin Kendi Dili Vardı: SOAP
SOAP, sağlık alanında yaygın bir klinik not standardı. Dört harf, dört bölüm — ve bu bölümler keyfi değil, bir düşünme sırası:
| Harf | Ne yazılır | Kim söylüyor |
|---|---|---|
| S · Subjektif | Danışanın ya da ailesinin anlattığı | Danışan |
| O · Objektif | Seansta gözlenen, ölçülen | Terapist |
| A · Assessment | Bu ikisinden çıkan değerlendirme | Terapist |
| P · Plan | Bir sonraki adım | Terapist |
Bu sıra bir şey öğretiyor: önce ne duyduğunu yaz, sonra ne gördüğünü, ancak ondan sonra ne düşündüğünü. Yorumu gözlemden ayırıyor. Benim "Gözlemler / Sonuç / Notlar" üçlüm bu ayrımı hiç yapmıyordu.
export interface SoapNoteContent {
s: string // Subjektif — danışanın anlattığı
o: string // Objektif — gözlenen, ölçülen
a: string // Assessment — değerlendirme
p: string // Plan — bir sonraki adım
}
/* Serbest not da duruyor: her seans SOAP yazmayı hak etmiyor ve
birini zorla forma sokmak, formu boş bırakmasına yol açıyor. */
export type NoteMode = 'free' | 'soap'Bir mesleğe araç yazıyorsan, o meslek kavramlarını çoktan adlandırmıştır. Senin işin yeni bir sözlük icat etmek değil, mevcut sözlüğü ekrana doğru yerleştirmek.
Serbest not seçeneğini bırakmam da bilinçli. Her seans dört bölümlük bir değerlendirmeyi hak etmiyor; on dakikalık bir kontrol seansına SOAP yazmaya zorlanan biri hiçbir şey yazmıyor.
Oyunlar Sadece Oyun Değil, Ölçüm
Panelde yedi terapi oyunu var ve hepsinin adı Türkçe: Sıra Hafızası, Kart Eşle, Mavi Nabız, Komut Rotası, Fark Avcısı, Hedef Tarama, Dizi Mantık.
Her biri farklı bir beceriye bakıyor — çalışma belleği, görsel tarama, tepki kontrolü, sıralı komut takibi. Ama asıl mesele oyunun kendisi değil, arkasında bıraktığı kayıt.
export interface RemoteScoreSummary {
label: string
best: number
last: number
sessions: number
lastPlayedAt: string | null
}Burada `best` ile `last` ayrımı küçük ama önemli. Sadece en iyiyi saklasan ilerleme hep yukarı gidiyor gibi görünür; sadece sonuncuyu saklasan kötü bir gün bütün tabloyu bozar. İkisi birlikte "iyi günü ne, bugünü ne" sorusunu cevaplıyor.
Bilgi
Bir şeyi bilerek yapmadım: skorları otomatik yorumlamak. "Gelişme var" ya da "gerileme var" yazan bir kutu koymayı düşündüm ve vazgeçtim. Bu yargıyı verecek kişi terapist; sistemin işi veriyi düzgün göstermek, onun yerine karar vermek değil.
Şema Her Hafta Değişti, Migration Yazmadım
Şimdi teknik tarafın en tartışmalı kararına geliyorum ve gerekçesi doğrudan yukarıdaki durumla ilgili.
Alanı bilmiyordum. Bu şu demek: şema tahminlerim yanlıştı ve her konuşmadan sonra değişiyordu. İlk ay içinde "destek düzeyi" alanı eklendi, sonra kullanıcı adı ve parola, sonra uzmanlık alanı. Her seferinde migration üretip uygulamak, tek kişilik bir projede kazandırdığından fazla vakit alıyordu.
Bu yüzden şemayı kodun içinde, idempotent ifadeler olarak tuttum. Uygulama açılırken çalışıyor ve istediğin kadar çalıştırabiliyorsun.
const SCHEMA_QUERIES = [
"CREATE EXTENSION IF NOT EXISTS pgcrypto",
`CREATE TABLE IF NOT EXISTS therapist_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
display_name TEXT NOT NULL UNIQUE,
clinic_name TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
// Sonradan eklenenler ayrı ifadeler: tablo zaten varsa CREATE
// atlanır, kolon eklemesi yine de çalışır.
"ALTER TABLE therapist_profiles ADD COLUMN IF NOT EXISTS specialty TEXT",
"ALTER TABLE therapist_profiles ADD COLUMN IF NOT EXISTS username TEXT",
"ALTER TABLE therapist_profiles ADD COLUMN IF NOT EXISTS password_hash TEXT",
]Şu an 6 `CREATE TABLE` ve 12 `ALTER TABLE` var. ORM de yok — sorgular `@neondatabase/serverless`'in etiketli şablonuyla yazılıyor, yani parametreler otomatik bağlanıyor ve string birleştirme hiç girmiyor işin içine.
Bunun Bedeli Ne
Ücretsiz değil, üç şey kaybediyorsun:
- Tip güvenliği. Satırlar `Record<string, unknown>` olarak geliyor; kolon adını yanlış yazarsan derleme değil, çalışma zamanı hatası alıyorsun.
- `ADD COLUMN IF NOT EXISTS` kolonu ekler ama silmez, tip değiştirmez. Geriye dönmek isteyince elle SQL yazman gerekiyor.
- Yeniden adlandırma acı verici — editör yardım etmiyor, grep'e kalıyorsun.
Bu bedeli altı tablolu ve tek geliştiricili bir projede ödemeye değer buldum. Açılış Zili'nde aynı kararı vermedim: orada 15 tablo var, ilişkiler iç içe ve aynı tabloyu okuyan beş ayrı yer bulunuyor. Orada Drizzle'ın verdiği şey ORM'liğinden çok tip üretimiydi — şemaya kolon eklediğim an eksik alan bırakan her yer derlemede kırmızı oluyor.
Aynı geliştirici, aynı veritabanı, iki farklı karar
Mimio · 6 tablo
Ham SQL
Açılış Zili · 15 tablo
Drizzle
Belirleyici soru "hangisi daha iyi" değil, "aynı tabloyu kaç yerden okuyorum". Bir yerden okuyorsan tipi elle yazmak sorun değil; beş yerden okuyorsan şemadan türemesi seni kurtarıyor.
Serverless'ta pg Kullanılmaz
Bağlantı katmanı tercihi değil zorunluluktu. Vercel'de her istek ayrı bir fonksiyonda çalışıyor ve yanıt döndükten sonra donuyor. Klasik bir bağlantı havuzu bu modelde işe yaramıyor: bağlantı açılıyor, fonksiyon donuyor, bağlantı açıkta kalıyor. Birkaç yüz istek sonra Postgres "too many connections" veriyor.
Neon'un HTTP sürücüsünde kalıcı bağlantı yok, dolayısıyla sızdıracak bağlantı da yok. Görünmeyen bedeli ise şu: her sorgu bir HTTP round-trip. Yerelde fark etmiyorsun çünkü gecikme 1 ms; üretimde 20 ms oluyor ve toplu iş yapan bir sayfa aniden saniyeler sürüyor.
Dikkat
Bu tuzağa başka bir projede tam olarak düştüm: 500 kayıt için `Promise.all` ile 500 ayrı insert. Masum görünüyor, çünkü kod paralel. Tek bir çok satırlı upsert'e çevirince 513 istek 2'ye indi.
Klinik Veri Olduğunu Unutmamak
Panelde danışan adları, seans notları ve gelişim kayıtları var. Bu hobi projesi verisi değil.
Pratikte yaptıklarım mütevazı: parola karması `bcrypt` ile, oturum kilidi tek kullanıcı için, uydurma demo verisi üretimde yok. Ama en önemlisi bir şeyi yapmamak oldu — hiçbir yere analitik, hata izleme ya da üçüncü taraf betiği koymadım. Seans notunun bir hata raporunun içinde başka bir sunucuya gitmesi, düşünmek bile istemediğim bir senaryo.
Ne Öğrendim
Bilmediğin bir alana araç yazarken en pahalı hata, alanı kendi kafandaki modele göre tasarlamak. SOAP'ı sonradan öğrenip geçiş yapmak, baştan sorup öğrenmekten çok daha uzun sürdü.
İkincisi: şema belirsizliği bir teknoloji tercihi doğuruyor. "ORM kullanmalı mıyım" sorusunun cevabı projeye değil, projenin hangi aşamasında olduğuna bağlı. Alanı öğrendiğim ve şema oturduğu gün Mimio'yu Drizzle'a taşımak mantıklı olabilir — henüz o gün gelmedi.
Hâlâ emin olmadığım bir şey var: skorları yorumlamamak doğru karar mıydı? Terapistin işini kolaylaştırmak ile onun yerine karar vermek arasındaki çizgi göründüğünden ince ve ben şu an çizginin epey gerisinde duruyorum.