A self-hosted, open-source Google Keep alternative. Fast, colorful notes with checklists, photo/video/audio attachments, and labels — running entirely on your own hardware, backed by your own Postgres database.
Try the live demo → — a static build with a mocked, browser-only backend (see Demo mode) so you can click around without installing anything.
No subscriptions, no ads, no third party reading your notes — just your data, on your server.
- 📝 Notes & checklists — pin, color, archive, trash (with undo everywhere it matters)
- 🖼️ Attachments — photos, videos, and audio recorded straight from the browser
- 🔗 Link previews — every URL in a note gets a card in both the grid and the open note: the OpenGraph image, or the most prominent image on the page when there's no OG tag, falling back to the site's favicon and then a plain domain tile — so there's always something to show. The image and favicon are fetched and re-served by Cleep, so cards render even behind a strict
img-src 'self'CSP and off-site hosts never see the reader's IP - 🏷️ Labels for organizing notes into collections, plus multi-select bulk actions
- 🔍 Search across your whole library, plus filter by one or several collections at once (Any/All)
- ⌨️ Keyboard shortcuts —
/to search,cnew note,lnew checklist,g n/a/tto jump around,?for the full list - 👥 Multi-user accounts with session-based auth — everyone gets their own private notes; admins can reset a locked-out user's password
- 📦 Import from Google Keep and export everything as a zip — your data is never trapped
- 📱 Installable as a PWA — add it to your home screen and it works offline (needs HTTPS, see below)
- 🐳 One
docker compose up— Postgres and the app, nothing else to configure
-
Clone the repo and set up your environment file:
git clone https://github.com/blindpassasjer/cleep.git cd cleep cp .env.example .envOpen
.envand fill in:SESSION_SECRET— generate one withopenssl rand -hex 32POSTGRES_PASSWORDandDATABASE_URL— keep the password in sync between the twoADMIN_EMAIL/ADMIN_PASSWORD(optional) — bootstraps your first account on startup, so you can sign in immediately instead of building a registration flow
Every variable is documented inline in .env.example, including
COOKIE_SECUREandTRUST_PROXYfor deployments behind a reverse proxy, and the optionalAUTH_RATE_LIMIT_MAX/UPLOAD_RATE_LIMIT_MAXknobs for tuning the rate limiter. -
Start it:
docker compose up -d
-
Open
http://<host>:6169and sign in with the admin account you configured above.
That's it — Postgres migrations run automatically before the app starts.
git pull # if you track the repo (for docker-compose.yml + scripts)
docker compose pull
docker compose up -dSchema migrations run automatically on container start. No .env changes are required — every
setting added since your last pull is optional and defaults to the previous behavior. Two things
worth knowing:
- If you already run behind TLS with
COOKIE_SECURE=true, Cleep now also sends anHSTSheader (6-month max-age). That's correct for an HTTPS deployment but means browsers will refuse plainhttp://to that hostname for a while — as before, only setCOOKIE_SECURE=truewhen TLS is actually terminated in front of Cleep. - If you deploy with this repo's
docker-compose.yml,git pullit so the newAUTH_RATE_LIMIT_*/UPLOAD_RATE_LIMIT_*passthrough lines are present (they default to empty and are harmless if missing).
Everything is stored in plain, host-visible folders instead of opaque Docker volumes, so you can browse, back up, or move it like any other files:
| What | Where |
|---|---|
| Notes, users, labels (Postgres) | ./data/postgres |
| Photos, videos, audio recordings | ./data/attachments/<user-id>/<note-id>/<file> |
scripts/backup.sh writes a timestamped folder (a pg_dump of the database + a tar of
./data/attachments) under ./backups. Run it from the directory with docker-compose.yml:
./scripts/backup.sh # -> ./backups/<timestamp>/
./scripts/backup.sh --keep 7 # prune to the 7 newest afterwardsRestore one with ./scripts/restore.sh ./backups/<timestamp> (it prompts before overwriting;
pass --yes to skip). A daily backup is one cron line: 0 3 * * * cd /srv/cleep && ./scripts/backup.sh --keep 14.
Settings → Your data has an Export all data button (a .zip of every note, label and
attachment) and an Import from Google Keep picker — export your notes from
Google Takeout (select Keep), then upload the resulting .zip.
Titles, checklists, colors, labels, pin/archive state and media all come across; trashed Keep
notes are skipped.
The docker-publish.yml GitHub Actions workflow builds and pushes
ghcr.io/blindpassasjer/cleep on every push to main. GHCR packages default to private on
their first publish, even in a public repo — after the first workflow run, open the package
settings on GitHub and set its visibility to public so docker compose pull works without
authentication.
The live demo is a static build deployed to GitHub
Pages by .github/workflows/deploy-demo.yml on every push to
main. GitHub Pages can only serve static files, so the demo build swaps the real Express/Postgres
API (src/api/client.ts) for a mock (src/api/mockClient.ts) that runs entirely in the browser:
notes and labels are saved to localStorage on your own device, attachments live only in memory
for the session, and there's no real login, multi-user, or admin behavior. Nothing is sent to a
server. Use "Reset demo data" in Settings to start over.
Build it yourself with:
VITE_DEMO=true npm run buildTo run the demo as a live dev server instead — e.g. behind a reverse proxy on a test host —
use npm run dev:demo. It starts Vite with the mock backend, serves from the domain root
(not the /cleep/ subpath the Pages build uses), trusts test.manriquez.no, and points HMR
back through that host's TLS. It also binds Vite's port with strictPort, so if another
project already has dev:demo running there this one exits with "Port 5173 is already in
use" — stop the other one first, and whichever is running is what test.manriquez.no shows.
Change the host by editing VITE_DEV_ORIGIN in package.json, or the port
with PORT=… npm run dev:demo.
Cleep is installable as a Progressive Web App — an "Install"/"Add to Home Screen" prompt, its own
window, and an offline app shell. This, like microphone access for audio recordings, only works
over a secure context (HTTPS, or localhost). Browsers won't register a service worker at all
on a plain http://<nas-ip>:6169 origin, so with the default setup above neither installability
nor offline support will be available — the app itself still works fine either way.
To unlock both, put a reverse proxy with a TLS certificate in front of Cleep — Caddy, Traefik,
Nginx Proxy Manager, or your NAS's built-in one all work well. Once you do, also set
TRUST_PROXY=true in .env — otherwise every request looks to the app like it's coming from the
proxy's own IP, which shares the login rate limiter across all visitors and can trip a
"Too many requests" error after perfectly normal use.
npm install
npm run server:dev # API server on :6169
npm run dev # Vite dev server on :5173, proxies /api to :6169Point DATABASE_URL at a local Postgres instance, set SESSION_SECRET and ATTACHMENTS_DIR (any
local folder for uploaded files), then run npm run db:migrate before starting the server.
To develop the frontend against a remote backend instead of running npm run server:dev locally,
set VITE_API_PROXY_TARGET in .env (e.g. VITE_API_PROXY_TARGET=https://test.manriquez.no) and
just run npm run dev.
DATABASE_URL_TEST=postgres://cleep:cleep@localhost:5432/cleep_test npm testnpm test runs the Vitest server-integration suite against a real Postgres (it creates and
truncates tables itself — point DATABASE_URL_TEST at a throwaway database). CI
(.github/workflows/ci.yml) runs lint, both builds, and the tests
against a Postgres service container on every push and PR.
React 18 · Express · PostgreSQL · Drizzle ORM · TypeScript · Vite — no framework lock-in, no managed cloud service required, just a small, readable codebase you can actually audit.
Issues and pull requests are welcome — this is a small enough codebase that most changes are straightforward to review.


