02 / 15 · Solution Design
How the components work together
A Bun server serves HTML and APIs. The browser saves notes locally first. Google Drive is the optional user-owned storage for synchronization.
For the user interface’s appearance, use 09 Design: visual layouts, colour tokens, typography, controls and mobile behaviour.
Detailed contracts
Use 11 API Contracts, 12 Failure & Recovery, 13 Markdown Compatibility, 14 Setup & Operations and 15 Acceptance & Fixtures for exact build decisions. Source observations are not automatically target behavior; known limitations explicitly marked for correction must be fixed in the new build.
Stack and implementation boundaries
This package reconstructs the existing application: Bun, CommonJS server modules, HTML rendering functions, TypeScript custom elements and ProseMirror. This specific architecture takes priority over the general preference for React. Switching to another framework requires a separate design.
| Layer | Choice | Contract |
|---|---|---|
| HTTP | Bun.serve + custom router | Do not assume Express. Routes, services, request context and rendering are separate. |
| Browser | TypeScript/ESM + custom elements | Bun serves browser modules; no separate frontend build is required. |
| Editor | ProseMirror directly | Schema, input rules, history, tables and Markdown parser/serializer. |
| Storage | IndexedDB + your own Drive | No SQL or persistent readable note text on the server. |
| Server data | JSON + files | Accounts, sessions, media, canvas and technical metadata per account; use atomic writes. |
| Tests | bun test + BrowserOS Neo | Test doubles for external services; report real integration checks separately. |
The repository uses prosemirror-*, orderedmap, dotenv, google-auth-library, fflate and loro-crdt. The 28 October baseline pins 17 runtime and two development dependencies. Password hashing and its helper script have been removed. Use the exact manifest and lockfile in Setup & Operations; record any deliberate change with new verification evidence. Do not automatically add a second sync protocol just because a dependency exists.
Suggested project structure
filelovenote/
server.js # bootstrap and start
src/app/ # context, request scope, service graph
src/http/ # router, auth, request body, responses
src/config/ # environment and feature flags
src/session/ # sessions
src/accounts/ # accounts and isolation
src/data/ # technical metadata and media data
src/routes/ # endpoints by feature
src/services/ # Drive, sync, media, search, migration
src/views/ # HTML, themes, navigation, dialogs
client/paper/ # editor, stores, Drive picker, canvas
tests/ # unit and integration tests
prompt/ # this transferable build package
.env.example # names, explanations, no secrets
bun.lock # locked dependencies
APP_DATA_DIR/ # outside the checkout in deployment
users.json, sessions.json
users/<userKey>/ # account-scoped technical data and media
... # no new readable note filesData model and ownership
Use 10 Data Structure for the field dictionary, logical table keys, relationships, synthetic records and compatibility limits.
| Object | Fields / contents | Storage location |
|---|---|---|
| Note | id, title, folderId, content (ProseMirror JSON), createdAt, updatedAt; optional questions and source references | IndexedDB; Markdown with metadata in Drive. Titles also count as note content. |
| Local outgoing operation | Account, id, operation, local revision token, payload, confirmed base and Drive revision | IndexedDB; clear only after this exact version is confirmed. |
| Drive index | fileId, revision, etag, hash, deleted, updatedAt, source, lastToken | The server may retain only these technical fields. |
| Folder | id, name, parentId, timestamps; deletion origin for legacy trash items | Browser in local mode; account-scoped folder metadata and Drive operations during sync. |
| Media file | Safe id, name, source, type, size, progress, tags, annotations, transcript status | Own media source; account-scoped metadata on the server. Keep this separate from note text. |
| Canvas | Cards, positions, connections, source ids | Own canvas store per account; the note storage contract does not silently change canvas storage. |
| Sync ticket | Id, encrypted payload/result, processing status | Temporary encrypted server packet; delete when the Drive result is retrieved. |
Read and write flows
- Open the local list and selected note first. Then refresh in the background. Without a cache, the initial loading state may end only when fetching finishes or an error is known.
- Save edits locally, with a durable queue and base version. Work without Drive remains local.
- Automatically send batches after a change, on return and on reconnection. A visible tab checks periodically, currently every twelve seconds. The button triggers the same flow.
- A push may return a result directly or, with
station: true, first return{ queued: true, ticket }. A ticket confirms receipt by the server, not yet storage in Drive. - Encrypt temporary packets with AES-256-GCM, a random nonce and a server secret; decrypt only in memory. The current implementation derives the key from a secret. This is not end-to-end encryption: the server can process the packet.
- Write at most eight notes to Drive at once. Create required folders first and delete folders last. Retrieve the outcome for each item; remove only the exact confirmed local operations and clean up the ticket.
- New text written during a push keeps its own revision and is sent afterward. BroadcastChannel and an account-scoped lock coordinate tabs.
Conflicts, outages and deletion
- Compare the local version, confirmed base and current Drive version. Use the etag when writing. A changed side takes priority over an unchanged side; independent text changes may be merged. Only incompatible changes to the same text create a controlled conflict copy.
- A 503, limit, lost response or missing list entry does not prove deletion. Check missing Drive files by id; do not delete an unconfirmed new local note.
- Unreadable Markdown or an HTML error page must not overwrite valid local text or a merge base. Show the problem for that note.
- An old selection or load operation must not populate the editor after a new selection. Use an increasing load token. Deletion markers and queue operations use the same lock, so stale responses cannot bring a note back.
- Delete permanently removes the explicitly selected object after one confirmation. Removing a source reference keeps its original file. Library metadata removal and original-file destruction are distinct actions; apply the deletion scope table. Existing trash items retain separate restore/purge behavior; do not introduce a new general soft-delete rule.
- On pagehide, a bounded keepalive request may send the current editor content. The current web client uses at most 48 KB per request and 60 KB in total. This is extra protection; IndexedDB and retries are still needed.
- Migration is needed only when legacy data exists: import, save durably, read back and confirm before cleaning up the source. A read or status request must not start migration.
API boundaries
| Route family | Contract |
|---|---|
GET /notes; GET /files | Workspaces and shell; / redirects to /notes. |
POST /auth/google; GET /auth/google/callback; POST /logout | Start Google OAuth, validate the callback, create and end sessions. |
GET /api/notes/storage | Distinguish configured, connected and active note storage; show the connected Drive account. |
POST /api/drive-notes | Body { revisions: { [id]: revision } }; response { documents, revisions, hasMore }. At most 40 changed/missing notes per batch. |
POST /api/drive-notes/push | Body { upserts, deletes, folders, station? }; directly { notes, folders } or { queued: true, ticket }. Per-item result with a revision or conflict. |
GET /api/drive-notes/push-ticket?ticket=… | While processing: { queued: true, ticket }; then the processing result. Account-scoped. |
GET /api/notes; GET|POST /api/notes/:id | Existing note routes as a compatibility layer. Without Drive, no persistent server writes. |
GET /api/note-folders; GET|POST /api/folders | Note folder tree and file folders, respectively; browser mode works locally. |
POST /search | Notes and folders only; { items, total }. Current browser documents may be sent temporarily. |
GET /api/google-drive/status; GET /api/google-drive/list | Connection status and picker; includeAll=true for all types, with pagination. |
POST /api/library-items/delete; GET /api/trash | Targeted permanent deletion; manage legacy trash separately. |
GET /stream/:name; GET /raw/:name; GET /media/:name; GET /thumb/:name | Byte-range playback, inline viewing, downloads and thumbnails. |
POST /api/notes/:id/ai; GET|PUT|DELETE /api/notes/:docId/canvas/:canvasId | Explicit AI action and canvas; P05. |
This is a route overview. 11 API Contracts owns concrete item fields, examples, validation, errors, pagination and retry behavior; test browser/server against it. 12 Failure & Recovery owns state transitions, deletion scope and recovery. The legacy GET /api/drive-notes currently returns 409. Do not give it a new bulk download feature.
Media and integrations
Support audio, video, images, PDF and EPUB. Streaming responds correctly to normal, open-ended and suffix byte ranges with 206, and to invalid ranges with 416. Save playback progress per account. ffmpeg creates thumbnails; without the binary, show a placeholder.
The current local vault upload route is not a universal upload destination. Send active uploads to Google Drive when connected; without Drive, clearly show that uploading is unavailable. Test chunk order, interrupted uploads, name reservations and two simultaneous files with the same name. Drive media upload is not a Google HTTP batch; use resumable uploads. Batched note sync is an application request with bounded Drive writes.
Google sign-in and Google Drive use separate OAuth configurations. Drive requests read access for the picker and write access for app files. Without configuration, show a clear connect button or unavailable status. The inline Drive picker, source insertion and annotation flow are core FR-17 in P03/P04; Gemini, YouTube, canvas and Whisper are separate extensions and must not block the core.
Configuration and execution
# Example for a new local reconstruction; do not copy an existing .env.
PORT=3000
APP_DATA_DIR=./runtime-data
SESSION_SECRET=<create-your-own-long-random-secret>
SESSION_COOKIE_SECURE=false
FOLDERS_ENABLED=true
LOCAL_FIRST_ENABLED=false
# Required for sign-in: your own Google OAuth client
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=
# Optional: separate Google Drive OAuth
GOOGLE_DRIVE_CLIENT_ID=
GOOGLE_DRIVE_CLIENT_SECRET=
GOOGLE_DRIVE_REDIRECT_URI=
# Only for selected extensions
GEMINI_API_KEY=
GEMINI_MODEL=
YOUTUBE_API_KEY=
LOCAL_FIRST_ENABLED is a legacy server sync flag; local IndexedDB storage must work without it. The examples are choices for a new build, not claims about production settings. Follow the pinned setup and OAuth instructions, including fail-closed production configuration and the backup/restore matrix. After implementation, start with bun install --frozen-lockfile and bun run dev; run bun run test. In production, use HTTPS, secure cookies, Google OAuth and a data directory outside the checkout.