App Developer Docs
Architecture
Overview
┌──────────────────────────────────────────────────────────┐
│ Browser │
│ Astro marketing/docs site ──► served at / │
│ Vue 3 SPA (Pinia + Router + Tailwind) ──► served at /app/│
│ Hash routing (#/chat, #/habits, #/today, …) │
└───────────────────────┬───────────────────────────────────┘
│ HTTP (Vite proxy in dev)
▼
┌──────────────────────────────────────────────────────────┐
│ Hono (Node.js) — port 3001 │
│ /api/* — auth, chat, goals, decisions, panic, checkins, │
│ activation, tasks, life-goals, todo-lists, │
│ user-states, education, routines, push, … │
│ static — Astro dist at /, Vue dist at /app/ (production) │
└──────┬─────────────────┬──────────────────┬────────────────┘
│ │ │
▼ ▼ ▼
better-sqlite3 OpenAI API web-push (VAPID)
sulci.db chat + nudges ▲ driven by a 1/min cron
(relational+blob) → POST/GET /api/push/tick
Backend
Framework: Hono — a small, fast HTTP framework that runs on @hono/node-server in Node.js (and could run on any edge runtime without changes).
Entry points:
backend/src/app.js— Hono app with routes mounted. Import this in tests.backend/src/index.js— Loads.env, importsapp.js, starts the server. Never imported in tests.
Modules:
src/db.js— Opens the SQLite file, creates tables if they don’t exist, exports typed query helpers.src/email.js— Sends magic link emails via nodemailer. InNODE_ENV !== production, logs the URL to stdout instead.src/routes/auth.js— All authentication routes incl. password registration (see Auth).src/routes/chat.js— Proxies messages to OpenAI, injects the system prompt, returns the reply.src/routes/push.js— Web Push: subscribe/unsubscribe, test, and the crontickthat delivers notifications and morning nudges.src/push.js—web-pushwrapper (VAPID setup, send, prune-on-gone).src/nudge.js— Generates morning-nudge copy via OpenAI (engage-don’t-guilt), with a static fallback.
The backend has grown well beyond auth + chat — goals, decisions, panic, check-ins, tasks/habits, activation debt, education, routines, and push all live under
src/routes/. This page covers the core spine and the notification pipeline; see the API reference for routes.
Frontend
Framework: Vue 3 with the Composition API (<script setup>).
State management: Pinia. Stores include:
stores/auth.js—user,loading,fetchMe(),login(),register(),logout()stores/theme.js—theme,setTheme(),init()— writesdata-themeattribute on<html>stores/decision.js,stores/goals.js,stores/panic.js,stores/checkins.js,stores/activation.js— per-feature state, hydrated from the user payload on login
Routing: Vue Router with hash history (createWebHashHistory). Hash routing means the SPA works without server-side route handling — the backend only needs to serve index.html for the root.
API client: api/client.js — axios instance pointing at /api. The Vite dev server proxies /api → localhost:3001. A 401 interceptor redirects unauthenticated users to #/onboarding.
Component tree (authenticated):
App.vue
├── top-bar (decision/panic metrics · ⚙ settings · admin* · theme · logout)
├── router-view → ChatView, StateCheckView, TwinView, DecisionView,
│ GoalsView, PanicView, InstitutionalView (#/training),
│ CheckinView, TodayView, EducationView/ModuleView (#/learn),
│ HabitsTreeView (#/habits), TodoView, DatabaseView,
│ SettingsView, HelpView, AdminView*
└── bottom-nav (Chat · State · Decision · Goals · Panic · Train · Twin
· Learn · Database · Todo · Habits)
* admin-only
Component tree (unauthenticated):
App.vue
└── router-view
├── OnboardingView.vue
├── LoginView.vue (Password · Email me a link · Create account)
└── SetPasswordView.vue
Two apps, two mounts. In production Hono serves the Astro marketing/docs site at
/and the Vue SPA at/app/. The SPA uses hash routing under that base (/app/#/chat). This is why magic-link and other server redirects are/app-prefixed in production — see Auth.
Database
Single SQLite file (sulci.db, WAL mode). It mixes two styles: a few relational tables with explicit columns (the originals), and many JSON-blob tables shaped (id, data, created_at, updated_at) (the life-agent-derived data), queried via json_extract. New columns are added idempotently at startup via ensureColumn; one-off migrations run through schema_migrations.
The two core relational tables:
users
| Column | Type | Notes |
|---|---|---|
id | INTEGER PK | Auto-increment |
email | TEXT UNIQUE | Primary identifier |
display_name | TEXT | Nullable |
password_hash | TEXT | bcrypt hash, null until set |
needs_password_setup | INTEGER | 1 = new user, 0 = returning |
theme | TEXT | dark or light |
created_at | TEXT | ISO datetime |
sessions
| Column | Type | Notes |
|---|---|---|
id | INTEGER PK | Auto-increment |
user_id | INTEGER FK | References users.id |
token | TEXT UNIQUE | 64-char hex, random |
type | TEXT | session or magic |
expires_at | TEXT | ISO datetime |
Magic link tokens and real session tokens both live in the same sessions table. A magic token with type = 'magic' is burned (deleted) the moment it’s used.
The users table has grown additive columns over time (via ensureColumn), including is_admin, timezone, openai_api_key, and the notification prefs nudge_enabled + nudge_morning_time.
Other relational tables
Alongside users/sessions: goals (panic/decision goals with cycle phases), decisions (the decision log + state-check snapshots), panic_moments, checkin_sessions / checkins / state_probes (the training/check-in layer), training_load_snapshots, and education_modules / user_module_progress (the Learn section).
JSON-blob tables
The life-agent-derived data lives in blob tables (id, data, created_at, updated_at): life_goals, user_states, one_time_tasks, recurring_tasks (habits), todo_lists, weekly_reviews, journal_entries, chat_contexts, help_articles, activation_sessions, routines, plus the two push tables. Generic helpers blobInsert/blobGet/blobUpdate/blobDelete/blobList in db.js operate on any of them.
push_subscriptions — data: { user_id, endpoint, keys: { p256dh, auth }, ua }. Deduped by endpoint; pruned when a push service returns 404/410.
scheduled_notifications — data: { user_id, kind, title, body, url, send_at, sent }. The cron delivers rows whose send_at has passed. Morning-nudge dedupe rows use kind: 'morning' + dedupe_date (the user’s local day) so each user is nudged at most once per day.
Service worker
frontend/public/sw.js (served at /app/sw.js, scope /app/) handles push events (showNotification) and notificationclick (focus/open /app/#/chat). It’s registered when the user enables notifications in Settings.
Communication between frontend tabs
The State Check view and the Chat view are separate routes. When the user taps “Attach to chat →”, the State Check view dispatches a custom DOM event:
window.dispatchEvent(new CustomEvent('sulci:attach-state', { detail: formattedText }))
The Chat view listens for this event and prepopulates the input. There is no shared Pinia state between them — the browser event is intentionally the coupling point so the two features stay independent.
Testing strategy
Backend — integration tests using Hono’s app.request() against a real in-memory SQLite DB. No HTTP server is started. OpenAI and nodemailer are mocked.
Frontend — component tests using @vue/test-utils + jsdom. Axios is mocked. Pinia is freshly initialized before each test. Tests verify component behavior (what the user sees and does), not implementation details.
See the test files for the full test list.