Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/adr/002-authgate-stays.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# ADR 002: AuthGate stays

**Status:** Accepted
**Date:** 2026-08-22
**Deciders:** Tomas Maritano

## Context

Dripnex is local-first SQLite. An earlier write-up treated first-run login as a bug ("Inkdrop is offline, so skip the account").

## Decision

Keep AuthGate. Matching Inkdrop official docs:

- [Get started](https://docs.inkdrop.app/start-guide): create account, then "You'll see a login screen."
- [Privacy 5.4](https://docs.inkdrop.app/privacy): account required to use the client apps.

Offline and Don't Sync are **after** login. See #545.

## Consequences

First-run is account + login. Do not ship a guest editor as v1. Mobile, when it exists, uses the same account.
17 changes: 17 additions & 0 deletions docs/adr/003-sqlite-not-couchdb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# ADR 003: SQLite, not CouchDB

**Status:** Accepted
**Date:** 2026-08-22
**Deciders:** Tomas Maritano

## Context

Early Inkdrop posts (2017-18) used CouchDB/PouchDB+Cloudant. Planning CouchDB self-host as "Inkdrop parity" was wrong.

## Decision

Local store is SQLite (Inkdrop v6 replaced LevelDB with SQLite). Dripnex already uses better-sqlite3. Sync docs that mention Advanced CouchDB are leftover. Do not plan CouchDB self-host.

## Consequences

Mobile and MCP read/write the same note model (SQLite on device, optional E2E sync). No second database product.
25 changes: 25 additions & 0 deletions docs/adr/004-mcp-over-local-http.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ADR 004: MCP over Local HTTP

**Status:** Accepted
**Date:** 2026-08-22
**Deciders:** Tomas Maritano

## Context

`packages/mcp-server` opened the SQLite file via `DRIPNEX_DB_PATH`. The desktop already has a loopback API (Settings > Integrations, default `127.0.0.1:29168`, Bearer). Settings snippets still copied the SQLite path.

## Decision

One MCP for every agent (Grok Bot, Claude, Cursor, Codex). Prefer Local HTTP:

- `DRIPNEX_LOCAL_SERVER_URL` + `DRIPNEX_LOCAL_TOKEN`
- URL must be loopback
- SQLite path is fallback when HTTP env is unset
- Writes stay a separate toggle (`mcp.json` / `DRIPNEX_MCP_WRITES`). Enabling MCP/HTTP is not a write grant.
- Never commit the token. Rotate if it was pasted in chat.

Settings Claude/Codex snippets copy URL+token when MCP/HTTP is on. See PR 554.

## Consequences

Agents talk to the running app, not a second writer on the DB file. Clipper and Raycast can use the same HTTP. Grok Bot cannot attach that loopback as a hosted connector (127.0.0.1 is the user's machine).
22 changes: 22 additions & 0 deletions docs/adr/005-mobile-own-repo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# ADR 005: Mobile is its own repo

**Status:** Accepted
**Date:** 2026-08-22
**Deciders:** Tomas Maritano

## Context

Inkdrop has iOS/Android. Dripnex desktop is the product until write path + sync are proven. Putting UIKit/Swift inside `dripnex/app` (Electron monorepo) couples two release trains.

## Decision

- Do not put mobile in `dripnex/app`.
- Repo `dripnex/ios` (iPhone + iPad). Android later, not the first client.
- Same AuthGate + account as desktop. Same note model. Sync via existing `api.dripnex.app` after login.
- Two-profile desktop sync is a P3 risk, not a P1 blocker. Issue #551. Full plan: [`docs/mobile/PLAN.md`](../mobile/PLAN.md). Contract: [`docs/mobile/CONTRACT.md`](../mobile/CONTRACT.md).

## Consequences

Desktop CI/releases stay Electron-only. iOS has its own repo, signing, and TestFlight. Shared pieces are HTTP contracts and markdown rules, not the Electron shell.

`dripnex/ios` created 2026-08-22 (private). P1 skeleton starts there. iOS copies `NoteSnapshot` + `sync-core` wire fields; see [`docs/mobile/CONTRACT.md`](../mobile/CONTRACT.md).
10 changes: 10 additions & 0 deletions docs/ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Technical decisions

Accepted product calls live as ADRs. Dated log: [updates](./updates.md). Mobile plan: [`docs/mobile/PLAN.md`](../mobile/PLAN.md).

| ADR | Decision |
| ---------------------------------------- | -------------------------------------- |
| [002](../adr/002-authgate-stays.md) | AuthGate stays (match Inkdrop) |
| [003](../adr/003-sqlite-not-couchdb.md) | Local store is SQLite |
| [004](../adr/004-mcp-over-local-http.md) | MCP prefers Local HTTP |
| [005](../adr/005-mobile-own-repo.md) | Mobile is dripnex/ios, not dripnex/app |
13 changes: 13 additions & 0 deletions docs/ai/updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Updates

Newest first. Not release notes (`docs/releases/`).

## 2026-08-22

- Mobile **started**. Tomás said go. `dripnex/ios` created (private). Reuse is `NoteSnapshot` + `sync-core` wire. Contract: `docs/mobile/CONTRACT.md`. Two-profile desktop sync stays a P3 risk, not a P1 blocker.
- AuthGate stays. Inkdrop start guide + privacy 5.4. ADR 002.
- SQLite, not CouchDB. ADR 003.
- MCP over Local HTTP (Bearer, 127.0.0.1:29168). Settings snippets must copy URL+token. ADR 004. Code: PR 554.
- Technical decisions go in `docs/adr` + this folder. Mobile plan: `docs/mobile/PLAN.md`. ADR 005. Issue #551.
- #541 e2e CM merged. #552 Dependabot HTTPS rewrite merged. No release for CI-only.
- GitHub Project: https://github.com/orgs/dripnex/projects/3 (not Linear).
102 changes: 102 additions & 0 deletions docs/mobile/CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Mobile contract

Canonical types live in `dripnex/app`. iOS copies these fields. Do not invent new ones.

| Source | What iOS copies |
| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| [`packages/core/src/contracts/NoteSnapshot.ts`](../../packages/core/src/contracts/NoteSnapshot.ts) | Note shape that leaves core (IPC / UI / storage) |
| [`packages/core/src/domain/types.ts`](../../packages/core/src/domain/types.ts) | `NoteStatus`, `INBOX_NOTEBOOK_ID`, `TEMPLATES_NOTEBOOK_ID` |
| [`packages/sync-core/src/types.ts`](../../packages/sync-core/src/types.ts) | Zod wire schemas for push / pull |

Plan: [PLAN.md](./PLAN.md). Decision: [ADR 005](../adr/005-mobile-own-repo.md).

## NoteSnapshot

From `packages/core/src/contracts/NoteSnapshot.ts`. Plain serializable fields (no branded ids).

| Field | Type |
| ------------------ | ---------------- |
| `id` | `string` |
| `notebookId` | `string` |
| `content` | `string` |
| `title` | `string` |
| `createdAt` | `string` |
| `updatedAt` | `string` |
| `tags` | `string[]` |
| `wordCount` | `number` |
| `taskCount` | `number` |
| `checkedTaskCount` | `number` |
| `archivedAt` | `string \| null` |
| `isArchived` | `boolean` |
| `isPinned` | `boolean` |
| `isDeleted` | `boolean` |
| `status` | `NoteStatus` |

`NoteSummary` in the same file is the list-row subset (no `content`; adds `excerpt`). Same identity and status fields. Do not add a second note model.

## Domain constants

From `packages/core/src/domain/types.ts`:

| Name | Value |
| ----------------------- | --------------------------------------------------- |
| `NoteStatus` | `'active' \| 'on_hold' \| 'completed' \| 'dropped'` |
| `NOTE_STATUSES` | the four values above |
| `DEFAULT_NOTE_STATUS` | `'active'` |
| `INBOX_NOTEBOOK_ID` | `'inbox'` |
| `TEMPLATES_NOTEBOOK_ID` | `'templates'` |
| `MAX_NOTEBOOK_DEPTH` | `2` (levels 0, 1, 2) |

Inbox is the default notebook. Templates is reserved. Do not invent other reserved ids.

## Sync wire (Zod)

From `packages/sync-core/src/types.ts`. These schemas are the live wire. iOS must send and accept the same keys.

`NoteOperationSchema`: `'create' | 'update' | 'delete'`.

**LocalNotePushSchema** (plaintext content, renderer → main):

| Field | Rule |
| -------------- | ------------------------------------ |
| `noteId` | `string`, min 1, max 128 |
| `operation` | `NoteOperationSchema` |
| `content` | `string`, max 10 MiB, optional |
| `localVersion` | `number`, int, nonnegative, optional |

**EncryptedNotePushSchema** (content already encrypted, main → API):

| Field | Rule |
| --------------- | -------------------------- |
| `noteId` | `string`, min 1, max 128 |
| `operation` | `NoteOperationSchema` |
| `encryptedData` | `string \| null`, optional |
| `localVersion` | `number`, int, optional |

**EncryptedNotePushRequestSchema**: `changes` (1–100 `EncryptedNotePushSchema`), `deviceId` (UUID string).

**RemoteNoteChangeSchema** (API → client pull item):

| Field | Type |
| --------------- | --------------------- |
| `id` | `string` |
| `noteId` | `string` |
| `version` | `number` (int) |
| `operation` | `NoteOperationSchema` |
| `encryptedData` | `string \| null` |
| `deviceId` | `string` |
| `createdAt` | `string` |

**NotePushResultSchema**: `noteId`, `version` (int), `status` (`'applied' | 'conflict'`), optional `serverVersion` (int).

**NotePushResponseSchema**: `results` (`NotePushResultSchema[]`), `cursor` (int).

**NotePullResponseSchema**: `changes` (`RemoteNoteChangeSchema[]`), `cursor` (int), `hasMore` (boolean).

TypeScript names (`LocalNotePush`, `EncryptedNotePush`, …) are `z.infer` of those schemas. Copy the fields, not new ones.

## Rules

- Markdown is the note body (`content`). Do not persist a second AST or rich-text format.
- Same account / AuthGate as desktop. Sync is `api.dripnex.app` after login.
- If a field is not in these files, it is not in the contract.
99 changes: 99 additions & 0 deletions docs/mobile/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Mobile plan

Status: **started**. Tomás said go on 2026-08-22. P1 is [`dripnex/ios`](https://github.com/dripnex/ios) (private, created the same day). No iOS code in `dripnex/app`.
Issue: [#551](https://github.com/dripnex/app/issues/551). Decision: [ADR 005](../adr/005-mobile-own-repo.md). Contract: [CONTRACT.md](./CONTRACT.md).

Inkdrop ships iOS/Android after the same account login. Copy that product shape, not a guest-only phone editor.

## Why not a daily driver yet

Desktop still has to prove the write path and sync (two desktop profiles / two machines). That is a **P3 risk**, not a P1 blocker: if people write on the phone before merge is proven, mobile is a second offline silo. Clipper stays Later, not a side quest.

```mermaid
flowchart TD
repo[dripnex/ios P1 skeleton]
auth[AuthGate + magic link]
read[Read notes + notebooks]
write[Editor write path]
sync[E2E sync with desktop]
ipad[iPad layout]
android[Android later]
repo --> auth --> read --> write --> sync --> ipad --> android
```

## Gate

Satisfied for P1 (2026-08-22):

1. AuthGate stays on desktop (ADR 002).
2. Local HTTP + MCP are the agent path on desktop (ADR 004). Phone does not need MCP v1.
3. Tomás said go. Ask only for release / signing / TestFlight.

**P3 risk (not a skeleton blocker):** two desktop profiles can edit the same account and merge without data loss (#549). Keep it on the P3 dogfood phase. Do not wait on it to start P1 in `dripnex/ios`.

## Repo and stack

| | Choice | Why |
| -------- | --------------------------------------------------------------------------------------- | ---------------------------------------------- |
| Repo | `dripnex/ios` (private, created 2026-08-22) | Never inside `dripnex/app` |
| First OS | iPhone, then iPad | Inkdrop-shaped; Android after dogfood |
| UI | SwiftUI | Native, one store, no Electron in a phone |
| Notes DB | SQLite on device | ADR 003. Same fields as desktop |
| Editor | GFM in a WKWebView CodeMirror 6 shell, or a native markdown TextView if CM is too heavy | Editor is the product. Do not invent a WYSIWYG |
| Auth | Same account / magic link as desktop | AuthGate on first launch |
| Sync | `api.dripnex.app` after login, Don't Sync valid | Same as desktop |
| Plugins | Not v1 | Desktop plugin path (#547) first |

Do not use Capacitor wrapping the desktop app. Do not put React Native inside `dripnex/app`. iOS copies [CONTRACT.md](./CONTRACT.md); it does not invent note or sync fields.

## Product shape (v1 phone)

- Login, then three panes collapsed to list + editor (Inkdrop mobile).
- Inbox default. Statuses: hide completed/dropped in the main list.
- GFM only. One note per issue when that workflow exists on desktop.
- Offline after login. Local SQLite is source of truth.
- Share sheet / clipper: **not** phone v1 (same Later bucket as #551).

```mermaid
flowchart LR
subgraph phone [dripnex/ios]
login[AuthGate]
db[(SQLite)]
ed[GFM editor]
login --> db --> ed
end
api[api.dripnex.app]
desk[dripnex/app desktop]
db <--> api
desk <--> api
```

## Phases

**P0 — desktop (still in dripnex/app)**
Prove sync merge + attachments (#549). That work stays here. It does not block the P1 skeleton.

**P1 — skeleton (`dripnex/ios`, started)**
Xcode project, AuthGate, empty Inbox, local SQLite. Read-only notes when sync exists; do not block the skeleton on two-profile desktop sync. No iOS sources in this repo. TestFlight only with Tomás.

**P2 — write path**
Create / edit / trash. Titles = first non-empty line. Templates if desktop templates already sync.

**P3 — sync dogfood**
Same account: type on phone, see it on desktop, and the reverse. Conflict UI: keep this / keep other / open both (desktop already has this). Two-profile desktop merge (#549) is the quality risk in this phase.

**P4 — iPad**
Three-pane optional. Not a new app.

**P5 — Android / clipper**
Only after iOS is a daily driver. Clipper can be a share extension on iOS first, still Later.

## Out of scope

- Marketplace, graph, AI-notetaker, hosted note MCP on the phone
- Building inside `dripnex/app`
- Treating two-profile desktop sync as a P1 gate

## Done when (later)

Tomas can jot a note on iPhone after login, open it on desktop, and the markdown is the same. Until then P1 is the skeleton in `dripnex/ios`, not a daily-driver ticket.
3 changes: 3 additions & 0 deletions docs/mobile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Mobile

Plan: [PLAN.md](./PLAN.md). Contract: [CONTRACT.md](./CONTRACT.md). Decision: [ADR 005](../adr/005-mobile-own-repo.md). Issue: [#551](https://github.com/dripnex/app/issues/551).
Loading