A zero-dependency, self-hosted REST API for encrypted notes —
plus a tool to rescue your data from protectedtext.com.
Features · Quick Start · Export Tool · API · Docs · Contributing
ProtectedText is brilliant — encrypted notes, no account, just a URL and password.
This project gives you the same idea on your own server, and a CLI tool to export everything you already have on protectedtext.com to plain files.
The server never sees your plaintext. Ever.
| 🔒 Zero plaintext storage | Ciphertext, IV, and salt only — the key never leaves the client |
| 📦 Zero npm dependencies | Pure Node.js 20 built-ins, nothing to npm install |
| 🔑 Scrypt-hashed auth tokens | Client-derived tokens; the server stores only a salted hash |
| ⚡ Optimistic concurrency | Version-gated writes prevent silent overwrites |
| 🛡️ IP rate limiting | In-memory sliding window, configurable per deployment |
| 📤 protectedtext.com exporter | Reverse-engineered Argon2id + AES decryption for all site types |
| 🧪 Fully tested | 5 automated tests, pure Node.js built-in test runner |
Requirements: Node.js ≥ 20 — no other dependencies.
git clone https://github.com/laikhtman/ProtectedText_API.git
cd ProtectedText_API
npm start✅ Listening on http://127.0.0.1:3000
That's it. No npm install. No Docker required.
npm test✔ creates a new site
✔ rejects wrong auth token
✔ rejects version mismatch
✔ rate limiter blocks over-limit requests
✔ rate limiter resets after window
▶ 5 tests passed (373ms)
Pull every tab from any protectedtext.com site and save each one as a .txt file — one command, no browser needed.
Requirements: Python ≥ 3.10. Dependencies are installed automatically on first run.
python export_site.py mysiteSite ID: mysite
Password: ••••••••
🔓 Decrypting... ✔ 33 tabs decrypted (legacy AES)
📁 Saved to mysite/
├── Shopping list.txt
├── Project ideas.txt
├── Work notes.txt
└── ... 30 more
python export_site.py --import sites.csvCSV format — no header row required:
siteId,password[,masterDirectory]| Column | Required | Description |
|---|---|---|
1 — siteId |
✅ | The protectedtext.com site identifier |
2 — password |
✅ | The site password |
3 — masterDirectory |
optional | Parent folder for output. Files go to <masterDir>/<siteId>/ instead of ./<siteId>/ |
Example sites.csv:
mynotes,secret123
worknotes,pass456,backup
family,pa$$w0rd,D:\exportsRunning --import sites.csv on the above would create:
backup\worknotes\*.txt
D:\exports\family\*.txt
mynotes\*.txt
Each site is fetched independently — a failure on one site is reported and the batch continues.
A folder named after the site ID is created in your working directory.
Each tab becomes a .txt file named after the tab's first line (the title).
✅ Supports both legacy (AES + plain password) and modern (Argon2id-chain, up to 10 iterations) protectedtext.com encryption.
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Server health check |
GET |
/api/v1/sites/:siteId |
Fetch an encrypted note |
PUT |
/api/v1/sites/:siteId |
Create or update an encrypted note |
DELETE |
/api/v1/sites/:siteId |
Delete a note |
GET /api/v1/sites/:siteId
GET /api/v1/sites/my-note{
"siteId": "my-note",
"version": 1,
"createdAt": "2026-04-24T08:00:00.000Z",
"updatedAt": "2026-04-24T08:00:00.000Z",
"ciphertext": "base64...",
"iv": "base64...",
"salt": "base64...",
"algorithm": "aes-256-gcm",
"kdf": "argon2id",
"noteHash": "sha256..."
}PUT /api/v1/sites/:siteId — create or update
PUT /api/v1/sites/my-note
Content-Type: application/json{
"ciphertext": "base64...",
"iv": "base64...",
"salt": "base64...",
"algorithm": "aes-256-gcm",
"kdf": "argon2id",
"authToken": "client-derived-secret",
"expectedVersion": 0,
"noteHash": "sha256..."
}expectedVersion: 0→ create a new siteexpectedVersion: N→ update; returns409 Conflicton mismatch
DELETE /api/v1/sites/:siteId
DELETE /api/v1/sites/my-note
Content-Type: application/json{
"authToken": "client-derived-secret",
"expectedVersion": 1
}All configuration is via environment variables:
| Variable | Default | Description |
|---|---|---|
HOST |
127.0.0.1 |
Bind address |
PORT |
3000 |
Port |
DATA_FILE |
data/sites.json |
Persistent storage path |
RATE_LIMIT_WINDOW_MS |
60000 |
Rate limit window (ms) |
RATE_LIMIT_MAX_REQUESTS |
60 |
Max requests per window per IP |
PORT=8080 DATA_FILE=/var/data/sites.json npm start- Client-side encryption only — the server derives nothing from the password
- Auth tokens — clients derive a token from their secret; server stores only a
scrypthash - No accounts — notes are addressed by
siteId, not user identity - Timing-safe equality — auth comparison uses
crypto.timingSafeEqual - Optimistic concurrency — version field prevents blind overwrites
See docs/security.md for a full breakdown and production hardening checklist.
- OpenAPI 3 spec + Swagger UI
- SQLite / PostgreSQL storage adapter
- Docker + docker-compose
- Browser client reference implementation
- Distributed rate limiting (Redis)
- Structured logging
Have an idea? Open an issue or submit a PR!
Contributions, issues, and feature requests are welcome!
See CONTRIBUTING.md to get started.
- Fork the repo
- Create a feature branch —
git checkout -b feature/amazing-feature - Commit your changes
- Push to your fork and open a PR
MIT © laikhtman — see LICENSE for details.
If this project helped you, please consider giving it a ⭐ — it helps others find it!