Some checks failed
Build & Release APK / build (push) Failing after 13s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.6 KiB
4.6 KiB
Kin — scope & architecture
Written 2026-08-09, at project start.
What this is
A personal "relational wealth" app: the single place that answers who haven't I talked to in too long? and makes fixing that a two-tap action. Inspired by Monica CRM, but deliberately thin — one user, one server, no accounts, no social features.
Product principles
- The due list is the product. Contacts, notes, and history exist to feed one screen: who to reach out to today. Everything else is secondary.
- Logging must be cheaper than not logging. Two taps ("text, today, done") or the log stays empty and the due list lies. The old web CRM had 44 people and 0 interactions — that's the failure mode to design against.
- Cadence over guilt. Each person gets a cadence (weekly → 6-monthly) or none at all. No global "you're behind on 30 people" — only people you chose to track surface in reminders. Snooze is honest: it defers, it doesn't fake contact.
- Thin client, durable data. All state lives in the
personalPostgres database on the VPS. The phone caches for instant open and offline reading; writes go straight to the API.
Architecture
Pixel (Kin app, Expo RN) ──Bearer token──▶ Caddy ──▶ crm container (Bun+Hono) ──▶ personal-db (Postgres)
Browser (web UI) ──basic_auth───▶ ↑ same API, same data
- Server: the existing
crmcontainer, extended. Hono + porsager/postgres on Bun. Now has boot-time SQL migrations (schema_migrations), aGET /api/dueendpoint, cadence/snooze/archive columns, relationship create/delete, and Bearer-token auth for non-browser clients. - Due math (server):
urgency = days_since_last_contact / cadence_days, anchored oncreated_atfor never-contacted people so new adds surface immediately.overdue≥ 1.0,due_soon≥ 0.75. Sorted by urgency, so a weekly friend 3 days late outranks a yearly contact 3 days late. - App: Expo SDK 57 + expo-router, structured like the GTD app (read-cache
store on
useSyncExternalStore+ AsyncStorage, refresh on foreground/focus). Dark warm theme, Pixel 9 XL first. - Reminders: local notifications only, no server push. On every sync the app reschedules a daily digest for the next 7 days ("N people are due for a catch-up") at the configured hour. If the app isn't opened for a week, the notifications run out — which is itself the right nudge. Server-side push via the existing ntfy container is the v2 escape hatch if this proves too passive.
Screens
| Screen | Job |
|---|---|
| Today (home) | Due list in sections: Reach out / Coming up / On track / Snoozed. Quick-log button on every row. Empty state doubles as onboarding. |
| People | Whole network, client-side search + tag filter (44 people — no server round-trips). |
| Person | Contact actions (Message/Call/WhatsApp/Email deep links), cadence picker, log + snooze, notes, relationships, history (long-press to delete). |
| Log (modal) | Type chips + today/yesterday + optional note. Two taps minimum. |
| Add/Edit (modal) | Name, phone, email, location, tags, notes, cadence. |
| Settings (modal) | Server URL, token, reminder hour. Probes the API before saving. |
Deliberately out of v1
- Offline write queue — GTD's op-queue store is proven but heavy; a relationships app tolerates "log it when you're back online". Revisit if it annoys in practice.
- Birthday reminders — column exists (
people.birthday), UI doesn't. - Relationship editing in-app — API supports create/delete now; app only displays. Add when graph curation actually matters.
- Web UI parity — the vanilla-JS web UI still works for bulk edits at crm.rehbock.xyz; it doesn't know about cadence yet.
- Contact import from the phone — seed data came from the newsletter
subscriber list;
expo-contactsimport is a v2 candidate. - Multi-user / open-source hardening — single-user by design. The repo is structured to be open-sourceable (no secrets in git, migrations from scratch, auth documented), but no login system until someone else needs it.
Infrastructure facts
- VPS dir:
~/personal/crm(containercrm, networkcaddy_net+personal-db_default). DB:personalin the sharedpersonal-dbPostgres. - Caddy:
crm.rehbock.xyz; Bearer requests bypass basic_auth (app validates), everything else challenges. - CI: Gitea Actions on the desktop runner (label
desktop), signed APK per push to main, released askin-v1.<run>.apkfor Obtainium. - Keystore:
~/.android-keys/kin-release.jks, aliaskin— distinct from GTD's so Android treats the apps independently.