Overview
NoteFlow's "Bring Your Own Sync" feature lets users configure a custom HTTP endpoint to sync their notes across devices. Your server needs to implement just two operations at a single URL:
- GET — Return notes modified since a given timestamp
- POST — Accept notes that are newer on the client
That's it. No WebSockets, no OAuth, no complex handshakes. A single URL, two HTTP methods, JSON in and out.
Authentication
All requests include an API key in the X-API-Key header. The user configures this key in the extension settings alongside the endpoint URL.
X-API-Key: your-secret-key-here
How you validate this key is up to you — a simple static token check, a database lookup, or a full auth layer. The extension doesn't care as long as a 200 response comes back with the expected shape.
GET — Fetch Notes
The extension calls this to pull down any notes that have been modified since the last sync.
Request
GET {syncUrl}?last_sync={unix_timestamp_seconds}
X-API-Key: {key}
| Parameter | Location | Description |
|---|---|---|
last_sync |
Query string | Unix timestamp in seconds. Return all notes modified after this time. Will be 0 on first sync. |
X-API-Key |
Header | User-configured API key for authentication. |
Response
{
"status": "success",
"server_time": 1719000000000,
"notes": [
{
"video_id": "dQw4w9WgXcQ",
"video_title": "Rick Astley - Never Gonna Give You Up",
"markdown": "# My notes\n\n[0:15] Intro starts here",
"updated_at": "1719000000000"
}
]
}
| Field | Type | Description |
|---|---|---|
status |
string |
"success" on success |
server_time |
number |
Current server time in milliseconds (epoch). The extension stores this and sends it as last_sync on the next request (converted to seconds). |
notes |
array |
Notes modified since last_sync. Empty array if nothing changed. |
notes[].video_id |
string |
YouTube video ID (e.g. dQw4w9WgXcQ) |
notes[].video_title |
string |
Human-readable video title |
notes[].markdown |
string |
Full note content in Markdown |
notes[].updated_at |
string |
Last modified time in milliseconds (epoch), as a string |
POST — Push Notes
The extension sends locally-modified notes to your server. These are notes that have a newer updatedAt than what the server last reported.
Request
POST {syncUrl}
Content-Type: application/json
X-API-Key: {key}
{
"notes": [
{
"videoId": "dQw4w9WgXcQ",
"videoTitle": "Rick Astley - Never Gonna Give You Up",
"markdown": "# Updated notes content",
"updatedAt": 1719000000000
}
]
}
| Field | Type | Description |
|---|---|---|
notes[].videoId |
string |
YouTube video ID |
notes[].videoTitle |
string |
Video title |
notes[].markdown |
string |
Note content in Markdown |
notes[].updatedAt |
number |
Last modified time in milliseconds (epoch) |
Response
{
"status": "success"
}
snake_case (video_id, updated_at) while the POST body uses camelCase (videoId, updatedAt). Your server must handle both conventions.
Sync Behavior
- Manual trigger — Sync only runs when the user clicks "Sync Now" in the extension. There's no background polling or automatic sync.
- Last-write-wins — Conflict resolution is based on
updatedAttimestamps. Whichever side has the more recent timestamp wins. - No tombstones — When a user deletes a note locally, it's removed from local storage but no deletion event is sent to the server. If the note still exists on the server, it will reappear on the next sync.
- Delta sync — Only notes modified since
last_syncare exchanged in either direction.
Sync Flow
- Extension sends GET with the stored
last_synctimestamp - Server returns notes modified since that time
- Extension merges server notes into local storage (last-write-wins)
- Extension sends POST with any local notes that are newer than the server's versions
- Extension stores the
server_timefrom the GET response for next sync
Error Handling
The extension checks for a 200 status code and a JSON body with "status": "success". Anything else is treated as a failure and shown to the user as a sync error.
Recommended error responses:
{
"status": "error",
"message": "Invalid API key"
}
{
"status": "error",
"message": "Database connection failed"
}
The extension doesn't retry automatically — the user will need to click "Sync Now" again.
Implementation Checklist
Here's the minimum you need to get a working sync backend:
- A single URL that accepts both GET and POST
- Validate the
X-API-Keyheader on every request - Store notes keyed by
video_idwith theirupdated_attimestamp - GET: filter notes where
updated_at > last_sync(convert seconds → ms for comparison) - GET: return current server time in ms as
server_time - POST: upsert each note by
videoId, storing the newupdatedAt - Return
{ "status": "success" }on success with a 200 status - Return appropriate HTTP error codes with a JSON body on failure
That's roughly 50–100 lines of code in most languages. A SQLite database or even a JSON file on disk is plenty for personal use.