Skip to content
ronaldvanheldenPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

DocDrop

An Obsidian-style, web-based quick-documentation app. Fast markdown typing with a live side-by-side preview, fully offline-capable (PWA), multi-user with simple last-edit-wins sync.

  • Editor first: CodeMirror 6 markdown editor on the left, rendered preview on the right. Ctrl+\ collapses the preview, Ctrl+B the sidebar. On mobile it becomes an edit/preview toggle.
  • Wikilinks: [[Note Title]] (or [[Title|alias]]) links between notes, with autocomplete after typing [[. Links to notes that don't exist yet are styled dashed — clicking one creates the note.
  • Quick switcher: Ctrl+P (or Ctrl+K) fuzzy-jumps to any note; Shift+Enter creates a note with the typed name.
  • Tags: #tag renders as a clickable chip that filters the sidebar.
  • Search: Ctrl+Shift+F — SQLite FTS5 full-text search with snippets when online, automatic local fallback offline (badge shows which).
  • Folders: notes have a path (projects/alpha); the sidebar renders a collapsible tree. Move a note via the ↦ action.
  • Offline-first: the app shell is service-worker cached and all notes live in IndexedDB. You can close the browser, reopen offline, keep editing — everything syncs when you're back online.

How sync works (please read once)

Every edit gets a timestamp from a monotonic hybrid-logical clock (never goes backwards, jumps ahead of anything the client has seen from the server, and the server clamps timestamps more than 10 minutes in the future). Per note, the higher timestamp wins — exact ties are broken deterministically by writer id. Deletes are tombstones and participate in the same rule:

  • Two people edit the same note while offline → whoever edited later wins everywhere once both are back online. The loser gets a toast saying their older edit was replaced (the data is not merged — this is deliberate simplicity).
  • Edit at 10:00, delete at 10:05 → the note is deleted.
  • Delete at 10:00, offline edit from 10:05 arrives later → the edit wins and the note is resurrected.
  • If a winning remote version arrives while you have the note open with no pending keystrokes, your view refreshes; if you're actively typing, your keystrokes stay and become the newest edit (typing wins over background sync).

Sync runs on app start, when the browser comes back online, and every 15 seconds.

Running locally (no Entra setup needed)

npm install
AUTH_DISABLED=true npm run dev

Open http://localhost:5173. With AUTH_DISABLED=true each browser profile gets a random dev identity (x-debug-user header) — open a second browser/incognito window to watch two-user sync live.

npm test runs the server test suite (LWW semantics, tombstones, pagination, timestamp clamping, JWT validation).

Production with Docker

cp .env.example .env   # fill in ENTRA_TENANT_ID / ENTRA_CLIENT_ID
docker compose up --build

Serves the app and API on port 3001; the SQLite database lives on the docdrop-data volume.

Entra ID app registration (one-time)

  1. Entra admin center → App registrations → New registration. Name it (e.g. DocDrop), single tenant.
  2. Authentication → Add a platform → Single-page application → redirect URI = your DocDrop origin (e.g. https://docs.example.com, and http://localhost:3001 for testing).
  3. Expose an API → Set the Application ID URI to the default api://<client-id> → Add a scope named Notes.ReadWrite, admins + users can consent.
  4. API permissions → Add a permission → My APIs → your app → Notes.ReadWrite (then grant admin consent if your tenant requires it).
  5. Put the Directory (tenant) ID and Application (client) ID in .env.

The server validates bearer tokens against the tenant's JWKS (jose), accepting both api://<client-id> and the bare client id as audience. The user's oid claim is recorded as the note's last editor. Sign-in state is kept in localStorage so an installed PWA stays signed in across launches — be aware of that on shared machines.

Architecture

client/  Vite + React + TypeScript SPA (PWA)
         CodeMirror 6 editor · react-markdown preview · Dexie (IndexedDB) · MSAL
server/  Fastify + better-sqlite3 (WAL) · jose JWKS auth · FTS5 search
shared/  types + the single LWW compare function used by BOTH sides
  • shared/src/lww.ts — the one last-write-wins rule; every convergence guarantee hangs on this.
  • client/src/sync/syncEngine.ts — pushes dirty notes, pulls by server_seq cursor, applies LWW locally.
  • server/src/notesStore.ts — push transaction (clamp → compare → upsert + fresh server_seq), pull pagination, FTS.
  • Notes are flat UUIDs with a path field; the folder tree is derived, so renames/moves are one-row updates that survive sync.
  • The service worker precaches the app shell only; /api/* is never cached (IndexedDB is the offline store), except /api/config (stale-while-revalidate, needed to boot offline).

Sync API

Endpoint Description
POST /api/sync/push Batch of changed notes. Per item: applied (with new server_seq) or stale (with the winning server copy, so the loser converges immediately). Writer identity always comes from the token.
GET /api/sync/pull?since=<seq> Changes ordered by server_seq (global monotonic counter). Re-edited notes get a fresh seq, so a cursor never misses updates.
GET /api/search?q= FTS5 MATCH with bm25 ranking and snippet().

Development notes

  • npm run dev runs Fastify (3001) and Vite (5173, proxying /api) together.
  • npm run typecheck / npm test / npm run build at the root cover all workspaces.
  • Tombstones are kept forever (rows are tiny — content is cleared on delete). A purge job is a possible future addition.
  • Duplicate titles are allowed; wikilinks resolve to the most recently edited match.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages