Fond Waypoints helps people leave a clear path for the ones they love. We believe that grief is hard enough without also having to search for a password, a policy number, or a note that was never written down. So we built a place to gather everything important — accounts, wishes, contacts, the small things only you would know — and mark the way, gently, for whoever needs to find it.
📖 Full documentation lives on the wiki — this README covers the basics and how to install it. See RELEASE_NOTES for release notes and version history.
- Zero-knowledge encryption. Everything sensitive — credentials, notes, documents — is encrypted and decrypted in your browser using the WebCrypto API. The server only ever stores ciphertext. Nobody with access to the database or the server process can read your vault, not even an administrator of this deployment.
- TLS is mandatory, not optional. Browsers only expose
crypto.subtleon a secure context (HTTPS, orlocalhost). Deploy this behind a TLS-terminating reverse proxy — it will not work correctly otherwise. - A lost master password means a permanently lost vault, unless you saved the optional Recovery Kit from Settings. There is no "forgot password" reset — that's what makes it zero-knowledge. Treat your master password (and Recovery Kit, if you generate one) like the only key to a safe deposit box.
- Emergency access, not a dead-man's switch. You designate trusted contacts ahead of time. If one of them ever needs in, they request access; you're emailed and can deny it any time within a waiting period (14 days by default, configurable per contact); if you don't, access is granted automatically. Nothing happens on a timer just because you went quiet — someone has to actually ask.
- Waypoints (
/vault) — credentials, contacts, notes/wishes, documents (with encrypted file attachments), places, emergency details, valuables/heirlooms, and checklists. Every field is encrypted client-side before it ever reaches the server, including the item's name. A credential's password field has a Show/Hide toggle (it's your own vault — masking-with-no-reveal would make it useless for actual recovery), and any website field renders as a clickable link once saved. - Checklists — a waypoint type for executor-facing to-do lists ("cancel Netflix, notify the
bank, call the attorney"). Drag other waypoints in from a type-grouped picker (or click them) to
add them as checkable line items; membership reuses the existing related-items link mechanism, so
a checklist also shows up connected to its contents in the web view (
/vault/web). Only the checked/unchecked state is checklist-specific data. - Tags — admin-curated organizational labels (e.g. "Financial," "Medical") that any user can
create on the fly via a search-and-create picker, and filter by. These are deliberately not
encrypted (they're plaintext organizational metadata, like the item type), which is why creation
is admin-moderated rather than fully freeform — see
/admin. - Settings (
/settings) — change your master password, generate/replace your Recovery Kit, and customize the app's appearance (main text, secondary text, background, accent, and top-menu colors, independently for light and dark theme) from the Appearance section. - Account recovery (
/auth/recovery) — recovers a forgotten master password using a saved Recovery Kit code. Verified server-side via a non-secret fingerprint of your vault key, so a fabricated recovery request can't silently corrupt your vault. - Trusted contacts / emergency access (
/trusted-contacts) — invite, accept, confirm, request access, deny, and revoke, plus a read-only viewer for a vault you've been granted access to. The waiting-period promotion runs automatically every ~15 minutes (services/jobs/scheduler.js). - Admin (
/admin,/admin/audit-log,/admin/updates) — manage the tag taxonomy, review a log of account and access-control events (logins, item changes, emergency-access lifecycle events), and check for newer releases (running vs. latest version, when it was last checked, a manual "Check now"). The same update check also surfaces to everyone as a flashing footer link when one's available, and as a card on the public/statuspage. - Backup & restore (
/settings,/auth/restore) — download a fully-encrypted export of your vault, and restore it either into the same account or as a brand new account on a rebuilt server. Admins can also schedule automatic whole-server backups (/admin), restorable onto any deployment — even a fresh one on different MongoDB entirely — with a CLI script. See "Backup and Restore" on the wiki.
Light and dark themes
| Light | Dark |
|---|---|
![]() |
![]() |
Getting started
| Sign up | Log in |
|---|---|
![]() |
![]() |
The vault
| Vault overview | Editing an account |
|---|---|
![]() |
![]() |
Waypoint types
| Contact | Place |
|---|---|
![]() |
![]() |
| Note / wish | New document |
|---|---|
![]() |
![]() |
Trusted contacts, settings, and admin
| Trusted contacts | Settings |
|---|---|
![]() |
![]() |
The one-line install below is the quickest and easiest way to get running — it installs what it needs (git, Node), clones the repo into a scratch directory, walks you through
.envin a browser form, and brings the app up for you. Prefer to skip the repo checkout entirely and run just the published Docker image yourself instead? See Just the image, no repo checkout below — more manual, but nogit clonerequired.
curl -fsSL https://raw.githubusercontent.com/j5guy/fondwaypoints/master/install.sh | bashThat's the whole thing — no git clone or mkdir needed first. It defaults to /opt/fondwaypoints
(override with --dir /some/other/path, or the FONDWAYPOINTS_INSTALL_DIR environment variable), which
it creates for you (sudo mkdir + chown to your own user, so nothing afterward needs sudo or leaves
root-owned files behind).
Prefer to review the script first, or already have a checkout? Same thing, run locally:
git clone https://github.com/j5guy/fondwaypoints.git
cd fondwaypoints
./install.sh(./setup.sh is kept as an alias for the same command, for anyone used to typing that.) Run from
inside an existing checkout like this, it operates in place — no --dir/scratch clone involved, same as
it always has.
Testing scope: this install path has only actually been run on Rocky Linux 9 and Ubuntu 24.04/26 (x86_64) — Ubuntu tested as a VM and on bare metal, not as an LXC container (see the Proxmox LXC note below). It tries to handle a lot of environment variation automatically and won't be perfect everywhere — if it fails on your system, please report it with your OS/version and what happened. See "What the Setup Script Does" in the wiki (linked below) for exactly what it does, and the manual steps below as a fallback.
ARM / Raspberry Pi: untested, but should mostly work — Node and Docker Engine both support Debian/Ubuntu on arm64/armhf. The guided setup always builds the Docker image locally (see "Why build locally instead of pulling an image?" below), so there's no pre-built-image architecture mismatch to worry about — the first build will compile
bcryptfrom source if no prebuilt binary exists for your platform, which is slower but should still succeed. Separately, if choosing Internal MongoDB, the wizard auto-detects CPUs that don't support what current MongoDB versions require (e.g. Raspberry Pi 4 and earlier) and falls back to an older image automatically — External MongoDB is preferable where that's an option instead. See the Installation Guide in the wiki for details.
Early in the form it asks what should be left on disk once this finishes (only when installed via the curl command above, not when run from an existing checkout — there's nowhere else for that checkout to go):
- Docker only, minimal footprint (recommended) — builds the image, then deletes the source, leaving
just a small
docker-compose.yml,.env, andupdate.shbehind. Always uses Docker (Local Node.js process needs the source on disk to run) — Docker images persist independently of the source checkout, so deleting it afterward doesn't lose the image. - Full checkout — keeps the source at the install directory, so Local Node.js process (below) stays available too, same as before this choice existed.
The guided setup (both footprints above) always builds the Docker image from source — this project doesn't publish a pre-built image itself (see CI/CD and Releases), so there's nothing to pull from by default. A few reasons building locally is the right default even setting that aside:
- It works on any CPU architecture, ARM (Raspberry Pi, etc.) included — no architecture-detection or fallback logic needed, since there's no pre-built image whose architecture could mismatch the host in the first place.
- What you get always matches what you cloned. A local build compiles the exact checkout the wizard just fetched — no risk of a pulled tag being out of sync with the code you're looking at.
- One fewer thing that can fail. No dependency on reaching a registry — awkward on a host with
restricted outbound network access. Building locally only needs what the checkout and
npm/Docker Hub (for the base image) already require.
If you build and publish your own image to a registry you control, pulling it is still fully supported
manually — just not from the guided setup. See Just the image, no repo
checkout below, or add docker-compose.pull.yml to a manual
docker compose invocation from a full checkout.
Partway through the form it asks how to run the app — Docker (recommended, brings up a bundled nginx that terminates TLS itself, plus an optional bundled MongoDB) or a Local Node.js process (runs as a systemd service; not available with the minimal footprint above). Local Node.js process bundles nothing — you must point it at an external MongoDB server (the form warns about this when you pick it) and bring your own TLS-terminating reverse proxy. Docker itself is only installed at this point, once you've actually picked it — not preemptively. Either way the wizard brings the app up before exiting.
If nginx is already installed on this host, an Existing nginx step then offers to add a site for it automatically instead — reachable both by your domain and by this host's LAN IP (each on its own port, picked and live-checked for availability right there in the form) through that nginx, with a TLS certificate you pick: reuse one an existing site already references, generate a new self-signed one, or supply your own. If nginx isn't detected, you'll see a plain confirmation of what's happening instead (the bundled Docker nginx container in Docker mode — its own HTTPS port is likewise picked and live-checked in the form — or a reminder to bring your own in local mode).
If you choose "Local Node.js process" and don't use "Existing nginx" above, you must run a reverse proxy — like nginx — yourself to handle TLS. This mode does not terminate TLS for you the way Docker mode does, and TLS is mandatory (see below); without a reverse proxy in front of it, the app won't be reachable securely at all.
Import the certificate before visiting the site. Unless you're using a certificate from a public CA (e.g. Let's Encrypt), your browser won't trust it yet. If the wizard generated one for you, it shows a download link for the CA certificate at the end of setup — install that into your OS/browser trust store; if you supplied your own self-signed or private-CA cert, install that CA instead. Do this on every device (and browser) that needs to reach the site — otherwise you'll get a certificate warning on every visit, and some browsers or managed devices will refuse to proceed past it at all. If the download itself is blocked (some browsers, Brave included, are strict about this), grab
certs/ca.pemdirectly from the host instead — it's always there, outside Docker.
The terminal prints the URL(s) the app is reachable at once it's up (your WEB_FQDN and this
machine's LAN IP address(es)) — visit /auth/signup on one of those to create the first account.
There's no CLI-based account bootstrap, since account creation generates your encryption keys in the
browser.
Proxmox LXC: Ubuntu LXC is not recommended and not supported for Docker deployment — Docker nested inside an Ubuntu LXC guest reliably hits AppArmor confinement failures that range from awkward to unfixable depending on the exact kernel/runc combination. If deploying into an LXC container, use Rocky Linux (or another RHEL-based image) instead — RHEL-family distros use SELinux, not AppArmor, and don't have this problem; Rocky 9 is one of this project's two actually-tested platforms. Plain (non-LXC) Ubuntu — a real VM or bare metal — is unaffected and works fine. See the Installation Guide in the wiki for the full writeup if you want to try Ubuntu LXC anyway.
The app checks GitHub for newer releases on its own (every 12 hours, plus once at startup) and lets you
know two ways: a flashing "Update Available!" link at the bottom of every page, and /admin/updates (a
"Check now" button plus current/latest version info) if you're an admin. Neither one updates anything
automatically — when you see it, run the command below.
Same command regardless of which footprint or deploy mode you picked above, run from the install directory:
cd /opt/fondwaypoints # or wherever you installed to
./update.shFetches the newest release and brings it back up the same way it's already running — rebuilds/pulls the
image and docker compose up -ds again (Docker), or npm installs and restarts the systemd service
(Local Node.js process). Add --auto (or check "Enable automatic updates" during setup, which wires up
a weekly cron entry that passes it automatically) to run it non-interactively.
No local checkout required — download it first rather than piping straight into bash, so the
confirmation prompt below actually has a terminal to read your answer from:
curl -fsSL https://raw.githubusercontent.com/j5guy/fondwaypoints/master/uninstall.sh -o uninstall.sh
bash uninstall.shDestructive — this permanently deletes the Docker containers/images, every named volume (uploads,
logs, backups, and MongoDB data if internal — every account's vault, if this points at a real
deployment), the systemd service if it was installed as a Local Node.js process, the host nginx site if
one was set up (removed and nginx reloaded), the auto-update cron entry if enabled, and the install
directory itself (/opt/fondwaypoints by default — override with --dir, or FONDWAYPOINTS_INSTALL_DIR
the same way install.sh does). It will ask you to type yes to confirm before touching anything — only
pass --yes to skip that prompt if you're scripting this deliberately (e.g. tearing down a CI/test
deployment), since a piped curl | bash -s -- --yes never gives you the chance to back out. Ends with a
verification pass confirming nothing Fond-Waypoints-related is left.
The install command above is the quickest and easiest way to get
running — use this instead only if you specifically want to skip the repo checkout entirely (not even
a scratch one — see "Docker only, minimal footprint" above for a guided way to get a similarly small
footprint). This project doesn't publish a pre-built image itself (see Why build locally instead of
pulling an image? above), so this section assumes
you've built and pushed your own to a registry you control. On any machine that already has Docker
installed and can reach that registry, it's three things: pull the image, write a .env, write a small
docker-compose.yml of your own to bring up the stack. Nothing else from the repo is required —
docker-compose.yml, the bundled nginx container, and the setup wizard described above are all
conveniences that come with a checkout, not prerequisites — but this path is more manual, since none
of the automation above is there to do it for you.
TLS is mandatory (see above) but doesn't require the repo either — nginx:alpine is just a public
image, so a TLS-terminating reverse proxy can ride in the same self-authored docker-compose.yml.
docker pull <your-registry>/fondwaypoints:latestGenerate a self-signed cert/key (or drop in your own), and a browser/OS will only trust it once you
install cert.pem into its trust store — or use a real cert from a public CA instead and skip this:
openssl req -x509 -newkey rsa:4096 -sha256 -days 825 -nodes \
-keyout cert.key -out cert.pem -subj "/CN=fondwaypoints.example.com" \
-addext "subjectAltName=DNS:fondwaypoints.example.com,IP:127.0.0.1"Write an nginx.conf next to it:
server {
listen 80;
server_name _;
return 301 https://$host:5560$request_uri;
}
server {
listen 5560 ssl;
server_name _;
client_max_body_size 150M;
ssl_certificate /etc/nginx/certs/server.pem;
ssl_certificate_key /etc/nginx/certs/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app:5560;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
}
A .env — generate sessionSecret/PRELOGIN_SECRET with openssl rand -hex 64 (the same command
the setup wizard's "generate" buttons run):
WEB_FQDN=fondwaypoints.example.com
sessionSecret=<64 random hex bytes>
PRELOGIN_SECRET=<64 random hex bytes>
mongoHost=mongo
mongoUser=
mongoPass=
mongoPort=27017
mongoDBName=fondwaypoints
ADMIN_EMAIL=
SIGNUP_ALLOWLIST=
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_USER=
SMTP_PASS=
SMTP_FROM=Fond Waypoints <your-address@gmail.com>
DEFAULT_WAIT_DAYS=14
And a docker-compose.yml tying it all together:
services:
app:
image: <your-registry>/fondwaypoints:latest
restart: unless-stopped
env_file:
- .env
volumes:
- uploads-data:/app/public/uploads
- logs-data:/app/logs
depends_on:
mongo:
condition: service_healthy
nginx:
image: nginx:alpine
restart: unless-stopped
depends_on:
- app
ports:
- "80:80"
- "5560:5560"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./cert.pem:/etc/nginx/certs/server.pem:ro
- ./cert.key:/etc/nginx/certs/server.key:ro
mongo:
image: mongo:7
restart: unless-stopped
volumes:
- mongo-data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
interval: 5s
timeout: 5s
retries: 20
volumes:
uploads-data:
logs-data:
mongo-data:Then docker compose up -d. Note that app has no ports: of its own — nginx is the only thing
reachable from outside, at 80/5560 on whichever host runs this. Only bind those to 127.0.0.1
instead if you specifically want to restrict them to local-host-only reachability (e.g. because
something else in front of nginx handles the actual public-facing side).
Already have something else on port 80 or 5560?
nginxhere will fail to start with a "port is already allocated" error — nothing else breaks, but this stack's proxy won't come up until it's resolved. Either change the host-side port inports:(e.g."8443:5560"— only the number before the colon needs to change, the container-side5560must stay put to matchnginx.conf), or, if the other container is itself a reverse proxy, skip publishing a port here entirely and join thisnginxservice to that proxy's Docker network instead so it can route tofondwaypoints-nginx:5560directly.
Already have a MongoDB server elsewhere? Delete the mongo service and the depends_on block on
app, and point mongoHost (plus mongoUser/mongoPass if it needs auth) at that server instead.
Already have your own reverse proxy (or don't want nginx bundled in)? Delete the nginx service, add
ports: ["5560:5560"] to app instead, and point your proxy's TLS termination at that.
Then visit /auth/signup once it's reachable to create the first account — there's no CLI-based
account bootstrap. See .env.example in the repo (viewable on GitHub without cloning)
for the full list of environment variables and what each one does, including
ADMIN_EMAIL/SIGNUP_ALLOWLIST (who's allowed to sign up) and SMTP_* (invite/access-request
emails — the app still works with these blank, it just logs emails instead of sending them).
For the full guided-setup walkthrough (installation footprint, TLS certificate generation, pulling a
pre-built image manually instead of building), exactly what the setup script installs/configures and
where it needs sudo, CI/CD internals, backup & restore, and the admin area, see the
wiki.













