A fast, local-first notes app with image and video attachments, optional Facebook Page publishing, and end-to-end-encrypted backups to any S3-compatible bucket. Installable as a PWA and works offline.
- Local-first storage. Notes live in
localStorage; media (images & videos) live in IndexedDB as Blobs — no base64 inflation, browser-allotted quotas (often hundreds of MB up to several GB). - Smart media compression. Images are downscaled to 2048 px on the longest side and re-encoded as WebP (with JPEG fallback). Originals are kept only if smaller. Videos are stored as-is.
- Persistent storage. The app requests
navigator.storage.persist()so the browser does not evict your data under pressure. - Facebook Page publishing. Connect a Page Access Token + Page ID and publish notes (with attached photos or a video) directly from the editor. Edits to a published note can be synced back; posts can be deleted.
- Encrypted S3 backups. Notes and all referenced media blobs are bundled, encrypted with AES-GCM (PBKDF2-SHA-256, 150 k iterations, password-derived), and uploaded to an S3-compatible bucket. Restore on any device.
- Installable PWA. Auto-updating service worker, install prompt for Chromium/Android, manual hint for iOS.
- React 19 + TypeScript
- Vite 8 +
vite-plugin-pwa(Workbox) - Web Crypto, IndexedDB, Web Storage APIs (zero runtime deps for storage/crypto)
- Sync server (
server/) — Node +wsWebSocket relay backed by SQLite (better-sqlite3); an ephemeral mailbox that only relays client-encrypted blobs. Also handles the Facebook token exchange. Optional — the app works fully without it. - Nginx (production runtime) — serves the SPA and reverse-proxies
/wsand/apito the sync server
npm install
npm run dev # Vite dev server (default http://localhost:5173)
npm run build # tsc -b && vite build → ./dist
npm run preview # preview the built bundlePer-user settings (S3 credentials, theme, sync code) are entered at runtime in
Settings and stored in the browser only. A few environment variables
configure the optional sync server and Facebook integration — see
.env.example:
VITE_FB_APP_ID— build-time (baked into the frontend bundle); enables the "Connect Facebook Page" button.FB_APP_SECRET— server-only; used for the Facebook token exchange.SYNC_SERVER_KEY— server-only; signs WebSocket join tokens. If unset, a random key is generated at boot and all join tokens are invalidated on every restart, so set it in production.SYNC_PORT,SYNC_DATA_DIR— sync server port and SQLite data directory.
None are required to run the core local-first app; they only enable sync and Facebook publishing.
Open the ⚙ Settings panel from the sidebar:
- Facebook Page — click Connect Facebook Page to run the Facebook
Login flow. The browser obtains a short-lived user token, the sync server
exchanges it for permanent Page access tokens (requires
VITE_FB_APP_ID+FB_APP_SECRET), and you pick which Page(s) to connect. Page posts and insights are then called directly from the browser via Graph API v19. Requires thepages_manage_posts,pages_read_engagement,pages_show_list,pages_manage_metadataandbusiness_managementscopes. - S3 backup — region, bucket, access key, secret key, and an optional
endpoint. With no endpoint it targets AWS S3; set the endpoint to use any
S3-compatible provider (Cloudflare R2, Backblaze B2, MinIO, …). The bucket
must allow CORS
PUT/GET/LISTfrom your domain, and non-AWS endpoint hosts must be allowed by the page's CSPconnect-src(seenginx.conf). - Storage — see current usage versus the browser's quota and request persistent storage if it's still "best-effort".
The repo ships a production-ready Docker setup:
Dockerfile— multi-stage build (frontend + sync server) on a Node 20 Alpine runtime with nginx installed;entrypoint.shstarts both the sync server and nginxnginx.conf— SPA fallback, gzip, immutable cache for/assets/*, no-cache forindex.html,sw.js, and the manifest.dockerignore— keeps build context lean and prevents.envfiles from ever entering the image
- Create a new Application → choose your Git provider and point it at this repository (and the branch you want to deploy).
- Build type: select Dockerfile. Leave the build context as the repo
root and the Dockerfile path as
./Dockerfile. - Port: expose container port 80.
- Domain: attach your domain in Dokploy and enable HTTPS (Let's Encrypt). The PWA's installability and service worker require HTTPS in production.
- Environment variables: none are required for the core local-first app.
To enable sync and Facebook publishing, set the variables from
.env.example— notablySYNC_SERVER_KEY(persist it so join tokens survive restarts),VITE_FB_APP_ID(build-time) andFB_APP_SECRET. Mount a volume atSYNC_DATA_DIR(default/data/sync) so the sync SQLite database persists across deploys. - Deploy. On every push, Dokploy will rebuild the image and roll out a new container.
sw.js, registerSW.js, manifest.webmanifest, and index.html are served
with Cache-Control: no-cache so users always get the latest service worker;
hashed assets in /assets/* are served immutable, max-age=1y. The PWA is
configured with registerType: 'autoUpdate', so users will get the new build
on their next navigation after deploy.
The Dockerfile defines a HEALTHCHECK that hits http://localhost/. Dokploy
will use it to gate rolling deploys.
- Notes and media never leave your browser unless you explicitly publish to Facebook or upload an encrypted backup to your own S3 bucket.
- Backups are encrypted client-side with AES-GCM. The server (S3) only ever sees ciphertext; losing the password means losing the backup.
MIT