A local-first personal second brain for free-form text capture, persisted on device and organized automatically by an AI classifier.
English · Español
Most note and task apps make you decide where something belongs before you can save it: pick a notebook, pick a project, pick a list, pick a tag. That decision cost is what stops most captures from happening.
CoreOS removes it:
Capture → Persist locally → Classify with AI → Organize automatically
You write free-form text. It is stored on the device before any AI call. An LLM then classifies it into a structured table. The raw input is never lost, never rewritten, and organization happens after capture, never before.
| Today | Capture | Library |
|---|---|---|
![]() |
![]() |
![]() |
| Today's tasks and pending captures at a glance. | One field. Anything you want to remember. | Notes grouped by recency, with full-text search. |
flowchart LR
A[Free-form input] --> B[SQLite inbox]
B --> C[LLM classifier]
C --> D[Dispatch by type]
D --> E[Notes]
D --> F[Tasks]
D --> G[Expenses]
D --> H[Habits]
D --> I[Sleep]
Key properties:
- Raw input is persisted first — the SQLite insert happens before the network call.
raw_textis canonical — for notes it is stored verbatim asbody_md. The LLM only emits metadata (suggested title and tags); it never rewrites, summarizes or splits the user's text.- AI failure never discards captures — an invalid or failed model
response leaves the inbox row in
pendingstatus, retryable from Today.
- SQLite database lives on the device
- No backend, no remote service
- No account, no auth
- API keys held in
expo-secure-store(iOS Keychain / Android EncryptedSharedPreferences) - Capture persisted before any AI call
- AI failure leaves the capture pending — raw text is never lost
Three tabs (Today, Capture, Library) plus Tasks and Settings as secondary screens.
Today — pending and overdue tasks, sorted overdue-first then by priority. Inline complete. A chip shows unclassified captures and retries the pipeline on tap.
Capture — a single free-text field. On save the text is inserted into the
local inbox and the async classifier runs. Also reachable from the iOS share
sheet and the coreos://capture deeplink.
Library — notes grouped into Pinned / Today / Yesterday / This week / Earlier. Full-text search, tag filters, Markdown editor with autosave, pin, soft-delete with undo and restore.
Tasks — full CRUD with date normalization for free-form due_date inputs
(hoy, mañana, d/m/y, ISO → YYYY-MM-DD).
Architecture details
Documented in src/services/inbox.ts and enforced in code:
- The inbox row is marked
processedinside the same transaction as the dispatch, guarded byWHERE status='pending'(lock optimistic). - Items are processed sequentially (
for/await, neverPromise.all). - Exported pipeline functions never throw — they return a result the caller can retry.
- A module-level mutex (
_batchInFlight) prevents concurrent batches; late captures automatically trigger one extra drain pass.
src/app/ expo-router screens (file-based, typed routes)
src/stores/ Zustand stores
src/db/ SQLite singleton, migrations, dispatcher
src/db/queries/ per-domain query modules
src/services/ LLM client + inbox orchestration
src/components/ shared UI
src/hooks/ theme, note editor
src/lib/ animations + note save gate
src/constants/ theme tokens
- Notes have a FTS5 virtual table (
notes_fts) withtokenize='porter unicode61', ranked withbm25. - Triggers keep the FTS index in sync on INSERT / UPDATE / DELETE.
- Soft-delete via
deleted_at; restore sets it back toNULL.
The LLM emits one of five types: nota, gasto, tarea, habito, sueno.
Schema for each is enforced client-side in processInboxText; if the response
is missing required fields or returns an unknown type, the pipeline logs a
warning and leaves the row in pending so the next batch re-picks it. The
raw_text is never lost — the row only flips to processed once a valid
dispatch succeeds inside the same transaction.
expo-router's unstable-native-tabs render the iOS native tab bar on iOS;
Android and web fall back to a JS implementation. This is why a dev build is
required (and Expo Go is not enough).
| Layer | Choice |
|---|---|
| Framework | Expo SDK 57, React Native 0.86 |
| Language | TypeScript, strict |
| Routing | expo-router (file-based, typed routes) |
| State | zustand |
| Database | expo-sqlite (FTS5) |
| Secrets | expo-secure-store |
| Animation | react-native-reanimated 4 + worklets |
| AI | Any OpenAI-compatible chat completions endpoint |
v0.1.0 · Personal V1 · Active development
Not a commercial product. Some tables in the pipeline (gastos,
habitos_log, sueno_log) are already written by the classifier, but their
management UI is intentionally not part of V1 — the data is captured and
stored; the screens come later. There is no automated test suite yet.
npm install
npx expo run:iosA development build is required: the project uses native config plugins (FTS5-enabled SQLite, SecureStore, iOS share extension) and native tabs, so Expo Go is not a supported target.
Open Settings inside the app and configure the AI provider (base URL, API
key, model). Keys live in expo-secure-store; there is no .env and nothing
sensitive is committed. Without a key, captures are still saved to the inbox
and stay pending until one is configured and the pipeline is retried.
npm run lint # ESLint via expo lint
npx tsc --noEmit # typecheckexpo-secure-store is unavailable on web — the AI pipeline does not work in a
browser build.
- Backend
- Cloud sync
- Multi-user / accounts
- Hermes vs JSC engine work
- Graph view and embeddings (
note_embeddingsexists in the schema but is unused) - Expense / habit / sleep management UI
Built by Ezequiel Menor — ezequielmenor.es · github.com/EzequielMenor


