S Sulci Early Access

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, imports app.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. In NODE_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.jsuser, loading, fetchMe(), login(), logout()
  • stores/theme.jstheme, setTheme(), init() — writes data-theme attribute 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 /apilocalhost: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

ColumnTypeNotes
idINTEGER PKAuto-increment
emailTEXT UNIQUEPrimary identifier
display_nameTEXTNullable
password_hashTEXTbcrypt hash, null until set
needs_password_setupINTEGER1 = new user, 0 = returning
themeTEXTdark or light
created_atTEXTISO datetime

sessions

ColumnTypeNotes
idINTEGER PKAuto-increment
user_idINTEGER FKReferences users.id
tokenTEXT UNIQUE64-char hex, random
typeTEXTsession or magic
expires_atTEXTISO 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.