A self-hosted collection of simple, link-based web utilities. No accounts, no tracking — just create something and share the link.
- Note Sharing — Create and share plain text or Markdown notes
- Date Poll — Doodle-style scheduling with a time grid picker
- Expense Share — Split costs in a group with per-participant links and settlement calculation
- One-Time Secret — Client-side encrypted messages that self-destruct after one view
- File Share — Upload files and share them via link, with configurable expiration
- Password Generator — Local-only alphanumeric passwords and XKCD-style passphrases in English or German
- Clock — Parameter-based digital, Swiss railway analogue, and countdown clock displays; analogue mode adapts Swiss-Railway-Clock by Manuel Meister
- Potluck Planner — Coordinate who brings what, with real-time claim tracking
- Micropage — Build a complete styled page whose entire content lives in the URL; nothing is stored server-side
- Server: Fastify with EJS templates
- Database: SQLite via better-sqlite3
- Frontend: Server-rendered HTML, HTMX for partial updates, vanilla JS
- Build: esbuild bundles the server into a single file
- Client-side crypto: Web Crypto API (AES-256-GCM) for the secrets tool — the server never sees plaintext or the encryption key
- Node.js 20+
git clone <repo-url> && cd microtools
npm installnpm run dev
# Server starts on port 3459 by default
# Override with: PORT=3000 npm run devnpm run build # Bundles to dist/index.mjs, vendors htmx + qrcode to public/
npm run start # Runs the production bundleThe build script copies htmx, qrcode-generator, and the IBM Plex Sans font files (via @fontsource/ibm-plex-sans) from node_modules into public/ so they are served locally instead of from CDNs. These generated files are gitignored — run npm run build after cloning to produce them.
The interface is available in English and German. The language belongs to the viewer, not to the link, because links get passed on: an explicit ?lang=de (remembered in a lang cookie) wins, then the browser's Accept-Language, then English. The switcher in the header uses the same ?lang= parameter. It is left off the one-time secret view, where a reload would destroy the secret.
Every user-facing string lives in server/locales/. Templates and server code call t('key', params); parameters are HTML-escaped inside t(), so locale files may contain markup but user input never reaches the page unescaped. Keys under js. are sent to the browser as window.T for the client-side scripts.
Micropage is the exception: a rendered page is a pure function of its URL, so its language is part of the link (lde, default len) and sets the fixed words a template adds. Slot content is never translated.
The Micropage documentation at /micropage/docs is the reference for people and agents alike and follows the viewer's language; /micropage/docs.md is the same content as plain Markdown. /micropage/agents is the builder's data source (templates, colours, limits as JSON), not a separate API.
- Copy
server/locales/de.tstoserver/locales/<code>.tsand translate the values. It is typed againsten.ts, sonpm run checkfails on any missing or misspelled key. - Register it in
server/i18n.ts: theLangtype,LANGS,TABLES,LOCALES, andpluralRules. - Keep established English terms where the language has no natural word of its own.
| Variable | Default | Description |
|---|---|---|
PORT |
3459 |
HTTP listen port |
NODE_ENV |
— | Set to production by the build script |
microtools/
├── server/
│ ├── index.ts # All routes and application logic
│ ├── i18n.ts # Language resolution, t(), date and money formatting
│ ├── locales/ # en.ts defines the keys, de.ts is typed against it
│ ├── db.ts # SQLite setup and schema
│ └── objectStore.ts # Generic CRUD for JSON objects in SQLite
├── views/
│ ├── _head.ejs # Shared <head> contents (styles, htmx, QR rendering)
│ ├── _header.ejs # Site header: wordmark plus a per-tool breadcrumb
│ ├── layout.ejs # Shared HTML shell (used by creation forms)
│ ├── index.ejs # Landing page
│ ├── 404.ejs # Not-found page
│ ├── notes/ # Note templates (show, md, new)
│ ├── polls/ # Poll templates (show, new, _container partial)
│ ├── expenses/ # Expense templates (show, participant, new, _entries partial)
│ ├── secrets/ # Secret templates (show, gone, new)
│ ├── files/ # File share templates (show, gone, new)
│ ├── clock/ # Clock config + display templates
│ ├── passwords/ # Password generator template
│ ├── bring/ # Potluck templates (show, new, _list partial)
│ └── micropage/ # Micropage render, docs, builder, error
├── public/
│ ├── style.css # All styles (single file, no build step)
│ ├── clock-config.js # Client-side clock link generator
│ ├── clock-display.js # Client-side clock renderer
│ ├── password-words.js # Vendored local word lists for the password generator
│ ├── passwords.js # Client-side password generator logic
│ ├── htmx.min.js # Vendored from node_modules (gitignored)
│ └── qrcode.min.js # Vendored from node_modules (gitignored)
├── script/
│ └── build.ts # esbuild production bundler + vendor script copier
├── data/ # Runtime data (gitignored)
│ ├── store.db # SQLite database (auto-created)
│ └── files/ # Uploaded file storage
├── dist/ # Build output (gitignored)
│ └── index.mjs # Production bundle
├── package.json
└── tsconfig.json
Single-table storage. All tools share one objects table with columns id, type, data (JSON), created_at, and expires_at. The objectStore module provides typed CRUD operations. This keeps things simple — no migrations, no ORM.
Templates. Most view templates are standalone HTML documents (with their own <head>) rather than using the shared layout. This was a deliberate choice to avoid the limitations of EJS's include system with template literals. Creation form pages (*/new.ejs) still use layout.ejs.
HTMX partials. POST routes return HTML fragments that HTMX swaps into the page. Templates prefixed with _ (like _container.ejs, _entries.ejs, _list.ejs) are partials rendered after mutations.
Client-side encryption. The secrets tool encrypts in the browser using AES-256-GCM. The key is placed in the URL fragment (#...), which browsers never send to the server. The server only stores ciphertext.
File storage. Uploaded files are stored on disk under data/files/<share-id>/. Metadata lives in SQLite. Expired shares are cleaned up both lazily (on access) and periodically (hourly timer).
URL as storage. The micropage tool has no database row at all — a page's template, theme and every word of its content are encoded in one p query parameter, so the URL is the document. Its parser reads request.raw.url and splits on ~ before percent-decoding, which is what makes %7E a literal tilde distinct from the delimiter and stops + silently becoming a space. Because it renders attacker-authorable markup on our own origin, /micropage sends a strict route-scoped CSP (script-src 'none', form-action 'none') and its query string is redacted from request logs.
Markdown safety. marked does not sanitize URLs and does not escape image alt text, so server/safeMarkdown.ts overrides the link and image renderers: schemes are allowlisted after entity-decoding, and attributes are escaped. A rejected renderer override must return escaped text rather than false, since false falls through to marked's own vulnerable renderer. Covered by npm run test:safety.
QR codes. Share links include an auto-generated QR code rendered client-side using qrcode-generator, vendored locally from npm. No server-side image generation.
The production build outputs a single dist/index.mjs that you can run with node. In production you'll want:
- A reverse proxy (nginx, Caddy, etc.) for TLS termination
- The
X-Forwarded-Protoheader set so generated URLs usehttps:// - A process manager (systemd, pm2, etc.) to keep the server running
- The
data/directory persisted and backed up (it contains the database and uploaded files)
npm run build
PORT=3000 npm run startMIT