App Developer Docs
Architecture
Overview
┌─────────────────────────────────────────────────────┐
│ Browser │
│ Vue 3 SPA — Pinia + Vue Router + Tailwind │
│ Hash-based routing (#/chat, #/state-check, etc.) │
└────────────────────┬────────────────────────────────┘
│ HTTP (proxied in dev)
▼
┌─────────────────────────────────────────────────────┐
│ Hono (Node.js) — port 3001 │
│ /api/auth/* — session auth, magic links │
│ /api/chat — OpenAI proxy │
│ /static — serves frontend/dist in production │
└────────┬──────────────────────┬──────────────────────┘
│ │
▼ ▼
better-sqlite3 OpenAI API
sulci.db chat completions
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 (see Auth).src/routes/chat.js— Proxies messages to OpenAI, injects the system prompt, returns the reply.
Frontend
Framework: Vue 3 with the Composition API (<script setup>).
State management: Pinia. Two stores:
stores/auth.js—user,loading,fetchMe(),login(),logout()stores/theme.js—theme,setTheme(),init()— writesdata-themeattribute on<html>
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 (theme toggle, logout)
├── router-view
│ ├── ChatView.vue
│ └── StateCheckView.vue
└── bottom-nav (Chat tab, State Check tab)
Component tree (unauthenticated):
App.vue
└── router-view
├── OnboardingView.vue
├── LoginView.vue
└── SetPasswordView.vue
Database
Single SQLite file (sulci.db) with two 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.
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.