A lightweight, serverless note-taking web app written in TypeScript. Create, edit, and share notes with auto-save. Deploys on Cloudflare Workers (native KV) or Vercel Edge (Upstash Redis). No JS frameworks, no build step, zero dependencies — pure vanilla HTML/CSS/JS.
- Simple Note Editor: Lightweight web interface for creating and editing notes
- Auto-Save: Automatically saves note content every second
- Shareable URLs: Notes accessible via direct links with human-friendly WORDnn IDs
- Password-Protected Notes: Optional per-note password encryption; a protected note is deleted after its first successful read
- CLI Help and Version:
curlon the app root prints usage examples and the build version; the browser header displays the same version as a link to the GitHub repository - Favicon:
/favicon.icoserves the app icon - Multi-Deployment: Cloudflare Workers or Vercel Edge
- Zero Dependencies: No npm runtime deps — KV via native bindings (Cloudflare) or REST fetch (Vercel)
- XSS Protection: User content is HTML-escaped
- Responsive: Works on desktop and mobile
- Print Support: Print-friendly interface
- Node.js 24+
The app uses Wrangler (Cloudflare Workers) for local dev. KV is simulated by Miniflare — no account needed:
npm install
npm run devOpen http://localhost:8787.
Storage: Cloudflare KV — persistent, native, zero-config.
npx wrangler kv:namespace create "KV"Copy the ID from the output and paste it:
[[kv_namespaces]]
binding = "KV"
id = "your-namespace-id"npm run deployYour app is live at https://your-app.your-account.workers.dev with persistent KV storage.
Storage: Upstash Redis — persistent with 7-day TTL.
Create a Redis database at upstash.com (free tier). Copy the KV_REST_API_URL and KV_REST_API_TOKEN from the REST API section.
Alternatively, create via Vercel Dashboard: Storage → Create → Redis. This auto-injects the env vars.
npx vercel env add KV_REST_API_URL production --value <your-url>
npx vercel env add KV_REST_API_TOKEN production --value <your-token>npx vercel --prodvercel.json builds src/vercel.ts as an Edge Function and routes all requests to it. The app uses VercelKVStorage which connects to Upstash Redis via its REST API.
Your app is live at https://your-app.vercel.app with persistent KV storage.
The repository includes test and deployment workflows:
Runs on every push/PR to main:
npm ci→tsc --noEmit→npm test
Pushing a v* tag (for example, v0.3.1) deploys to the production Worker. Manual workflow_dispatch runs wrangler preview --name staging --ignore-base-config and produces a Cloudflare Preview URL without updating production. Preview uses a separate KV namespace. Both paths run typecheck and tests first; tag builds show the tag as the app version, while manual builds show the short commit SHA.
Required GitHub Actions secrets:
| Secret | Description |
|---|---|
CF_API_TOKEN |
Cloudflare API token with Workers permissions |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account ID |
CLOUDFLARE_KV_ID |
Production KV namespace ID |
CLOUDFLARE_PREVIEW_KV_ID |
Separate KV namespace ID for Worker Previews |
Pushing a v* tag deploys to Vercel Production. Manual workflow_dispatch deploys with --target preview. When the GitHub Actions secret VERCEL_AUTOMATION_BYPASS_SECRET is configured, the workflow uses curl with Vercel's protection-bypass header to smoke-test password-protected note creation, read-once, and burn behavior. Preview reads KV_REST_API_URL and KV_REST_API_TOKEN from the Vercel project's Preview environment; point these at a separate staging Redis database. Both paths run typecheck and tests first. Pushing a matching tag triggers both Cloudflare and Vercel production deployments.
Required secrets:
| Secret | Description |
|---|---|
VERCEL_TOKEN |
Vercel access token (create in Vercel Dashboard → Settings → Tokens) |
VERCEL_ORG_ID |
Your Vercel team/org ID (from .vercel/project.json) |
VERCEL_PROJECT_ID |
Your Vercel project ID (from .vercel/project.json) |
VERCEL_AUTOMATION_BYPASS_SECRET |
Vercel deployment protection automation bypass secret for Preview smoke tests |
To set up secrets: Go to your GitHub repo → Settings → Secrets and variables → Actions, then add each value. Run workflows via Actions tab → select workflow → Run workflow.
Retrieve and display a note.
Parameters:
noteId(path) ornote(query): Note ID (alphanumeric, 3–32 chars, no I/O/0/1)
Notes:
-
The preferred URL format is
/noteid/{noteId}for shell-friendly links (e.g.,http://example.com/noteid/BLAST47). -
Backwards compatibility:
/?note={noteId}still works. -
Links use the request host or
x-forwarded-*headers for reverse proxy support. -
Requests with
User-Agentcontaining "curl" return plain text instead of HTML. -
Running
curl https://your-app.com/prints terminal usage examples for text, files, and password-protected notes. Browser requests to/still show the note editor. -
The CLI help and browser badge use the build version;
/favicon.icoserves the GIF favicon. -
Unprotected notes retain the existing behavior. A protected note shows a password prompt in a browser and does not return content before successful verification.
-
A protected note can be read once with the correct password. Its encrypted content is removed after successful decryption; a later reload reports that it has already been read.
-
HEADrequests and incorrect passwords do not consume a protected note. -
CLI clients can provide the note-specific password in the
X-Note-Passwordheader. A successfulcurlGET returns plaintext and consumes the note:curl -H "X-Note-Password: $NOTE_PASSWORD" https://your-app.com/noteid/BLAST47
Save or delete a note.
Request body (JSON):
{
"noteId": "BLAST47",
"content": "Note content here",
"password": "optional password for a newly created protected note"
}For an existing protected note, password must be its current password. newPassword is optional and rotates the password while saving content:
{
"noteId": "BLAST47",
"content": "Updated note content",
"password": "current note password",
"newPassword": "optional replacement password"
}Response (JSON):
{
"success": true,
"noteId": "BLAST47"
}Behavior:
- If
noteIdis empty, a random WORDnn ID is generated (e.g., "BLAST47") - If
contentis empty or whitespace-only, the note is deleted - Otherwise, the note is saved
- Supplying a
passwordin JSON orX-Note-Passwordheader when creating a note enables password protection and automatically enables burn-after-read. Passwords must be at least 8 characters. - Password protection can only be selected when creating the note. It cannot be added to an existing unprotected note or removed from a protected note.
- Updating a protected note requires its current password. If
newPasswordis omitted, the current password is retained. Updating after a read republishes the note under the same ID, making it unread again. - Browser unlock uses a JSON
POSTwithaction: "unlock",noteId, andpassword; the response contains plaintext only after successful password verification.
Examples:
# Create new note
curl -X POST https://your-app.com/ \
-H "Content-Type: application/json" \
-d '{"content":"Hello World"}'
# Returns URL for curl requests
curl -X POST https://your-app.com/ \
-H "Content-Type: application/json" \
-H "User-Agent: curl/8.0" \
-d '{"content":"Hello World"}'
# → https://your-app.com/noteid/BLAST47
# Create a password-protected note (use a strong, unique passphrase)
echo "Secret note" | curl -sL --data-binary @- \
-H "X-Note-Password: $NOTE_PASSWORD" \
https://your-app.com/
# Read the protected note once; a successful request burns it
curl -H "X-Note-Password: $NOTE_PASSWORD" \
https://your-app.com/noteid/BLAST47.
├── src/
│ ├── workers.ts # Cloudflare Workers entry point
│ ├── vercel.ts # Vercel Edge entry point
│ ├── handler.ts # HTTP handlers (GET, POST, OPTIONS)
│ ├── handler.test.ts # Handler unit tests
│ └── lib/
│ ├── template.ts # Full HTML/CSS/JS UI template
│ ├── version.ts # Shared package version
│ ├── crypto.ts # Per-note password derivation and AES-GCM encryption
│ ├── crypto.test.ts # Protected-note cryptography tests
│ ├── storage.ts # Storage interface + KV / Vercel KV backends
│ ├── storage.test.ts # Storage unit tests
│ ├── utils.ts # Note ID generation/validation, HTML escaping, ClientIP
│ └── types.ts # TypeScript type definitions
├── wrangler.toml # Cloudflare Workers configuration
├── vercel.json # Vercel deployment configuration
├── .vercelignore # Files excluded from Vercel deployment
├── .github/workflows/ # GitHub Actions CI/CD workflows
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── README.md # This file
| Command | Description |
|---|---|
npm run dev |
Start local dev server with Wrangler |
npm run deploy |
Deploy to Cloudflare Workers |
npm test |
Run Vitest unit tests |
npm run typecheck |
Run TypeScript type check (tsc --noEmit) |
Note IDs use a WORDnn format — a random dictionary word (3–5 uppercase letters, excluding ambiguous characters I/O/0/1) followed by 2 digits. Examples: BLAST47, ACE23, ZEBRA99.
Both platforms use persistent KV storage with 7-day TTL. Cloudflare Workers uses native Workers KV via env.KV. Vercel Edge uses Upstash Redis via REST API (KV_REST_API_URL / KV_REST_API_TOKEN). Both implement the same Storage interface.
npm test # Run Vitest
npm run typecheck # TypeScript type checking- Input validation: Note IDs restricted to
[A-HJ-NP-Z2-9]{3,32} - XSS protection: User content server-side HTML-escaped before rendering
- Password-protected note content is encrypted with AES-GCM using a per-note key derived with PBKDF2-SHA-256 and a random salt; passwords are not stored in plaintext.
- A protected note is single-read. After successful unlock, the ciphertext is removed; a short-lived verifier tombstone remains so authenticated edits can republish the same note ID until its seven-day expiry.
- The server receives the password and decrypts the note to serve browser and CLI clients. This protects stored KV values from a KV-only data leak, but is not end-to-end encryption; the running application can access plaintext.
- Passwords are unrecoverable. Use a long, unique passphrase; a KV dump allows offline password guesses against the verifier.
- No third-party frontend dependencies
MIT License — feel free to use and modify for your needs.