- TypeScript 98.6%
- Shell 0.8%
- JavaScript 0.3%
- CSS 0.2%
## What Rate limiting derived the client IP from proxy headers without any notion of which proxy, if any, was trusted to set them. Because those headers arrive with the request, the value used as the rate-limit bucket key was not reliably the caller's. This makes every limit in the app weaker than intended: login, registration, password reset, email verification, CardDAV, error reporting, push subscription, and the notification endpoints. This adds a trust boundary, de-duplicates the three copies of the resolution logic, and stops the limits resting entirely on the IP. ## Upgrade notes No action required. The defaults match both reverse proxy configurations in our own documentation, so existing deployments behave the same after upgrading. Two new optional settings, both documented in `self-hosting/configuration.md` and `self-hosting/reverse-proxy.md`: - `TRUSTED_PROXY_COUNT` (default `1`): how many proxies sit in front of Nametag. Set `0` if none, `2` for a CDN in front of a load balancer. - `TRUSTED_PROXY_HEADER` (default `x-forwarded-for`): which header your proxy sets. Set `x-real-ip` if your proxy sets that one instead. ## Why two settings rather than one The two topologies cannot be told apart at runtime, and each makes the *other* header untrustworthy: - A proxy that sets `X-Forwarded-For` (our documented nginx and Caddy configs both do) passes any client-supplied `X-Real-IP` straight through. - A proxy that sets only `X-Real-IP` passes any client-supplied `X-Forwarded-For` straight through. Preferring either header by default is therefore wrong for somebody, and wrong silently. So the operator declares it, with the default matching what our own documentation prescribes. ## How the resolution works - With `x-forwarded-for`, entries are counted from the **right**. The documented nginx config uses `proxy_add_x_forwarded_for`, which appends, so anything a client supplied stays to the left of what the proxy adds. - With `x-real-ip`, that header is read alone. It carries a single value the proxy replaces rather than appends, so the hop count does not apply. - The result must parse as an IP address, otherwise arbitrary text becomes a bucket key. - A count of `0` ignores both headers entirely. - When no trustworthy IP can be determined the resolver returns `null` rather than a placeholder, and the app logs a warning naming both settings, because a silently shared bucket is how this stays broken. There is an inherent limit worth stating: with one trusted proxy, a single-entry header is indistinguishable from a compliant proxy's single appended hop. Hop counting is only sound if the app cannot be reached except through the proxy. That precondition is now stated in the documentation, with a commented `127.0.0.1` binding in `docker-compose.yml` for operators who front the app with a proxy. ## Cloudflare `TRUSTED_PROXY_HEADER` also accepts `cf-connecting-ip`. Cloudflare **overwrites** that header with the visitor address on every request, rather than appending the way it does with `X-Forwarded-For`, so in that mode there is no chain to count and no hop number to get wrong when a layer is added or removed. **That mode moves the entire security boundary to your origin.** With `x-forwarded-for`, a wrong hop count degrades gracefully: an attacker supplies one forged entry among several and the resolver usually still lands on a real address. `CF-Connecting-IP` has no such cushion. Anyone who can reach the origin without passing through Cloudflare can set that header to anything, and nothing contradicts them. Use it only with the origin locked down, by restricting the firewall to Cloudflare's published ranges, enabling Authenticated Origin Pulls, or running a Cloudflare Tunnel so the origin has no public address at all. If you would rather not depend on that, leave the header at its default and set `TRUSTED_PROXY_COUNT` to the number of appending hops instead: `2` for Cloudflare in front of your own reverse proxy, `1` if Cloudflare reaches the app directly. Do **not** use `x-real-ip` behind Cloudflare. An origin Nginx setting `X-Real-IP $remote_addr` records the Cloudflare **edge** address, so every visitor arriving through the same edge would share a single rate-limit bucket. ## Trusted versus untrusted `resolveTrustedClientIp` is for security decisions and returns `null` when it cannot be sure. `getClientIp` keeps its previous best-effort behaviour and is now documented as being for log lines only. Every remaining caller of the latter was checked individually and is a logging argument. ## Reducing the reliance on the IP at all Three routes previously keyed on the IP alone. `forgot-password`, `resend-verification` and `register` now also key on the normalised email address, so a per-account bound holds even where the IP cannot be trusted. These are the paths that send mail to an address supplied in the request. Each of them checks the limit twice: an unkeyed check before the body is parsed, and an email-keyed check after. Without the first, a malformed body would consume no limit at all. `login` is deliberately unchanged: its body must reach the auth handler intact, and a per-email key would not bound password spraying, which is one password tried against many addresses. Per-account lockout already covers repeated guesses against a single account. `reset-password` and `verify-email` carry only a token, which would produce a fresh key per guess. ## Testing `npm run verify` passes: lint, typecheck, 3437 tests, production build, plus the docs site build. The resolver is covered against spoofed prefixes and rotation in both modes, hop counts of 0, 1 and 2, chains shorter than the configured count, non-address values, IPv6, whitespace and empty entries, and each mode ignoring the other's header. |
||
|---|---|---|
| .devcontainer | ||
| .github | ||
| .vscode | ||
| app | ||
| components | ||
| docs | ||
| hooks | ||
| lib | ||
| locales | ||
| prisma | ||
| public | ||
| scripts | ||
| tests | ||
| types | ||
| .auto-changelog | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .impeccable.md | ||
| .npmrc | ||
| .prettierrc.yaml | ||
| .release-please-manifest.json | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CODE_OF_CONDUCT.md | ||
| CONTRIBUTING.md | ||
| docker-compose.services.yml | ||
| docker-compose.yml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| i18n.ts | ||
| instrumentation.ts | ||
| LICENSE | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| postcss.config.mjs | ||
| prisma.config.ts | ||
| proxy.ts | ||
| README.md | ||
| release-please-config.json | ||
| SECURITY.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Nametag
⚠️ Active Development Notice
Nametag is under active development and may introduce breaking changes between releases. Please read the release notes carefully before updating to ensure a smooth upgrade process.
Nametag is a personal relationships manager that helps you remember the people in your life and how they're connected. Track birthdays, contact information, how people are connected, and visualize your network as an interactive graph.
Dashboard with network overview and statistics
Screenshots
Features
- Track people with flexible attributes (name, birthday, important dates, photos, and user-defined custom fields)
- Map relationships between people with custom relationship types
- Visualize your network with interactive force-directed graphs
- Organize contacts into custom groups
- Interactive map view with geocoded addresses and clustered markers
- Journal for logging interactions and notes over time
- Set reminders for important dates and staying in touch
- CardDAV sync with Google Contacts, iCloud, Outlook, and Nextcloud
- vCard and JSON import/export
- Duplicate detection and contact merging
- Global fuzzy search with accent-insensitive matching
- REST API with OpenAPI documentation and token authentication
- OIDC / SSO authentication support
- Installable as a PWA
- Trash with soft-delete and restore
- Bulk operations (delete, group assignment, relationships)
- Messaging app quick links (WhatsApp, Signal, Telegram, and more)
- Full dark and light theme support
- Multiple languages (English, Spanish, Japanese, Norwegian, German, Chinese, Italian, Russian, Dutch)
- Mobile-responsive design
- Multi-platform Docker support (AMD64 and ARM64)
Hosted vs Self-Hosted
We offer a hosted version at nametag.one with a generous free tier and affordable paid plans, which helps fund development. You can also self-host Nametag for free with unlimited contacts and complete data ownership.
See the installation guide in the docs to get started with self-hosting.
Quick Start (Self-Hosted)
mkdir nametag && cd nametag
# add a docker-compose.yml and .env file (see the installation guide)
docker compose up -d
Then visit http://localhost:3000. For the full installation guide, environment variables, email, OIDC, Redis, and reverse proxy setup, see the docs.
Contributing
We welcome contributions! See CONTRIBUTING.md for how to get involved, and the development setup guide for detailed instructions on running Nametag locally.
Roadmap
ℹ️ Contributions to any of these items are very welcome! Items that require the most help will have the [HELP NEEDED] tag. If you want to contribute to an item that does not have a PR or Issue associated to it, please create it yoursef.
To do
Future features and improvements, ordered by priority:
- [HELP NEEDED] Mobile app (Native apps for Android and iOS are preferred)
- Add support for SQLite databases
- Add notification support [Issue #6]
- Support multi-user groups [Issue #37]
- Immich integration [Issue #46]
- [HELP NEEDED] Additional language translations (Portuguese, Korean, etc.)
Done
Features and improvements that have already been implemented:
UI/UX improvements and accessibility enhancementsDocumentation improvements (API, deployment, functionality, development, etc)SMTP support[Issue #4, PR #21]Option to disable registration[Issue #9, PR #17]ARM build for docker images[Issue #14, PR #18]Improve development setup to make contributors' lives easier[PR #25]Implement CardDAV support[Issue #15, PR #82]API for third-party integrations[Issue #29, PR #70]Add photos to people[Issue #19, PR #135]Add custom template titles for important dates[Issue #23, PR #176]Add journaling capabilities[Issue #28, PR #192]Implement OIDC[Issue #10]Add map to show people's locations[Issue #26, PR #341]Full-text fuzzy search[PR #301]Configurable photo compression[PR #331]HEIC photo support[PR #357]Messaging app quick links[PR #375]PWA / installable app with offline support[PR #379]Duplicate detection and contact mergingCustom field templatesTrash with soft-delete and restoreData import/export (JSON)Bulk operationsEastern name order support
License
Licensed under the GNU Affero General Public License v3.0. This ensures that if you modify and deploy Nametag, you must make your source code available.
Support
- Hosted version: For support with the hosted service, email support@nametag.one
- Self-hosting: Open an issue on GitHub, or check the docs
- Security issues: See SECURITY.md
Support Development
If you find Nametag useful and want to support its development, you can buy me a coffee! ☕
Built with care for people who care about people.









