Browser TOTP generation · Two sharing modes · Text/JSON API · Self-hosted
Generate TOTP codes locally, create setup links, share temporary code access, or expose the current code through a stateless API.
- Generates TOTP codes in the browser with no account or database required.
- Supports SHA1, SHA256, and SHA512; 6- or 8-digit codes; and 15-, 30-, or 60-second periods.
- Creates
otpauth://QR codes for authenticator apps. - Offers client-only setup links and Redis-backed temporary code links.
- Returns the current code from
/api/codeas JSON or plain text. - Includes English and Simplified Chinese interfaces, plus light and dark themes.
- Ships with a multi-stage Docker image that uses Bun for dependency installation and Node.js 22 for production, plus an ephemeral Redis Compose service.
OTP Share deliberately provides three modes with different trust boundaries:
| Mode | URL | What the server receives | Lifetime and access |
|---|---|---|---|
| Setup link | https://host/#<secret> |
The HTTP request does not contain the fragment | The recipient receives the complete TOTP secret and can generate future codes until the secret is rotated |
| Temporary link | https://host/s/<id> |
Pre-generated OTP codes, never the TOTP secret | UI options are 1, 12, or 24 hours, with optional burn-after-reading |
| Direct code API | /api/code?secret=<secret> |
The TOTP secret on every request | Stateless; returns the code for the server's current time and does not intentionally persist the secret |
These modes are not interchangeable. A setup link protects the secret from the application server, but it gives the secret to the recipient. A temporary link limits what the recipient receives, while the direct API explicitly sends the secret to the server for calculation.
Requirements: Docker with the Compose plugin.
git clone https://github.com/Kadxy/OTP-share.git
cd OTP-share
docker compose up -d --buildOpen http://localhost:3000. The Compose deployment needs no environment file, database migration, or initialization command.
To use another host port:
PORT=8080 docker compose up -dUseful commands:
docker compose ps
docker compose logs -f app
docker compose up -d --build
docker compose downThe app container exposes only port 3000. Redis is reachable only from the internal Compose network.
For the default SHA1, 6-digit, 30-second configuration, a setup link contains only the normalized Base32 secret:
https://example.com/#JBSWY3DPEHPK3PXP
Non-default settings use this fragment format:
#<secret>.<algorithm-code>.<digits>.<period>
For example:
https://example.com/#JBSWY3DPEHPK3PXP.256.8.60
Algorithm codes are 1 for SHA1, 256 for SHA256, and 512 for SHA512. The three settings fields must be supplied together.
Setup-link fragments accept case-insensitive, unpadded Base32 secrets and ignore whitespace. This is intentionally stricter than the direct API, which also accepts valid Base32 padding.
URL fragments are not sent in HTTP requests or HTTP Referer headers. They are still visible to the recipient, browser history, the clipboard, browser extensions, screenshots, and any client-side code running on the page. Treat a setup link as the TOTP credential itself: it is not temporary and cannot be revoked without rotating the secret.
Legacy ?secret=... links are accepted for compatibility and rewritten to a fragment in the browser. Do not create new query-string setup links because the initial URL can be recorded by servers, proxies, and analytics systems.
Temporary links are created entirely from the browser:
- The sender's browser calculates the codes required for the selected window.
- Only those generated codes and timing metadata are sent to
/api/share. - Redis stores the payload under a random seven-character ID with a TTL.
- The recipient opens
/s/<id>and receives the code matching the current server time.
The UI offers 1-, 12-, and 24-hour windows. Burn-after-reading is enabled by default.
For burn-after-reading links, the first successful read atomically deletes the Redis payload and returns only a short display window of up to approximately three minutes to that first viewer. Later requests receive 410 Gone. The selected expiration primarily controls how long the unopened link remains available.
For reusable links, the first successful page load receives all codes remaining in the selected window. Once codes have reached a recipient's browser, deleting or restarting Redis cannot revoke that already-delivered data. The code window is aligned to TOTP periods, so its final usable code can end up to one period earlier than the Redis TTL.
Temporary links are bearer credentials: anyone who has the URL can open them. Generated codes are stored unencrypted in Redis memory; the underlying TOTP secret is not stored.
GET /api/code returns the current TOTP code using the application server's clock. It does not require Redis.
| Parameter | Required | Default | Accepted values |
|---|---|---|---|
secret |
Yes | — | Base32 secret; case-insensitive, whitespace ignored, valid padded or unpadded input, maximum raw length 1024 characters |
token |
No | — | Alias for secret; secret takes precedence when both are present |
algorithm |
No | SHA1 |
SHA1, SHA256, SHA512 |
digits |
No | 6 |
6, 8 |
period |
No | 30 |
15, 30, 60 seconds |
format |
No | json |
json, text |
When format is omitted, the endpoint returns JSON unless the Accept header explicitly includes an allowed text/plain media range.
curl 'http://localhost:3000/api/code?secret=JBSWY3DPEHPK3PXP&format=json'{
"code": "123456",
"algorithm": "SHA1",
"digits": 6,
"period": 30,
"remainingSeconds": 18,
"validFrom": "2026-07-12T10:29:30.000Z",
"validUntil": "2026-07-12T10:30:00.000Z"
}curl 'http://localhost:3000/api/code?secret=JBSWY3DPEHPK3PXP&format=text'123456
Plain-text success responses also include X-OTP-Remaining-Seconds and X-OTP-Valid-Until headers. Text bodies end with a newline for command-line use.
Invalid requests return HTTP 400 with one of these error codes:
MISSING_SECRETINVALID_SECRETINVALID_FORMATINVALID_ALGORITHMINVALID_DIGITSINVALID_PERIOD
Unexpected generation failures return HTTP 500 with INTERNAL_SERVER_ERROR. When format=text is valid, errors are returned as plain text; an invalid format always returns JSON.
The endpoint is intended for server-to-server, command-line, and same-origin use. It does not currently add cross-origin browser CORS headers, application-level authentication, or application-level rate limiting.
The API does not intentionally store the secret, but the secret is still sent through the application server in the URL. It may be recorded by browser history, shell history, reverse proxies, CDN/WAF products, APM tools, or access logs. Use HTTPS and redact the
/api/codequery string throughout the request path.
Requirements:
- Bun 1.3.8 or newer
- Redis only if you need to create or open temporary links
Install dependencies and start the app:
bun install
bun run devThe browser generator, setup links, QR generation, and /api/code work without Redis. To test temporary links, run a local Redis instance in another terminal or container. For example:
docker run --rm -d \
--name otp-share-dev-redis \
-p 127.0.0.1:6379:6379 \
redis:7.4-alpine \
redis-server --save "" --appendonly noThe default Redis URL is redis://127.0.0.1:6379. Copy .env.example to .env.local only when you need to override it.
cp .env.example .env.localDevelopment commands:
bun run dev
bun run lint
bun run buildRemove the example development Redis container with:
docker rm -f otp-share-dev-redis| Variable | Default | Scope |
|---|---|---|
REDIS_URL |
redis://127.0.0.1:6379 |
Application connection used only by temporary links; Compose sets it to redis://redis:6379 |
PORT |
3000 |
Docker Compose host-port substitution, as in PORT=8080 docker compose up -d |
When running the Docker image without the provided Compose file, set REDIS_URL to a reachable Redis instance if temporary links are required.
The provided Redis service is intentionally ephemeral:
- RDB snapshots and AOF are disabled.
/datais mounted as a 16 MBtmpfsrather than a disk-backed volume.- Redis
maxmemoryis set to 128 MB. This is a Redis dataset limit, not a container or process memory limit. - The
volatile-ttleviction policy may remove active links before their configured TTL when Redis is under memory pressure.
Restarting only the app container preserves active links. Restarting, replacing, or removing the Redis container removes all links. This deployment is designed for disposable sharing, not durable storage or high availability.
Multiple app replicas can share the same Redis instance behind a load balancer. The included Redis service itself is not replicated or configured for failover.
- Terminate HTTPS before exposing the application publicly.
- Deploy the app at the origin root, such as
https://otp.example.com/. The current client URLs and API calls do not support mounting the app below a subpath such as/otp-share. - Keep host, container, and user-device clocks synchronized with NTP. Browser codes use the device clock,
/api/codeuses the app-server clock, and temporary links rely on sender and server time alignment. - Bypass caching for
/api/code,/api/share, and/api/share/*. The routes also sendno-storeresponse headers. - Redact
/api/codequery strings and avoid client analytics that collect setup-link fragments or full URLs. - Apply rate limits to
/api/codeand the share endpoints at the CDN or reverse proxy. - Cap
/api/sharerequest bodies at 512 KiB or less before they reach the app. - Do not publish the Redis port to the public network.
- Remember that setup and temporary links are bearer credentials; there is no account-based access control.
- Next.js 16 App Router
- React 19 and TypeScript
- Bun 1.3.8 for package management and local development
- Node.js 22 in the production container
- Tailwind CSS 4
otplibfor TOTP generationnext-intlfor English and Simplified Chinese- Redis 7.4 for temporary links
- Docker and Docker Compose
Before opening a pull request, run:
bun run lint
bun run buildBug reports and feature requests are welcome through GitHub Issues.
Distributed under the MIT License. See LICENSE for details.