Self-host SnapPwd

The SnapPwd API, a self-hosted web client, the CLI and the agent skill are open source under the MIT license. You can run the API, Redis and the web client on your own machine with Docker Compose in a few minutes. This guide shows how, and what to configure before anyone else can reach your instance.

Commands on this page were last run on against the main branch of each repository. We make SnapPwd, so read the limitations section before you rely on it.

What is open source and what is not

Four repositories are public and MIT licensed. Together they are everything you need to run your own instance.

Open-source SnapPwd repositories
RepositoryWhat it isLicense
snappwd-serviceThe backend API, written in Rust with Axum. Stores encrypted payloads in Redis.MIT
snappwd-webA static web client in plain HTML, CSS and JavaScript, with no build step.MIT
snappwd-cliThe command-line client, published on npm as @snappwd/cli.MIT
snappwd-skillAn agent skill that teaches AI coding assistants to share secrets and files through SnapPwd.MIT

The web app at www.snappwd.io is a separate application and its source is not public. It uses the same API service (the snappwd-service README states this), but the self-hosted web client is a different and much smaller codebase. Self-hosting gives you the open-source components listed above. It does not give you a copy of the hosted site, and the two web clients do not have the same features (see known limitations).

Architecture

A self-hosted instance has three parts:

  • Web client. Static files served by nginx. It encrypts in the browser with the Web Crypto API (AES-GCM) and calls the API directly.
  • API. A single Rust binary. It validates requests, applies write limits and reads and writes Redis. It needs no other database.
  • Redis. Version 6.2 or later (the service uses GETDEL for one-time retrieval). Every record has a time to live, so data expires without a cleanup job.

What the server stores

  • The ciphertext your client uploads, the creation time and the expiry (60 seconds to 30 days).
  • Metadata, in plaintext. For files this is the original filename, the content type and the initialization vector. For secrets it is any optional metadata a client sends, such as a label. Anyone who has the ID can read this metadata with ?peek=true, without the key. Filenames and labels often reveal what a secret is, so leave sensitive details out.
  • A sender email address, only if a client asks for a reveal notification. It is kept in the same record, never returned by the API, and deleted with the record.

