{"id":13564692,"url":"https://github.com/computing-den/unforget","last_synced_at":"2025-12-29T23:43:51.033Z","repository":{"id":241919477,"uuid":"808203125","full_name":"computing-den/unforget","owner":"computing-den","description":"Unforget is a minimalist end-to-end encrypted note-taking app without Electron.js","archived":false,"fork":false,"pushed_at":"2024-10-19T16:23:01.000Z","size":4773,"stargazers_count":340,"open_issues_count":5,"forks_count":13,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-11-04T17:47:57.588Z","etag":null,"topics":["app","cloud","e2ee","e2ee-encryption","encrypted","minimal","minimalist","no-electron","note","note-taking","notes","offline","offline-first","privacy","progressive-web-app","pwa","react","todo"],"latest_commit_sha":null,"homepage":"https://unforget.computing-den.com/demo","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/computing-den.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-30T15:29:51.000Z","updated_at":"2024-11-01T15:32:37.000Z","dependencies_parsed_at":"2024-06-12T15:09:19.053Z","dependency_job_id":"955abcad-b3a6-49fb-8a6b-94d0957ead27","html_url":"https://github.com/computing-den/unforget","commit_stats":null,"previous_names":["computing-den/unforget"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/computing-den%2Funforget","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/computing-den%2Funforget/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/computing-den%2Funforget/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/computing-den%2Funforget/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/computing-den","download_url":"https://codeload.github.com/computing-den/unforget/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247083306,"owners_count":20880808,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["app","cloud","e2ee","e2ee-encryption","encrypted","minimal","minimalist","no-electron","note","note-taking","notes","offline","offline-first","privacy","progressive-web-app","pwa","react","todo"],"created_at":"2024-08-01T13:01:34.505Z","updated_at":"2025-12-29T23:43:51.024Z","avatar_url":"https://github.com/computing-den.png","language":"TypeScript","readme":"# Unforget\n\n![screenshot](doc/screenshots.png)\n\n*Start now without registering at [unforget.computing-den.com](https://unforget.computing-den.com/demo).*\n\nUnforget is a minimalist, offline-first, and end-to-end encrypted note-taking app (without Electron.js) featuring:\n\n- [x] Offline first\n- [x] Privacy first\n- [x] Progressive web app\n- [x] Open source MIT License\n- [x] End-to-end encrypted sync\n- [x] Desktop, Mobile, Web\n- [x] Markdown support\n- [x] Self hosted and cloud options\n- [x] One-click data export as JSON\n- [x] Optional one-click installation\n- [x] Public APIs, create your own client\n- [x] Import Google Keep\n- [x] Import Apple Notes\n- [x] Import Standard Notes\n\n\n*Unforget is made by [Computing Den](https://computing-den.com), a software company specializing in web technologies.*\n\n*Contact us at sean@computing-den.com*\n\n\n# Easy Signup\n\n[Sign up](https://unforget.computing-den.com/login) for free to back up your notes safely to the cloud fully encrypted and sync across devices.\n\n*No email or phone required.*\n\n# Optional installation\n\nUse it directly in your browser or install:\n\n| Browser         | Installation                |\n|-----------------|-----------------------------|\n| Chrome          | Install icon in the URL bar |\n| Edge            | Install icon in the URL bar |\n| Android Browser | Menu → Add to Home Screen   |\n| Safari Desktop  | Share → Add to Dock         |\n| Safari iOS      | Share → Add to Home Screen  |\n| Firefox Desktop | *cannot install*            |\n| Firefox Android | Install icon in the URL bar |\n\n# Organization and Workflow\n\nNotes are organized **chronologically**, with pinned notes displayed at the top.\n\nThis organization has proven very effective despite its simplicity. The **search is very fast** (and done offline), allowing you to quickly narrow down notes by entering a few phrases. Additionally, you can search for non-alphabetic characters, enabling the use of **tags** such as #idea, #project, #work, #book, etc.\n\nThere is **no limit** to the size of a note. For larger notes, you can insert a `---` on a line by itself to **collapse** the rest of the note.\n\nNotes are **immediately saved** as you type and synced every few seconds.\n\nIf you edit a note from two devices and a **conflict** occurs during sync, the most recent edit will take precedence.\n\n# Security and Privacy\n\nUnforget does not receive or store any personal data. No email or phone is required to sign up. As long as you pick a strong password, your notes will be stored in the cloud fully encrypted and safe.\n\nOnly your username and note modification dates are visible to Unforget servers.\n\n# Text Formatting\n\nThe main differences with the [Github flavored markdown](https://github.github.com/gfm/) are:\n- If the first line of a note is followed by a blank line, it is a H1 header.\n- Anything after the first horizontal rule `---` in a note will be hidden and replaced with a \"show more\" button that will expand the note.\n\n~~~\n# H1 header\n## H2 header\n### H3 header\n#### H4 header\n##### H5 header\n###### H6 header\n\n*This is italic.*.\n\n**This is bold.**.\n\n***This is bold and italic.***\n\n~~This is strikethrough~~\n\n\n- This is a bullet point\n- Another bullet point\n  - Inner bullet point\n- [ ] This is a checkbox\n  And more text related to the checkbox.\n\n1. This is an ordered list item\n2. And another one\n\n[this is a link](https://unforget.computing-den.com)\n\nInline `code` using back-ticks.\n\nBlock of code:\n\n```javascript\nfunction plusOne(a) {\n  return a + 1;\n}\n```\n\n\n| Tables        | Are           | Cool  |\n| ------------- |:-------------:| -----:|\n| col 3 is      | right-aligned | $1600 |\n| col 2 is      | centered      |   $12 |\n\n\nHorizontal rule:\n\n---\n\n\n~~~\n\n# Build and Self Host\n\nTo build Unforget for production, put a `.env` file in the project's root directory:\n\n```\nPORT=3000\nNODE_ENV=production\nDISABLE_CACHE=0\nLOG_TO_CONSOLE=0\nFORWARD_LOGS_TO_SERVER=0\nFORWARD_ERRORS_TO_SERVER=0\n```\n\nand then run\n\n```\ncd unforget/\nnpm run build\nnpm run start\n\n```\n\nIt is recommended to use [Nginx as a reverse proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) and set up SSL certificate using [Let's Encrypt](https://letsencrypt.org/).\n\n# Development\n\nTo build and run Unforget in development mode, put a `.env` file in the project's root directory:\n\n```\nPORT=3000\nNODE_ENV=development\nDISABLE_CACHE=1\nLOG_TO_CONSOLE=1\nFORWARD_LOGS_TO_SERVER=0\nFORWARD_ERRORS_TO_SERVER=0\n```\n\nand then run\n\n```\ncd unforget/\nnpm install\nnpm run dev\n\n```\n\nThis will build the project and watch for changes in the source files.\n\n# Public APIs - write your own client\n\nHere, all paths are relative to either the official server at [https://unforget.computing-den.com](https://unforget.computing-den.com) or your own server if you're self hosting.\n\n## Examples\n\nIn the [examples/](examples/) directory you will find example code for TypeScript and Python.\n\nTo run the **Typescript** example:\n\n``` bash\ncd examples/\n\n# Signup\nnpx tsx example.ts signup USERNAME PASSWORD\n\n# Login\nnpx tsx example.ts login USERNAME PASSWORD\n\n# Create new note\nnpx tsx example.ts create \"Hello world!\"\n\n# Get all notes\nnpx tsx example.ts get\n\n# Get note by ID\nnpx tsx example.ts get ID\n```\n\nTo run the **Python** example:\n\n``` bash\ncd examples/\n\n# Signup\npython3 example.py signup USERNAME PASSWORD\n\n# Login\npython3 example.py login USERNAME PASSWORD\n\n# Create new note\npython3 example.py create \"Hello world!\"\n\n# Get all notes\npython3 example.py get\n\n# Get note by ID\npython3 example.py get ID\n```\n\n## Note Types\n\n```ts\ntype Note = {\n\n  // UUID version 4\n  id: string;\n\n  // Deleted notes have text: null\n  text: string | null;\n\n  // ISO 8601 format\n  creation_date: string;\n  \n  // ISO 8601 format\n  modification_date: string;\n  \n  // 0 means deleted, 1 means not deleted\n  not_deleted: number;\n  \n  // 0 means archived, 1 means not archived\n  not_archived: number;\n  \n  // 0 means not pinned, 1 means pinned\n  pinned: number;\n\n  // A higher number means higher on the list\n  // Usually, by default it's milliseconds since the epoch\n  order: number;\n\n}\n\ntype EncryptedNote = {\n\n  // UUID version 4\n  id: string;\n\n  // ISO 8601 format\n  modification_date: string;\n  \n  // The encrypted Note in base64 format\n  encrypted_base64: string;\n  \n  // Initial vector, a random number, that was used for encrypting this specific note\n  iv: string;\n\n}\n\n```\n\nThe server only knows about ```EncryptedNote``` and never sees the actual ```Note```. So, the client must encrypt before sending to and decrypt after receiving notes from the server.\n\nSide note: the reason for using number (0 and 1) instead of boolean is to make it easier to store notes in SQLite which doesn't support boolean. And the reason why some of these fields are flipped (```not_deleted``` instead of ```deleted```) is to facilitate the use of IndexedDB which doesn't support indexing by multiple keys in arbitrary order.\n\n\n## Signup, Login, Logout\n\nTo sign up, send a POST request to ```/api/signup``` with a JSON payload of type ```SignupData```:\n\n```ts\ntype SignupData = {\n  username: string;\n  password_client_hash: string;\n  encryption_salt: string;\n}\n```\n\nTo log in, send a POST request to ```/api/login``` with a JSON payload of type ```LoginData```:\n\n```ts\ntype LoginData = {\n  username: string;\n  password_client_hash: string;\n}\n```\n\nIn both cases, if the credentials are wrong you will receive a 401 error. Otherwise, the server will respond with ```LoginResponse``` and code 200:\n\n```ts\ntype LoginResponse = {\n  username: string;\n  token: string;\n  encryption_salt: string;\n}\n```\n\nTo log out, send a POST request to ```/api/login?token=TOKEN```\n\nIn the following sections, all the requests to the server must include the ```token``` either as a query parameter in the URL (e.g. ```/api/delta-sync?token=XXX```) or as a cookie named ```unforget_token```.\n\nNotice that we never send the raw password to the server. Instead we calculate its hash as ```password_client_hash``` which is derived from the username, password, and a static random number. It is important to use the exact same algorithm for calculating the hash if you want to be able to use the official Unforget client as well as your own. The ```encryption_salt``` is a random number used to derive the key for encryption and decryption of notes. It is stored on the server and provided on login. The [examples](#examples) show how to calculate the hash and generate the salt.\n\n## Get Notes\n\nSend a POST request to ```/api/get-notes?token=TOKEN``` to get all notes. Optionally you can provide a JSON payload of type ```{ids: string[]}``` to get specific notes.\n\nYou will receive ```EncryptedNote[]```.\n\n## Merge Notes\n\nSend a POST request to ```/api/merge-notes?token=TOKEN``` with a JSON payload of type ```{notes: EncryptedNote[]}```.\n\nIf the note doesn't already exist, it will be added.\nIf its ```modification_date``` is larger than the existing note, it will replace the existing note.\nOtherwise, it will be thrown away.\n\n## Delete Notes\n\nTo delete a note set its `text: null` and `not_deleted: 0` and [merge](#merge-notes) it. This way, the stub will stay in the database and the fact that it was deleted will propogate to all the other clients.\n\n## Sync and Merge\n\nFor a long-running client, instead of using [Get Notes](#get-notes) and [Merge Notes](#merge-notes), you can use sync in the following manner.\n\nThe client and the server each maintain a queue of changes to send to each other as well as a sync number. The exchange of these changes is called a **delta sync**.\n\nThe sync number is 0 at login and is incremented by each side only after all the received changes have been merged and stored. At the start of each delta sync, if their sync numbers differ, it indicates that something went wrong in the last delta sync and so they must do a queue sync.\n\nA **queue sync** is when each side sends its sync number along with a list of IDs and modification dates of all the notes that it knows about. After a queue sync, both sides will know which changes the other side lacks and therefore can update their own queue and sync number.\n\nWhen the sync number is 0 (immediately after login), the server will send all notes in the first delta sync.\n\nTo perform a **delta sync**, send a POST request to ```/api/delta-sync?token=TOKEN``` with a JSON payload of type ```SyncData```:\n\n```ts\ntype SyncData = {\n  notes: EncryptedNote[];\n  syncNumber: number;\n}\n```\n\nIf the server agrees with the ```syncNumber```, it will respond with ```DeltaSyncResNormal``` which includes the changes stored on the server for that client since the last sync. Otherwise, the server will respond with ```PartialSyncResRequireQueueSync``` requiring the client to initiate a queue sync.\n\n```ts\ntype DeltaSyncResNormal = {\n  type: 'ok';\n  notes: EncryptedNote[];\n  syncNumber: number;\n}\n\ntype DeltaSyncResRequireQueueSync = {\n  type: 'require_queue_sync';\n}\n```\n\nTo perform a **queue sync**, send a POST request to ```/api/queue-sync?token=TOKEN``` with a JSON payload of type ```SyncHeadsData``` including the heads of all the notes known by the client and its sync number. You will then receive another ```SyncHeadsData``` including the heads of all the notes known by the server for that user along with the server's sync number for that client.\n\n```ts\ntype SyncHeadsData = {\n  noteHeads: NoteHead[];\n  syncNumber: number;\n}\n\ntype NoteHead = {\n  id: string;\n  modification_date: string;\n}\n```\n\nAfter a queue sync, each side updates its queue to include the changes the other side is mising as well as setting the new sync number to be the larger sync number + 1.\n\nIt is important that the client and the server agree on how the **merging** of the notes is done so that they end up with a consistent state. We say that note A must replace note B if ```A.id == B.id``` and ```A.modification_date \u003e B.modification_date```.\n\n## Encryption and Decryption\n\nThe details of encryption and decryption are more easily explained in code. See the [Examples](#examples) section.\n\n## Error handling\n\nAll the API calls will return an object of type ```ServerError``` when encountering an error with a status code \u003e= 400:\n\n```ts\ntype ServerError {\n  message: string;\n  code: number;\n  type: 'app_requires_update' | 'generic';\n}\n```\n\nIf you receive an error with type ```app_requires_update``` that indicates that you are using an older version of the API that is no longer supported.\n","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcomputing-den%2Funforget","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcomputing-den%2Funforget","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcomputing-den%2Funforget/lists"}