What the server never receives

  • The plaintext of a secret or file.
  • The decryption key. The clients put it in the URL fragment (after #), which browsers do not send to servers, and the CLI never includes it in a request.

This holds for the open-source clients. The API stores whatever string it is given, so a client that skips encryption would store plaintext. The security architecture page describes the model, and the API reference lists every field.

Quick start: the full stack

You need Docker with the Compose plugin, and git. Ports 80 and 8080 must be free. The compose file in snappwd-web builds the API from a sibling directory, so clone both repositories side by side:

mkdir snappwd-selfhosted && cd snappwd-selfhosted
git clone https://github.com/SnapPwd/snappwd-web.git
git clone https://github.com/SnapPwd/snappwd-service.git

cd snappwd-web
docker compose up -d

The first run compiles the Rust service, which takes a few minutes. When it finishes, three containers are running:

Containers started by the full-stack compose file
ServiceAddressNotes
webhttp://localhostThe web client (nginx, port 80).
apihttp://localhost:8080The API.
redisnot publishedReachable only from the other containers.

Open http://localhost, type a secret, and choose Encrypt & Create Link. The link looks like http://localhost/?id=sps-...#key=.... Opening it shows a confirmation screen first. The secret is fetched and deleted only when you choose to reveal it, and a second visit reports that the secret was not found or already viewed.

To stop the stack:

docker compose down      # keep the Redis volume
docker compose down -v   # also delete the Redis volume

This compose file is for trying SnapPwd out

It runs the stock redis:alpine image with a redis_data volume and no Redis configuration, so Redis keeps its defaults: snapshots are written to disk and there is no memory limit. Ciphertext can therefore outlive a container restart, up to its expiry. For anything beyond evaluation, give Redis the settings shown in going beyond localhost.

API only, with the CLI

If you only need the API, for example to use it from the CLI or your own client, start it from the service repository. This compose file also loads a redis.conf with a memory limit, an eviction policy and persistence turned off.

git clone https://github.com/SnapPwd/snappwd-service.git
cd snappwd-service
docker compose up --build -d

The API listens on http://localhost:8080. Store a secret and read it back with the CLI (needs Node.js):

npx @snappwd/cli put "hello" --api-url http://localhost:8080/v1
# Secret created successfully!
# URL: http://localhost:8080/v1/g/sps-...#...

npx @snappwd/cli get "<the URL printed above>" --api-url http://localhost:8080/v1
# hello

Running get a second time fails with API Error (404): Secret not found or already accessed, because retrieval deletes the record.

Configuration reference

The service is configured with environment variables. Defaults apply when a variable is unset.

snappwd-service environment variables
VariablePurposeDefault
PORTHTTP port to listen on.8080
REDIS_URLRedis connection string.redis://127.0.0.1:6379
MAX_FILE_SIZE_MBLargest encrypted file accepted, measured before base64 encoding.2
WRITE_REQUESTS_PER_MINUTEWrite attempts allowed per client IP in a 60-second window, shared by both POST routes and across replicas.10
STORAGE_MAX_BYTESRedis memory level above which new writes are refused.134217728 (128 MiB)
STORAGE_MAX_KEYSLargest number of keys in the Redis database, including rate-limit keys.10000
WRITE_API_TOKENBearer token required on both POST routes. At least 32 non-space ASCII characters.unset (anyone can write)
TRUSTED_PROXY_IPSComma-separated IP addresses of reverse proxies allowed to supply the client IP in X-Real-IP.empty
CLIENT_INFO_TOKENLets a trusted frontend that retrieves on behalf of visitors describe the visitor in reveal notifications. At least 32 non-space ASCII characters.unset
SMTP_HOST, SMTP_FROM, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORDOutgoing mail for sender notifications.unset (notifications off); port 587
RUST_LOGLog level, such as debug or info.info

The web client has one setting:

snappwd-web environment variables
VariablePurposeDefault
API_URLThe address the browser uses to reach the API. It must be reachable from the visitor, not just from the web container.http://localhost:8080 (set in docker-compose.yml)

In Docker, API_URL is written into env.js when the container starts. Without Docker, edit env.js yourself. The copy in the repository points at https://api.snappwd.io, so change it before serving the files, or your client will store secrets on the hosted API.

Going beyond localhost

HTTPS is required

The web client encrypts with the Web Crypto API, which browsers expose only on HTTPS pages. http://localhost is the one exception. To use your instance from another machine, serve both the web client and the API over HTTPS, for example behind a TLS-terminating reverse proxy, and set API_URL to the API's https:// address. An HTTPS page cannot call an http:// API. This guide does not include a tested reverse proxy configuration.

Abuse protection

Anyone who can reach the API can write to it unless you say otherwise. The service has these controls:

  • Write rate limit. WRITE_REQUESTS_PER_MINUTE applies per client IP. Invalid requests count too. When it is exhausted the API answers 429 with a Retry-After header in seconds. Reads are not limited.
  • Storage caps. STORAGE_MAX_BYTES and STORAGE_MAX_KEYS are checked before every write. When either is reached the API answers 503 until records are retrieved or expire.
  • Write token. Set WRITE_API_TOKEN and writers must send Authorization: Bearer <token>. Without it the API answers 401. Reading stays public, so recipients need no token. The CLI and the self-hosted web client cannot send this token today, so with it set only your own clients can create shares.
  • Client IP behind a proxy. By default the service uses the socket address and ignores forwarded headers, so every visitor behind a reverse proxy shares one write allowance. Set TRUSTED_PROXY_IPS to the proxy's address and have the proxy overwrite X-Real-IP with the verified client IP. Keep direct access to the API restricted.
  • Redis memory policy. The service never changes Redis settings. Its example redis.conf sets maxmemory 256mb, maxmemory-policy volatile-ttl and no persistence, with a 384 MiB container limit. Under memory pressure volatile-ttl can evict a share before its expiry. If records must survive until they expire, use noeviction and accept rejected writes instead. Redis Cluster is not supported.

The API answers cross-origin requests from any origin. The abuse protection and rollout section of the service README has the full details, including the Redis permissions the service needs.

Optional sender notifications

The API can email the sender when a secret or file is retrieved. A client asks for this by sending senderEmail when it creates a share. To turn the feature on, set SMTP_HOST and SMTP_FROM, and optionally SMTP_PORT (default 587). Authentication uses SMTP_USERNAME and SMTP_PASSWORD together. STARTTLS is required and the server certificate is verified. A partial or invalid configuration stops the service from starting.

  • Without SMTP configured, a request that includes senderEmail is refused with 503. Requests without it work as usual.
  • The email contains the ID, plus the IP address and user agent of whoever retrieved it. It never contains ciphertext, keys, metadata or a link.
  • Delivery is one best-effort attempt. There is no retry, so a mail server failure loses the notification.
  • Neither the CLI nor the self-hosted web client sends senderEmail. Only a client you write can use this.

Using the CLI with your instance

Every SnapPwd CLI command accepts --api-url. The value is your API's base URL including /v1:

npx @snappwd/cli put "Internal secret" --api-url http://localhost:8080/v1
npx @snappwd/cli put-file ./database.env --api-url http://localhost:8080/v1
npx @snappwd/cli peek "<url>" --api-url http://localhost:8080/v1
npx @snappwd/cli get "<url>" --api-url http://localhost:8080/v1

The link the CLI prints for a self-hosted API, such as http://localhost:8080/v1/g/sps-...#..., is built from the API URL. It is not a page you can open in a browser. Pass it back to get or peek with the same --api-url.

Known limitations

  • The self-hosted web client and the CLI cannot open each other's shares. They use different link and ciphertext formats. A CLI link has the form /g/<id>#<key>, while the web client only reads ?id=<id>#key=<key>, so opening a CLI link as it is does nothing. Do not rewrite a CLI share into the web client's link format: when you then choose to reveal it, the web client fetches the record, which deletes it, before decryption fails, and the share is lost. The CLI rejects a web client link without contacting the API. Read each share with the client that created it.
  • The self-hosted web client is smaller than the hosted app. It shares text only, with a choice of 1 hour, 1 day or 1 week. It has no file sharing, no custom expiry, no option to send the key separately and no reveal notifications, all of which the hosted app has. The API and the CLI do support files.
  • A write token locks out the stock clients. With WRITE_API_TOKEN set, the CLI and the web client cannot create shares, because neither can send the token.
  • Metadata is not encrypted. See what the server stores.
  • One-time means one retrieval, not one reader. Whoever fetches a share first gets it. Anyone holding the complete link, including the part after #, can decrypt it.

Found something that does not match this page? Open an issue on the snappwd-service or snappwd-web repository.