Vibecode Bitwarden
track this build7 phases, 16 steps, beginner friendly0%If your goal is to stop paying hosted Bitwarden Premium, the core vault can be self-hosted or replaced with KeePass-style local storage; the caveat is security responsibility.
You are building a lean indie version of Bitwarden. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Bitwarden · indie build Your own password server without writing a password manager: vaultwarden in Docker behind Caddy on HTTPS, the official Bitwarden apps and extensions pointed at it, signups closed after your account exists, an admin token done the way current vaultwarden requires, encrypted nightly backups off the box, and a restore you have actually performed before trusting it. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | vaultwarden/server in Docker | rule zero: do not write a password manager | | TLS | Caddy | the clients refuse non-HTTPS servers | | Clients | The official Bitwarden apps and extensions | autofill, mobile and passkeys keep working because you did not rebuild them | | Backups | tar plus age encryption, off the box | a backup of every credential you own must be encrypted | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. The vault must be reachable from your phone and laptops. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **Docker Engine and Compose on the VPS** · free - Why: vaultwarden runs as a container. - Get it: docs.docker.com/engine/install/ubuntu, then add your user to the docker group. - Verify: docker compose version prints - [ ] **A subdomain for the vault** · free on a domain you own - Why: The browser extensions and mobile apps refuse to talk to a non-HTTPS server; a domain is how you get a certificate. - Get it: vault.yourdomain.com, one A record to the VPS. - [ ] **age for backup encryption** · free - Why: Backups of your entire credential set must be encrypted at rest wherever they go. - Get it: apt install age, or from the releases page. Generate a key pair with age-keygen and keep the private key in your existing password manager. - Verify: age --version prints - [ ] **An export of your current vault** · free - Why: Phase 5 imports it. Bitwarden exports are unencrypted by default; you will delete the file securely afterwards. - Get it: In your current manager: Export vault, JSON or CSV. Keep it only until import. - [ ] **Off-box storage for backups** · cents a month - Why: A backup on the server being backed up is not a backup. - Get it: Backblaze B2 or Cloudflare R2 plus rclone on the VPS. ## Quick start ```sh docker compose up -d curl -I https://vault.yourdomain.com ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Emergency access, organizations and family sharing. - Pretending you have a security team. - managed hosting - premium support - emergency access - polished admin - reduced security maintenance risk If one of those is essential to you, that is the reason to keep paying for Bitwarden, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bitwarden The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted password vault to replace hosted Bitwarden Premium. Build it in phases, in the order below. This is an operations task, not a coding task. Each phase ends in a verification you actually run, and Phase 6 is the one that decides whether this was a good idea. ### Rule zero Do not write a password manager. Do not invent cryptography, do not design a key derivation scheme, and do not build a browser extension. Deploy vaultwarden, the Bitwarden-compatible server, and use the official Bitwarden clients against it. The clients are the reason this works: autofill, mobile and passkeys keep functioning because you did not rebuild them. ### Stack (fixed, do not substitute) - Docker Compose with the `vaultwarden/server` image and a data volume at `./vw-data`. - Caddy in front for automatic HTTPS on a subdomain. The browser extensions and mobile apps refuse to talk to a non-HTTPS server, so this is not optional. - `age` for backup encryption. ### Phase 1 · Bring it up behind HTTPS Build: the compose file and the Caddy config, on the real subdomain, with the data volume mounted. Do not run it on plain HTTP even briefly · the clients will not connect and you will spend the time debugging the wrong layer. Done when: the web vault loads over HTTPS with a valid certificate, and the container survives `docker compose down && up` with its data intact. Do not build yet: the admin panel, backups, clients. ### Phase 2 · The admin token Build: `ADMIN_TOKEN`, and read this carefully because it is the step that wastes an evening. Current vaultwarden expects the token as an Argon2 PHC hash, not a plain string. Generate it with: `docker run --rm -it vaultwarden/server:latest /vaultwarden hash` Two traps follow. In a Compose environment file the `$` characters must be escaped as `$$`, or single-quote the whole value, because Compose otherwise interprets them as variable references. And at `/admin` you enter the original password you typed into the hash command, not the PHC string you pasted into the config. Done when: `/admin` accepts the original password and rejects a wrong one, and the value survives a `docker compose up` without Compose mangling it. ### Phase 3 · Lock it down Build: register your one account, then set `SIGNUPS_ALLOWED=false` and restart. Add anyone else by invitation from the admin panel. Confirm nothing else on the host is exposed · the vaultwarden port must not be reachable except through Caddy. Done when: a fresh browser cannot reach a registration form, an invitation flow works, and a port scan from another machine shows only 80 and 443. ### Phase 4 · Clients Build: point the official Bitwarden browser extension, desktop app and mobile app at your server URL (the self-hosted setting, entered before login). Done when: all three log in, autofill works on a real site, a passkey saves and authenticates, and a vault item created on the phone appears in the browser extension. ### Phase 5 · Import Build: import your existing vault using the standard Bitwarden export/import flow. Then delete the plaintext export file securely · a Bitwarden export is unencrypted by default, and leaving one in Downloads undoes the entire point of this project. Done when: item counts match before and after, TOTP seeds still generate correct codes, and the export file is gone from disk and from your trash. ### Phase 6 · Backup, and a restore drill Build: a nightly job that stops the container or uses a consistent snapshot, tars `vw-data`, encrypts it with `age`, and copies it off the machine. A backup that lives only on the server it backs up is not a backup. Write `restore.sh`. Then do the part that matters: restore into a throwaway instance and log in against it with a client. Done when: you have completed one full restore into a clean environment and successfully unlocked the restored vault. Not "the script runs" · a restore you performed, with the date written in the README. Until that is done, keep the hosted subscription. ### Phase 7 · Updates and the honest handover Build: an update procedure (pull image, back up first, restart, verify login) and the README. Done when: one upgrade cycle has been run and all three clients still log in. ### Out of scope (and why) - Emergency access, organizations and family sharing. - Pretending you have a security team. You are now responsible for patching a service that holds every credential you own. ### README must contain - The admin-token generation command, the `$$` escaping trap, and the note that you log in with the original password. - The date of the last successful restore drill. - The reassurance and the warning together: the clients cache the vault, so a dead server does not lock you out of your passwords · but an unpatched server holding your entire credential set is a real risk you have taken on, and hosted Bitwarden is inexpensive precisely because that risk is theirs. ===== AGENTS.md ===== # Agent instructions · Bitwarden indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: vaultwarden/server in Docker, Caddy, The official Bitwarden apps and extensions, tar plus age encryption, off the box. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. - Until the restore drill is done, keep the hosted subscription. ===== BUILD_PLAN.md ===== # Build plan · Bitwarden Your own password server without writing a password manager: vaultwarden in Docker behind Caddy on HTTPS, the official Bitwarden apps and extensions pointed at it, signups closed after your account exists, an admin token done the way current vaultwarden requires, encrypted nightly backups off the box, and a restore you have actually performed before trusting it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Bring it up behind HTTPS The web vault over a valid certificate, data surviving a restart. Never run it on plain HTTP even briefly. ### Steps 1. Write docker-compose.yml with vaultwarden/server and a ./vw-data volume, port published on 127.0.0.1 only Files: `docker-compose.yml`, `.env` 2. Write the Caddyfile and point DNS vault.yourdomain.com with reverse_proxy localhost:80 (the container's port). Files: `Caddyfile` 3. Start and verify ```sh docker compose up -d curl -I https://vault.yourdomain.com ``` ### Done when - [ ] The web vault loads over HTTPS with a valid certificate - [ ] docker compose down && up keeps the data ### Watch out - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. ## Phase 2 · The admin token The step that wastes an evening, done right. ### Steps 1. Generate the Argon2 hash ```sh docker run --rm -it vaultwarden/server:latest /vaultwarden hash ``` 2. Put it in .env correctly Single-quote the whole value, or escape each $ as $$; Compose otherwise interprets them as variables. 3. Log in at /admin with the original password you typed, not the hash ### Done when - [ ] /admin accepts the original password and rejects a wrong one - [ ] The value survives docker compose up without Compose mangling it ## Phase 3 · Lock it down One account, signups closed, nothing else exposed. ### Steps 1. Register your account, then set SIGNUPS_ALLOWED=false and restart 2. Confirm only 80 and 443 are reachable from outside ```sh nmap -p- vault.yourdomain.com ``` ### Done when - [ ] A fresh browser cannot reach a registration form - [ ] The invitation flow from /admin works - [ ] A port scan shows only 80 and 443 ## Phase 4 · Clients Extension, desktop and mobile all logged in to your server. ### Steps 1. Log in the browser extension and the desktop app, choosing self-hosted and entering your server URL before login 2. Log in the mobile app and test a passkey and autofill end to end ### Done when - [ ] All three log in - [ ] Autofill works on a real site - [ ] A passkey saves and authenticates - [ ] An item created on the phone appears in the extension ## Phase 5 · Import Everything moved, and the plaintext export gone. ### Steps 1. Import through Tools > Import in the web vault 2. Securely delete the export file and empty the trash ### Done when - [ ] Item counts match before and after - [ ] TOTP seeds still generate correct codes - [ ] The export file is gone from disk and trash ## Phase 6 · Backup, and a restore drill Encrypted nightly backup off the box, and a restore you performed. ### Steps 1. Write backup.sh: stop or snapshot, tar vw-data, encrypt with age, copy off Files: `backup.sh`, `restore.sh` ```sh docker compose stop && tar czf - vw-data | age -r $AGE_RECIPIENT -o /tmp/vw-$(date +%F).tgz.age && docker compose start rclone copy /tmp/vw-$(date +%F).tgz.age remote:vault-backups/ ``` 2. Restore into a throwaway instance and unlock the restored vault with a client ### Done when - [ ] Last night's encrypted backup exists off the box - [ ] A restore into a clean environment unlocks with a client - [ ] The restore date is written in the README ### Watch out - Until the restore drill is done, keep the hosted subscription. ## Phase 7 · Updates and the honest handover A repeatable update and the trade written down. ### Steps 1. Write update.sh: backup, pull, up -d, verify login Files: `update.sh` 2. Write the README The admin-token command and the $$ trap, the restore date, and the reassurance plus warning: clients cache the vault so a dead server locks you out of nothing, but an unpatched server holding every credential is a risk you took on. Files: `README.md` ### Done when - [ ] One upgrade cycle run and all three clients still log in - [ ] The README carries the token trap and the restore date ## Not in this build - Emergency access, organizations and family sharing. - Pretending you have a security team. ## After v1, if you want it - A second admin device with hardware-key 2FA on your account - Fail2ban in front of the login endpoint ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The exact public address; vaultwarden needs it for WebAuthn and links. DOMAIN=https://vault.yourdomain.com # Required · secret. An Argon2 PHC hash from docker run --rm -it vaultwarden/server:latest /vaultwarden hash. In a Compose .env either single-quote the whole value or escape every $ as $$. ADMIN_TOKEN='$argon2id$v=19$...' # Required. true until your account exists, then false. SIGNUPS_ALLOWED=true # Required. Your age public key, for encrypting backups. AGE_RECIPIENT=age1...
You are building a lean indie version of Bitwarden. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Bitwarden · indie build Your own password server without writing a password manager: vaultwarden in Docker behind Caddy on HTTPS, the official Bitwarden apps and extensions pointed at it, signups closed after your account exists, an admin token done the way current vaultwarden requires, encrypted nightly backups off the box, and a restore you have actually performed before trusting it. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | vaultwarden/server in Docker | rule zero: do not write a password manager | | TLS | Caddy | the clients refuse non-HTTPS servers | | Clients | The official Bitwarden apps and extensions | autofill, mobile and passkeys keep working because you did not rebuild them | | Backups | tar plus age encryption, off the box | a backup of every credential you own must be encrypted | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. The vault must be reachable from your phone and laptops. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **Docker Engine and Compose on the VPS** · free - Why: vaultwarden runs as a container. - Get it: docs.docker.com/engine/install/ubuntu, then add your user to the docker group. - Verify: docker compose version prints - [ ] **A subdomain for the vault** · free on a domain you own - Why: The browser extensions and mobile apps refuse to talk to a non-HTTPS server; a domain is how you get a certificate. - Get it: vault.yourdomain.com, one A record to the VPS. - [ ] **age for backup encryption** · free - Why: Backups of your entire credential set must be encrypted at rest wherever they go. - Get it: apt install age, or from the releases page. Generate a key pair with age-keygen and keep the private key in your existing password manager. - Verify: age --version prints - [ ] **An export of your current vault** · free - Why: Phase 5 imports it. Bitwarden exports are unencrypted by default; you will delete the file securely afterwards. - Get it: In your current manager: Export vault, JSON or CSV. Keep it only until import. - [ ] **Off-box storage for backups** · cents a month - Why: A backup on the server being backed up is not a backup. - Get it: Backblaze B2 or Cloudflare R2 plus rclone on the VPS. ## Quick start ```sh docker compose up -d curl -I https://vault.yourdomain.com ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Emergency access, organizations and family sharing. - Pretending you have a security team. - managed hosting - premium support - emergency access - polished admin - reduced security maintenance risk If one of those is essential to you, that is the reason to keep paying for Bitwarden, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bitwarden The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted password vault to replace hosted Bitwarden Premium. Build it in phases, in the order below. This is an operations task, not a coding task. Each phase ends in a verification you actually run, and Phase 6 is the one that decides whether this was a good idea. ### Rule zero Do not write a password manager. Do not invent cryptography, do not design a key derivation scheme, and do not build a browser extension. Deploy vaultwarden, the Bitwarden-compatible server, and use the official Bitwarden clients against it. The clients are the reason this works: autofill, mobile and passkeys keep functioning because you did not rebuild them. ### Stack (fixed, do not substitute) - Docker Compose with the `vaultwarden/server` image and a data volume at `./vw-data`. - Caddy in front for automatic HTTPS on a subdomain. The browser extensions and mobile apps refuse to talk to a non-HTTPS server, so this is not optional. - `age` for backup encryption. ### Phase 1 · Bring it up behind HTTPS Build: the compose file and the Caddy config, on the real subdomain, with the data volume mounted. Do not run it on plain HTTP even briefly · the clients will not connect and you will spend the time debugging the wrong layer. Done when: the web vault loads over HTTPS with a valid certificate, and the container survives `docker compose down && up` with its data intact. Do not build yet: the admin panel, backups, clients. ### Phase 2 · The admin token Build: `ADMIN_TOKEN`, and read this carefully because it is the step that wastes an evening. Current vaultwarden expects the token as an Argon2 PHC hash, not a plain string. Generate it with: `docker run --rm -it vaultwarden/server:latest /vaultwarden hash` Two traps follow. In a Compose environment file the `$` characters must be escaped as `$$`, or single-quote the whole value, because Compose otherwise interprets them as variable references. And at `/admin` you enter the original password you typed into the hash command, not the PHC string you pasted into the config. Done when: `/admin` accepts the original password and rejects a wrong one, and the value survives a `docker compose up` without Compose mangling it. ### Phase 3 · Lock it down Build: register your one account, then set `SIGNUPS_ALLOWED=false` and restart. Add anyone else by invitation from the admin panel. Confirm nothing else on the host is exposed · the vaultwarden port must not be reachable except through Caddy. Done when: a fresh browser cannot reach a registration form, an invitation flow works, and a port scan from another machine shows only 80 and 443. ### Phase 4 · Clients Build: point the official Bitwarden browser extension, desktop app and mobile app at your server URL (the self-hosted setting, entered before login). Done when: all three log in, autofill works on a real site, a passkey saves and authenticates, and a vault item created on the phone appears in the browser extension. ### Phase 5 · Import Build: import your existing vault using the standard Bitwarden export/import flow. Then delete the plaintext export file securely · a Bitwarden export is unencrypted by default, and leaving one in Downloads undoes the entire point of this project. Done when: item counts match before and after, TOTP seeds still generate correct codes, and the export file is gone from disk and from your trash. ### Phase 6 · Backup, and a restore drill Build: a nightly job that stops the container or uses a consistent snapshot, tars `vw-data`, encrypts it with `age`, and copies it off the machine. A backup that lives only on the server it backs up is not a backup. Write `restore.sh`. Then do the part that matters: restore into a throwaway instance and log in against it with a client. Done when: you have completed one full restore into a clean environment and successfully unlocked the restored vault. Not "the script runs" · a restore you performed, with the date written in the README. Until that is done, keep the hosted subscription. ### Phase 7 · Updates and the honest handover Build: an update procedure (pull image, back up first, restart, verify login) and the README. Done when: one upgrade cycle has been run and all three clients still log in. ### Out of scope (and why) - Emergency access, organizations and family sharing. - Pretending you have a security team. You are now responsible for patching a service that holds every credential you own. ### README must contain - The admin-token generation command, the `$$` escaping trap, and the note that you log in with the original password. - The date of the last successful restore drill. - The reassurance and the warning together: the clients cache the vault, so a dead server does not lock you out of your passwords · but an unpatched server holding your entire credential set is a real risk you have taken on, and hosted Bitwarden is inexpensive precisely because that risk is theirs. ===== AGENTS.md ===== # Agent instructions · Bitwarden indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: vaultwarden/server in Docker, Caddy, The official Bitwarden apps and extensions, tar plus age encryption, off the box. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. - Until the restore drill is done, keep the hosted subscription. ===== BUILD_PLAN.md ===== # Build plan · Bitwarden Your own password server without writing a password manager: vaultwarden in Docker behind Caddy on HTTPS, the official Bitwarden apps and extensions pointed at it, signups closed after your account exists, an admin token done the way current vaultwarden requires, encrypted nightly backups off the box, and a restore you have actually performed before trusting it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Bring it up behind HTTPS The web vault over a valid certificate, data surviving a restart. Never run it on plain HTTP even briefly. ### Steps 1. Write docker-compose.yml with vaultwarden/server and a ./vw-data volume, port published on 127.0.0.1 only Files: `docker-compose.yml`, `.env` 2. Write the Caddyfile and point DNS vault.yourdomain.com with reverse_proxy localhost:80 (the container's port). Files: `Caddyfile` 3. Start and verify ```sh docker compose up -d curl -I https://vault.yourdomain.com ``` ### Done when - [ ] The web vault loads over HTTPS with a valid certificate - [ ] docker compose down && up keeps the data ### Watch out - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. ## Phase 2 · The admin token The step that wastes an evening, done right. ### Steps 1. Generate the Argon2 hash ```sh docker run --rm -it vaultwarden/server:latest /vaultwarden hash ``` 2. Put it in .env correctly Single-quote the whole value, or escape each $ as $$; Compose otherwise interprets them as variables. 3. Log in at /admin with the original password you typed, not the hash ### Done when - [ ] /admin accepts the original password and rejects a wrong one - [ ] The value survives docker compose up without Compose mangling it ## Phase 3 · Lock it down One account, signups closed, nothing else exposed. ### Steps 1. Register your account, then set SIGNUPS_ALLOWED=false and restart 2. Confirm only 80 and 443 are reachable from outside ```sh nmap -p- vault.yourdomain.com ``` ### Done when - [ ] A fresh browser cannot reach a registration form - [ ] The invitation flow from /admin works - [ ] A port scan shows only 80 and 443 ## Phase 4 · Clients Extension, desktop and mobile all logged in to your server. ### Steps 1. Log in the browser extension and the desktop app, choosing self-hosted and entering your server URL before login 2. Log in the mobile app and test a passkey and autofill end to end ### Done when - [ ] All three log in - [ ] Autofill works on a real site - [ ] A passkey saves and authenticates - [ ] An item created on the phone appears in the extension ## Phase 5 · Import Everything moved, and the plaintext export gone. ### Steps 1. Import through Tools > Import in the web vault 2. Securely delete the export file and empty the trash ### Done when - [ ] Item counts match before and after - [ ] TOTP seeds still generate correct codes - [ ] The export file is gone from disk and trash ## Phase 6 · Backup, and a restore drill Encrypted nightly backup off the box, and a restore you performed. ### Steps 1. Write backup.sh: stop or snapshot, tar vw-data, encrypt with age, copy off Files: `backup.sh`, `restore.sh` ```sh docker compose stop && tar czf - vw-data | age -r $AGE_RECIPIENT -o /tmp/vw-$(date +%F).tgz.age && docker compose start rclone copy /tmp/vw-$(date +%F).tgz.age remote:vault-backups/ ``` 2. Restore into a throwaway instance and unlock the restored vault with a client ### Done when - [ ] Last night's encrypted backup exists off the box - [ ] A restore into a clean environment unlocks with a client - [ ] The restore date is written in the README ### Watch out - Until the restore drill is done, keep the hosted subscription. ## Phase 7 · Updates and the honest handover A repeatable update and the trade written down. ### Steps 1. Write update.sh: backup, pull, up -d, verify login Files: `update.sh` 2. Write the README The admin-token command and the $$ trap, the restore date, and the reassurance plus warning: clients cache the vault so a dead server locks you out of nothing, but an unpatched server holding every credential is a risk you took on. Files: `README.md` ### Done when - [ ] One upgrade cycle run and all three clients still log in - [ ] The README carries the token trap and the restore date ## Not in this build - Emergency access, organizations and family sharing. - Pretending you have a security team. ## After v1, if you want it - A second admin device with hardware-key 2FA on your account - Fail2ban in front of the login endpoint ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The exact public address; vaultwarden needs it for WebAuthn and links. DOMAIN=https://vault.yourdomain.com # Required · secret. An Argon2 PHC hash from docker run --rm -it vaultwarden/server:latest /vaultwarden hash. In a Compose .env either single-quote the whole value or escape every $ as $$. ADMIN_TOKEN='$argon2id$v=19$...' # Required. true until your account exists, then false. SIGNUPS_ALLOWED=true # Required. Your age public key, for encrypting backups. AGE_RECIPIENT=age1...
You are building a production product version of Bitwarden. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Bitwarden · product brief ## Problem If your goal is to stop paying hosted Bitwarden Premium, the core vault can be self-hosted or replaced with KeePass-style local storage; the caveat is security responsibility. ## Product outcome A credential server you operate responsibly: closed signups, encrypted off-box backups with a proven restore, and a monthly patch routine. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - server or local vault - domain/HTTPS if self-hosting - secure backup - browser/mobile clients ## Explicit non-goals for v1 - Emergency access, organizations and family sharing. - Pretending you have a security team. - managed hosting - premium support - emergency access - polished admin - reduced security maintenance risk ## Success criteria - Restore drill performed and dated - Port scan shows only 80 and 443 - SIGNUPS_ALLOWED=false confirmed - update.sh run once ===== BRIEF.md ===== # Build brief · Bitwarden The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted password vault to replace hosted Bitwarden Premium. Build it in phases, in the order below. This is an operations task, not a coding task. Each phase ends in a verification you actually run, and Phase 6 is the one that decides whether this was a good idea. ### Rule zero Do not write a password manager. Do not invent cryptography, do not design a key derivation scheme, and do not build a browser extension. Deploy vaultwarden, the Bitwarden-compatible server, and use the official Bitwarden clients against it. The clients are the reason this works: autofill, mobile and passkeys keep functioning because you did not rebuild them. ### Stack (fixed, do not substitute) - Docker Compose with the `vaultwarden/server` image and a data volume at `./vw-data`. - Caddy in front for automatic HTTPS on a subdomain. The browser extensions and mobile apps refuse to talk to a non-HTTPS server, so this is not optional. - `age` for backup encryption. ### Phase 1 · Bring it up behind HTTPS Build: the compose file and the Caddy config, on the real subdomain, with the data volume mounted. Do not run it on plain HTTP even briefly · the clients will not connect and you will spend the time debugging the wrong layer. Done when: the web vault loads over HTTPS with a valid certificate, and the container survives `docker compose down && up` with its data intact. Do not build yet: the admin panel, backups, clients. ### Phase 2 · The admin token Build: `ADMIN_TOKEN`, and read this carefully because it is the step that wastes an evening. Current vaultwarden expects the token as an Argon2 PHC hash, not a plain string. Generate it with: `docker run --rm -it vaultwarden/server:latest /vaultwarden hash` Two traps follow. In a Compose environment file the `$` characters must be escaped as `$$`, or single-quote the whole value, because Compose otherwise interprets them as variable references. And at `/admin` you enter the original password you typed into the hash command, not the PHC string you pasted into the config. Done when: `/admin` accepts the original password and rejects a wrong one, and the value survives a `docker compose up` without Compose mangling it. ### Phase 3 · Lock it down Build: register your one account, then set `SIGNUPS_ALLOWED=false` and restart. Add anyone else by invitation from the admin panel. Confirm nothing else on the host is exposed · the vaultwarden port must not be reachable except through Caddy. Done when: a fresh browser cannot reach a registration form, an invitation flow works, and a port scan from another machine shows only 80 and 443. ### Phase 4 · Clients Build: point the official Bitwarden browser extension, desktop app and mobile app at your server URL (the self-hosted setting, entered before login). Done when: all three log in, autofill works on a real site, a passkey saves and authenticates, and a vault item created on the phone appears in the browser extension. ### Phase 5 · Import Build: import your existing vault using the standard Bitwarden export/import flow. Then delete the plaintext export file securely · a Bitwarden export is unencrypted by default, and leaving one in Downloads undoes the entire point of this project. Done when: item counts match before and after, TOTP seeds still generate correct codes, and the export file is gone from disk and from your trash. ### Phase 6 · Backup, and a restore drill Build: a nightly job that stops the container or uses a consistent snapshot, tars `vw-data`, encrypts it with `age`, and copies it off the machine. A backup that lives only on the server it backs up is not a backup. Write `restore.sh`. Then do the part that matters: restore into a throwaway instance and log in against it with a client. Done when: you have completed one full restore into a clean environment and successfully unlocked the restored vault. Not "the script runs" · a restore you performed, with the date written in the README. Until that is done, keep the hosted subscription. ### Phase 7 · Updates and the honest handover Build: an update procedure (pull image, back up first, restart, verify login) and the README. Done when: one upgrade cycle has been run and all three clients still log in. ### Out of scope (and why) - Emergency access, organizations and family sharing. - Pretending you have a security team. You are now responsible for patching a service that holds every credential you own. ### README must contain - The admin-token generation command, the `$$` escaping trap, and the note that you log in with the original password. - The date of the last successful restore drill. - The reassurance and the warning together: the clients cache the vault, so a dead server does not lock you out of your passwords · but an unpatched server holding your entire credential set is a real risk you have taken on, and hosted Bitwarden is inexpensive precisely because that risk is theirs. ===== ARCHITECTURE.md ===== # Architecture · Bitwarden ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | vaultwarden/server in Docker | rule zero: do not write a password manager | | TLS | Caddy | the clients refuse non-HTTPS servers | | Clients | The official Bitwarden apps and extensions | autofill, mobile and passkeys keep working because you did not rebuild them | | Backups | tar plus age encryption, off the box | a backup of every credential you own must be encrypted | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | vaultwarden | the vault, sync, WebAuthn | It is the product | | Caddy | TLS | Any reverse proxy with automatic certificates | | Backup | tar, age, rclone, restore.sh | Any storage rclone speaks | | Clients | nothing you built | Never fork them | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DOMAIN` · required · The exact public address; vaultwarden needs it for WebAuthn and links. - `ADMIN_TOKEN` · required, secret · An Argon2 PHC hash from docker run --rm -it vaultwarden/server:latest /vaultwarden hash. In a Compose .env either single-quote the whole value or escape every $ as $$. - `SIGNUPS_ALLOWED` · required · true until your account exists, then false. - `AGE_RECIPIENT` · required · Your age public key, for encrypting backups. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Bitwarden product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: vaultwarden/server in Docker, Caddy, The official Bitwarden apps and extensions, tar plus age encryption, off the box. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. - Until the restore drill is done, keep the hosted subscription. ===== MILESTONES.md ===== # Delivery milestones · Bitwarden Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Bring it up behind HTTPS The web vault over a valid certificate, data surviving a restart. Never run it on plain HTTP even briefly. ### Steps 1. Write docker-compose.yml with vaultwarden/server and a ./vw-data volume, port published on 127.0.0.1 only Files: `docker-compose.yml`, `.env` 2. Write the Caddyfile and point DNS vault.yourdomain.com with reverse_proxy localhost:80 (the container's port). Files: `Caddyfile` 3. Start and verify ```sh docker compose up -d curl -I https://vault.yourdomain.com ``` ### Done when - [ ] The web vault loads over HTTPS with a valid certificate - [ ] docker compose down && up keeps the data ### Watch out - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. ## M2 · The admin token The step that wastes an evening, done right. ### Steps 1. Generate the Argon2 hash ```sh docker run --rm -it vaultwarden/server:latest /vaultwarden hash ``` 2. Put it in .env correctly Single-quote the whole value, or escape each $ as $$; Compose otherwise interprets them as variables. 3. Log in at /admin with the original password you typed, not the hash ### Done when - [ ] /admin accepts the original password and rejects a wrong one - [ ] The value survives docker compose up without Compose mangling it ## M3 · Lock it down One account, signups closed, nothing else exposed. ### Steps 1. Register your account, then set SIGNUPS_ALLOWED=false and restart 2. Confirm only 80 and 443 are reachable from outside ```sh nmap -p- vault.yourdomain.com ``` ### Done when - [ ] A fresh browser cannot reach a registration form - [ ] The invitation flow from /admin works - [ ] A port scan shows only 80 and 443 ## M4 · Clients Extension, desktop and mobile all logged in to your server. ### Steps 1. Log in the browser extension and the desktop app, choosing self-hosted and entering your server URL before login 2. Log in the mobile app and test a passkey and autofill end to end ### Done when - [ ] All three log in - [ ] Autofill works on a real site - [ ] A passkey saves and authenticates - [ ] An item created on the phone appears in the extension ## M5 · Import Everything moved, and the plaintext export gone. ### Steps 1. Import through Tools > Import in the web vault 2. Securely delete the export file and empty the trash ### Done when - [ ] Item counts match before and after - [ ] TOTP seeds still generate correct codes - [ ] The export file is gone from disk and trash ## M6 · Backup, and a restore drill Encrypted nightly backup off the box, and a restore you performed. ### Steps 1. Write backup.sh: stop or snapshot, tar vw-data, encrypt with age, copy off Files: `backup.sh`, `restore.sh` ```sh docker compose stop && tar czf - vw-data | age -r $AGE_RECIPIENT -o /tmp/vw-$(date +%F).tgz.age && docker compose start rclone copy /tmp/vw-$(date +%F).tgz.age remote:vault-backups/ ``` 2. Restore into a throwaway instance and unlock the restored vault with a client ### Done when - [ ] Last night's encrypted backup exists off the box - [ ] A restore into a clean environment unlocks with a client - [ ] The restore date is written in the README ### Watch out - Until the restore drill is done, keep the hosted subscription. ## M7 · Updates and the honest handover A repeatable update and the trade written down. ### Steps 1. Write update.sh: backup, pull, up -d, verify login Files: `update.sh` 2. Write the README The admin-token command and the $$ trap, the restore date, and the reassurance plus warning: clients cache the vault so a dead server locks you out of nothing, but an unpatched server holding every credential is a risk you took on. Files: `README.md` ### Done when - [ ] One upgrade cycle run and all three clients still log in - [ ] The README carries the token trap and the restore date ===== OPERATIONS.md ===== # Operations · Bitwarden ## Backup Nightly encrypted tar off the box, thirty kept. ## Restore restore.sh into a clean stack; unlock with a client. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on the domain; a monthly reminder to run update.sh. ## Incident checklist Suspected compromise: rebuild the VPS from compose, restore the last known-good backup, rotate every credential in the vault starting with email. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Restore drill performed and dated - [ ] Port scan shows only 80 and 443 - [ ] SIGNUPS_ALLOWED=false confirmed - [ ] update.sh run once ## Launch constraint Do not market omitted Bitwarden capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The exact public address; vaultwarden needs it for WebAuthn and links. DOMAIN=https://vault.yourdomain.com # Required · secret. An Argon2 PHC hash from docker run --rm -it vaultwarden/server:latest /vaultwarden hash. In a Compose .env either single-quote the whole value or escape every $ as $$. ADMIN_TOKEN='$argon2id$v=19$...' # Required. true until your account exists, then false. SIGNUPS_ALLOWED=true # Required. Your age public key, for encrypting backups. AGE_RECIPIENT=age1...
# Bitwarden · indie build Your own password server without writing a password manager: vaultwarden in Docker behind Caddy on HTTPS, the official Bitwarden apps and extensions pointed at it, signups closed after your account exists, an admin token done the way current vaultwarden requires, encrypted nightly backups off the box, and a restore you have actually performed before trusting it. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | vaultwarden/server in Docker | rule zero: do not write a password manager | | TLS | Caddy | the clients refuse non-HTTPS servers | | Clients | The official Bitwarden apps and extensions | autofill, mobile and passkeys keep working because you did not rebuild them | | Backups | tar plus age encryption, off the box | a backup of every credential you own must be encrypted | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. The vault must be reachable from your phone and laptops. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **Docker Engine and Compose on the VPS** · free - Why: vaultwarden runs as a container. - Get it: docs.docker.com/engine/install/ubuntu, then add your user to the docker group. - Verify: docker compose version prints - [ ] **A subdomain for the vault** · free on a domain you own - Why: The browser extensions and mobile apps refuse to talk to a non-HTTPS server; a domain is how you get a certificate. - Get it: vault.yourdomain.com, one A record to the VPS. - [ ] **age for backup encryption** · free - Why: Backups of your entire credential set must be encrypted at rest wherever they go. - Get it: apt install age, or from the releases page. Generate a key pair with age-keygen and keep the private key in your existing password manager. - Verify: age --version prints - [ ] **An export of your current vault** · free - Why: Phase 5 imports it. Bitwarden exports are unencrypted by default; you will delete the file securely afterwards. - Get it: In your current manager: Export vault, JSON or CSV. Keep it only until import. - [ ] **Off-box storage for backups** · cents a month - Why: A backup on the server being backed up is not a backup. - Get it: Backblaze B2 or Cloudflare R2 plus rclone on the VPS. ## Quick start ```sh docker compose up -d curl -I https://vault.yourdomain.com ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Emergency access, organizations and family sharing. - Pretending you have a security team. - managed hosting - premium support - emergency access - polished admin - reduced security maintenance risk If one of those is essential to you, that is the reason to keep paying for Bitwarden, and the README should say so rather than pretend.
# Build brief · Bitwarden The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted password vault to replace hosted Bitwarden Premium. Build it in phases, in the order below. This is an operations task, not a coding task. Each phase ends in a verification you actually run, and Phase 6 is the one that decides whether this was a good idea. ### Rule zero Do not write a password manager. Do not invent cryptography, do not design a key derivation scheme, and do not build a browser extension. Deploy vaultwarden, the Bitwarden-compatible server, and use the official Bitwarden clients against it. The clients are the reason this works: autofill, mobile and passkeys keep functioning because you did not rebuild them. ### Stack (fixed, do not substitute) - Docker Compose with the `vaultwarden/server` image and a data volume at `./vw-data`. - Caddy in front for automatic HTTPS on a subdomain. The browser extensions and mobile apps refuse to talk to a non-HTTPS server, so this is not optional. - `age` for backup encryption. ### Phase 1 · Bring it up behind HTTPS Build: the compose file and the Caddy config, on the real subdomain, with the data volume mounted. Do not run it on plain HTTP even briefly · the clients will not connect and you will spend the time debugging the wrong layer. Done when: the web vault loads over HTTPS with a valid certificate, and the container survives `docker compose down && up` with its data intact. Do not build yet: the admin panel, backups, clients. ### Phase 2 · The admin token Build: `ADMIN_TOKEN`, and read this carefully because it is the step that wastes an evening. Current vaultwarden expects the token as an Argon2 PHC hash, not a plain string. Generate it with: `docker run --rm -it vaultwarden/server:latest /vaultwarden hash` Two traps follow. In a Compose environment file the `$` characters must be escaped as `$$`, or single-quote the whole value, because Compose otherwise interprets them as variable references. And at `/admin` you enter the original password you typed into the hash command, not the PHC string you pasted into the config. Done when: `/admin` accepts the original password and rejects a wrong one, and the value survives a `docker compose up` without Compose mangling it. ### Phase 3 · Lock it down Build: register your one account, then set `SIGNUPS_ALLOWED=false` and restart. Add anyone else by invitation from the admin panel. Confirm nothing else on the host is exposed · the vaultwarden port must not be reachable except through Caddy. Done when: a fresh browser cannot reach a registration form, an invitation flow works, and a port scan from another machine shows only 80 and 443. ### Phase 4 · Clients Build: point the official Bitwarden browser extension, desktop app and mobile app at your server URL (the self-hosted setting, entered before login). Done when: all three log in, autofill works on a real site, a passkey saves and authenticates, and a vault item created on the phone appears in the browser extension. ### Phase 5 · Import Build: import your existing vault using the standard Bitwarden export/import flow. Then delete the plaintext export file securely · a Bitwarden export is unencrypted by default, and leaving one in Downloads undoes the entire point of this project. Done when: item counts match before and after, TOTP seeds still generate correct codes, and the export file is gone from disk and from your trash. ### Phase 6 · Backup, and a restore drill Build: a nightly job that stops the container or uses a consistent snapshot, tars `vw-data`, encrypts it with `age`, and copies it off the machine. A backup that lives only on the server it backs up is not a backup. Write `restore.sh`. Then do the part that matters: restore into a throwaway instance and log in against it with a client. Done when: you have completed one full restore into a clean environment and successfully unlocked the restored vault. Not "the script runs" · a restore you performed, with the date written in the README. Until that is done, keep the hosted subscription. ### Phase 7 · Updates and the honest handover Build: an update procedure (pull image, back up first, restart, verify login) and the README. Done when: one upgrade cycle has been run and all three clients still log in. ### Out of scope (and why) - Emergency access, organizations and family sharing. - Pretending you have a security team. You are now responsible for patching a service that holds every credential you own. ### README must contain - The admin-token generation command, the `$$` escaping trap, and the note that you log in with the original password. - The date of the last successful restore drill. - The reassurance and the warning together: the clients cache the vault, so a dead server does not lock you out of your passwords · but an unpatched server holding your entire credential set is a real risk you have taken on, and hosted Bitwarden is inexpensive precisely because that risk is theirs.
# Agent instructions · Bitwarden indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: vaultwarden/server in Docker, Caddy, The official Bitwarden apps and extensions, tar plus age encryption, off the box. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. - Until the restore drill is done, keep the hosted subscription.
# Build plan · Bitwarden Your own password server without writing a password manager: vaultwarden in Docker behind Caddy on HTTPS, the official Bitwarden apps and extensions pointed at it, signups closed after your account exists, an admin token done the way current vaultwarden requires, encrypted nightly backups off the box, and a restore you have actually performed before trusting it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Bring it up behind HTTPS The web vault over a valid certificate, data surviving a restart. Never run it on plain HTTP even briefly. ### Steps 1. Write docker-compose.yml with vaultwarden/server and a ./vw-data volume, port published on 127.0.0.1 only Files: `docker-compose.yml`, `.env` 2. Write the Caddyfile and point DNS vault.yourdomain.com with reverse_proxy localhost:80 (the container's port). Files: `Caddyfile` 3. Start and verify ```sh docker compose up -d curl -I https://vault.yourdomain.com ``` ### Done when - [ ] The web vault loads over HTTPS with a valid certificate - [ ] docker compose down && up keeps the data ### Watch out - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. ## Phase 2 · The admin token The step that wastes an evening, done right. ### Steps 1. Generate the Argon2 hash ```sh docker run --rm -it vaultwarden/server:latest /vaultwarden hash ``` 2. Put it in .env correctly Single-quote the whole value, or escape each $ as $$; Compose otherwise interprets them as variables. 3. Log in at /admin with the original password you typed, not the hash ### Done when - [ ] /admin accepts the original password and rejects a wrong one - [ ] The value survives docker compose up without Compose mangling it ## Phase 3 · Lock it down One account, signups closed, nothing else exposed. ### Steps 1. Register your account, then set SIGNUPS_ALLOWED=false and restart 2. Confirm only 80 and 443 are reachable from outside ```sh nmap -p- vault.yourdomain.com ``` ### Done when - [ ] A fresh browser cannot reach a registration form - [ ] The invitation flow from /admin works - [ ] A port scan shows only 80 and 443 ## Phase 4 · Clients Extension, desktop and mobile all logged in to your server. ### Steps 1. Log in the browser extension and the desktop app, choosing self-hosted and entering your server URL before login 2. Log in the mobile app and test a passkey and autofill end to end ### Done when - [ ] All three log in - [ ] Autofill works on a real site - [ ] A passkey saves and authenticates - [ ] An item created on the phone appears in the extension ## Phase 5 · Import Everything moved, and the plaintext export gone. ### Steps 1. Import through Tools > Import in the web vault 2. Securely delete the export file and empty the trash ### Done when - [ ] Item counts match before and after - [ ] TOTP seeds still generate correct codes - [ ] The export file is gone from disk and trash ## Phase 6 · Backup, and a restore drill Encrypted nightly backup off the box, and a restore you performed. ### Steps 1. Write backup.sh: stop or snapshot, tar vw-data, encrypt with age, copy off Files: `backup.sh`, `restore.sh` ```sh docker compose stop && tar czf - vw-data | age -r $AGE_RECIPIENT -o /tmp/vw-$(date +%F).tgz.age && docker compose start rclone copy /tmp/vw-$(date +%F).tgz.age remote:vault-backups/ ``` 2. Restore into a throwaway instance and unlock the restored vault with a client ### Done when - [ ] Last night's encrypted backup exists off the box - [ ] A restore into a clean environment unlocks with a client - [ ] The restore date is written in the README ### Watch out - Until the restore drill is done, keep the hosted subscription. ## Phase 7 · Updates and the honest handover A repeatable update and the trade written down. ### Steps 1. Write update.sh: backup, pull, up -d, verify login Files: `update.sh` 2. Write the README The admin-token command and the $$ trap, the restore date, and the reassurance plus warning: clients cache the vault so a dead server locks you out of nothing, but an unpatched server holding every credential is a risk you took on. Files: `README.md` ### Done when - [ ] One upgrade cycle run and all three clients still log in - [ ] The README carries the token trap and the restore date ## Not in this build - Emergency access, organizations and family sharing. - Pretending you have a security team. ## After v1, if you want it - A second admin device with hardware-key 2FA on your account - Fail2ban in front of the login endpoint
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The exact public address; vaultwarden needs it for WebAuthn and links. DOMAIN=https://vault.yourdomain.com # Required · secret. An Argon2 PHC hash from docker run --rm -it vaultwarden/server:latest /vaultwarden hash. In a Compose .env either single-quote the whole value or escape every $ as $$. ADMIN_TOKEN='$argon2id$v=19$...' # Required. true until your account exists, then false. SIGNUPS_ALLOWED=true # Required. Your age public key, for encrypting backups. AGE_RECIPIENT=age1...
# Bitwarden · product brief ## Problem If your goal is to stop paying hosted Bitwarden Premium, the core vault can be self-hosted or replaced with KeePass-style local storage; the caveat is security responsibility. ## Product outcome A credential server you operate responsibly: closed signups, encrypted off-box backups with a proven restore, and a monthly patch routine. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - server or local vault - domain/HTTPS if self-hosting - secure backup - browser/mobile clients ## Explicit non-goals for v1 - Emergency access, organizations and family sharing. - Pretending you have a security team. - managed hosting - premium support - emergency access - polished admin - reduced security maintenance risk ## Success criteria - Restore drill performed and dated - Port scan shows only 80 and 443 - SIGNUPS_ALLOWED=false confirmed - update.sh run once
# Build brief · Bitwarden The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted password vault to replace hosted Bitwarden Premium. Build it in phases, in the order below. This is an operations task, not a coding task. Each phase ends in a verification you actually run, and Phase 6 is the one that decides whether this was a good idea. ### Rule zero Do not write a password manager. Do not invent cryptography, do not design a key derivation scheme, and do not build a browser extension. Deploy vaultwarden, the Bitwarden-compatible server, and use the official Bitwarden clients against it. The clients are the reason this works: autofill, mobile and passkeys keep functioning because you did not rebuild them. ### Stack (fixed, do not substitute) - Docker Compose with the `vaultwarden/server` image and a data volume at `./vw-data`. - Caddy in front for automatic HTTPS on a subdomain. The browser extensions and mobile apps refuse to talk to a non-HTTPS server, so this is not optional. - `age` for backup encryption. ### Phase 1 · Bring it up behind HTTPS Build: the compose file and the Caddy config, on the real subdomain, with the data volume mounted. Do not run it on plain HTTP even briefly · the clients will not connect and you will spend the time debugging the wrong layer. Done when: the web vault loads over HTTPS with a valid certificate, and the container survives `docker compose down && up` with its data intact. Do not build yet: the admin panel, backups, clients. ### Phase 2 · The admin token Build: `ADMIN_TOKEN`, and read this carefully because it is the step that wastes an evening. Current vaultwarden expects the token as an Argon2 PHC hash, not a plain string. Generate it with: `docker run --rm -it vaultwarden/server:latest /vaultwarden hash` Two traps follow. In a Compose environment file the `$` characters must be escaped as `$$`, or single-quote the whole value, because Compose otherwise interprets them as variable references. And at `/admin` you enter the original password you typed into the hash command, not the PHC string you pasted into the config. Done when: `/admin` accepts the original password and rejects a wrong one, and the value survives a `docker compose up` without Compose mangling it. ### Phase 3 · Lock it down Build: register your one account, then set `SIGNUPS_ALLOWED=false` and restart. Add anyone else by invitation from the admin panel. Confirm nothing else on the host is exposed · the vaultwarden port must not be reachable except through Caddy. Done when: a fresh browser cannot reach a registration form, an invitation flow works, and a port scan from another machine shows only 80 and 443. ### Phase 4 · Clients Build: point the official Bitwarden browser extension, desktop app and mobile app at your server URL (the self-hosted setting, entered before login). Done when: all three log in, autofill works on a real site, a passkey saves and authenticates, and a vault item created on the phone appears in the browser extension. ### Phase 5 · Import Build: import your existing vault using the standard Bitwarden export/import flow. Then delete the plaintext export file securely · a Bitwarden export is unencrypted by default, and leaving one in Downloads undoes the entire point of this project. Done when: item counts match before and after, TOTP seeds still generate correct codes, and the export file is gone from disk and from your trash. ### Phase 6 · Backup, and a restore drill Build: a nightly job that stops the container or uses a consistent snapshot, tars `vw-data`, encrypts it with `age`, and copies it off the machine. A backup that lives only on the server it backs up is not a backup. Write `restore.sh`. Then do the part that matters: restore into a throwaway instance and log in against it with a client. Done when: you have completed one full restore into a clean environment and successfully unlocked the restored vault. Not "the script runs" · a restore you performed, with the date written in the README. Until that is done, keep the hosted subscription. ### Phase 7 · Updates and the honest handover Build: an update procedure (pull image, back up first, restart, verify login) and the README. Done when: one upgrade cycle has been run and all three clients still log in. ### Out of scope (and why) - Emergency access, organizations and family sharing. - Pretending you have a security team. You are now responsible for patching a service that holds every credential you own. ### README must contain - The admin-token generation command, the `$$` escaping trap, and the note that you log in with the original password. - The date of the last successful restore drill. - The reassurance and the warning together: the clients cache the vault, so a dead server does not lock you out of your passwords · but an unpatched server holding your entire credential set is a real risk you have taken on, and hosted Bitwarden is inexpensive precisely because that risk is theirs.
# Architecture · Bitwarden ## Stack | Part | Choice | Why | | --- | --- | --- | | Server | vaultwarden/server in Docker | rule zero: do not write a password manager | | TLS | Caddy | the clients refuse non-HTTPS servers | | Clients | The official Bitwarden apps and extensions | autofill, mobile and passkeys keep working because you did not rebuild them | | Backups | tar plus age encryption, off the box | a backup of every credential you own must be encrypted | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | vaultwarden | the vault, sync, WebAuthn | It is the product | | Caddy | TLS | Any reverse proxy with automatic certificates | | Backup | tar, age, rclone, restore.sh | Any storage rclone speaks | | Clients | nothing you built | Never fork them | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DOMAIN` · required · The exact public address; vaultwarden needs it for WebAuthn and links. - `ADMIN_TOKEN` · required, secret · An Argon2 PHC hash from docker run --rm -it vaultwarden/server:latest /vaultwarden hash. In a Compose .env either single-quote the whole value or escape every $ as $$. - `SIGNUPS_ALLOWED` · required · true until your account exists, then false. - `AGE_RECIPIENT` · required · Your age public key, for encrypting backups. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Bitwarden product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: vaultwarden/server in Docker, Caddy, The official Bitwarden apps and extensions, tar plus age encryption, off the box. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. - Until the restore drill is done, keep the hosted subscription.
# Delivery milestones · Bitwarden Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Bring it up behind HTTPS The web vault over a valid certificate, data surviving a restart. Never run it on plain HTTP even briefly. ### Steps 1. Write docker-compose.yml with vaultwarden/server and a ./vw-data volume, port published on 127.0.0.1 only Files: `docker-compose.yml`, `.env` 2. Write the Caddyfile and point DNS vault.yourdomain.com with reverse_proxy localhost:80 (the container's port). Files: `Caddyfile` 3. Start and verify ```sh docker compose up -d curl -I https://vault.yourdomain.com ``` ### Done when - [ ] The web vault loads over HTTPS with a valid certificate - [ ] docker compose down && up keeps the data ### Watch out - The clients will not connect over HTTP and you will spend the time debugging the wrong layer. ## M2 · The admin token The step that wastes an evening, done right. ### Steps 1. Generate the Argon2 hash ```sh docker run --rm -it vaultwarden/server:latest /vaultwarden hash ``` 2. Put it in .env correctly Single-quote the whole value, or escape each $ as $$; Compose otherwise interprets them as variables. 3. Log in at /admin with the original password you typed, not the hash ### Done when - [ ] /admin accepts the original password and rejects a wrong one - [ ] The value survives docker compose up without Compose mangling it ## M3 · Lock it down One account, signups closed, nothing else exposed. ### Steps 1. Register your account, then set SIGNUPS_ALLOWED=false and restart 2. Confirm only 80 and 443 are reachable from outside ```sh nmap -p- vault.yourdomain.com ``` ### Done when - [ ] A fresh browser cannot reach a registration form - [ ] The invitation flow from /admin works - [ ] A port scan shows only 80 and 443 ## M4 · Clients Extension, desktop and mobile all logged in to your server. ### Steps 1. Log in the browser extension and the desktop app, choosing self-hosted and entering your server URL before login 2. Log in the mobile app and test a passkey and autofill end to end ### Done when - [ ] All three log in - [ ] Autofill works on a real site - [ ] A passkey saves and authenticates - [ ] An item created on the phone appears in the extension ## M5 · Import Everything moved, and the plaintext export gone. ### Steps 1. Import through Tools > Import in the web vault 2. Securely delete the export file and empty the trash ### Done when - [ ] Item counts match before and after - [ ] TOTP seeds still generate correct codes - [ ] The export file is gone from disk and trash ## M6 · Backup, and a restore drill Encrypted nightly backup off the box, and a restore you performed. ### Steps 1. Write backup.sh: stop or snapshot, tar vw-data, encrypt with age, copy off Files: `backup.sh`, `restore.sh` ```sh docker compose stop && tar czf - vw-data | age -r $AGE_RECIPIENT -o /tmp/vw-$(date +%F).tgz.age && docker compose start rclone copy /tmp/vw-$(date +%F).tgz.age remote:vault-backups/ ``` 2. Restore into a throwaway instance and unlock the restored vault with a client ### Done when - [ ] Last night's encrypted backup exists off the box - [ ] A restore into a clean environment unlocks with a client - [ ] The restore date is written in the README ### Watch out - Until the restore drill is done, keep the hosted subscription. ## M7 · Updates and the honest handover A repeatable update and the trade written down. ### Steps 1. Write update.sh: backup, pull, up -d, verify login Files: `update.sh` 2. Write the README The admin-token command and the $$ trap, the restore date, and the reassurance plus warning: clients cache the vault so a dead server locks you out of nothing, but an unpatched server holding every credential is a risk you took on. Files: `README.md` ### Done when - [ ] One upgrade cycle run and all three clients still log in - [ ] The README carries the token trap and the restore date
# Operations · Bitwarden ## Backup Nightly encrypted tar off the box, thirty kept. ## Restore restore.sh into a clean stack; unlock with a client. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on the domain; a monthly reminder to run update.sh. ## Incident checklist Suspected compromise: rebuild the VPS from compose, restore the last known-good backup, rotate every credential in the vault starting with email. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Restore drill performed and dated - [ ] Port scan shows only 80 and 443 - [ ] SIGNUPS_ALLOWED=false confirmed - [ ] update.sh run once ## Launch constraint Do not market omitted Bitwarden capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The exact public address; vaultwarden needs it for WebAuthn and links. DOMAIN=https://vault.yourdomain.com # Required · secret. An Argon2 PHC hash from docker run --rm -it vaultwarden/server:latest /vaultwarden hash. In a Compose .env either single-quote the whole value or escape every $ as $$. ADMIN_TOKEN='$argon2id$v=19$...' # Required. true until your account exists, then false. SIGNUPS_ALLOWED=true # Required. Your age public key, for encrypting backups. AGE_RECIPIENT=age1...
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because a professionally maintained security service is cheap relative to the downside of mistakes.
xmanaged hosting
xpremium support
xemergency access
xpolished admin
xreduced security maintenance risk
Don't feel like building it? These folks already made it free.
all 5 free alternatives to Bitwarden →· no votes, no pay-to-list · just what's real
Bitwarden pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free (individual) | $0 | $0 | 1 user; unlimited passwords, passkeys and devices; share with 1 other Bitwarden user; text-only Send. |
| free organization | $0/workspace | $0/workspace | 2 users and 2 collections; unlimited vault items. |
| premium | — | $1.65 | 1 user; unlimited passwords/devices; 5 GB personal attachment storage; up to 10 hardware security keys. |
| families | — | $3.99 | Up to 6 users; unlimited collections; 5 GB personal storage plus 5 GB organization storage. |
| teams | — | $4/user | Unlimited users and collections; Premium features for every seat; 5 GB personal plus 5 GB organization storage. |
| enterprise | — | $6/user | Unlimited users and collections; includes SSO, account recovery, self-hosting and 1 Families sponsorship per user. |
free tierFree individual: 1 user, unlimited passwords/passkeys/devices, sharing with 1 other user; Free organization: 2 users and 2 collections.
billingpublished prices are annual subscriptions; no public month-to-month prices displayed; taxes excluded
hidden costsExtra encrypted storage costs $0.33/GB/month; organization seats added by invitations are prorated, and unused purchased seats remain billable until the subscription seat count is reduced.
verified 2026-08-11 · source ↗
Is Bitwarden free?
Free covers unlimited passwords, devices and passkeys; Premium adds TOTP and reports. Paid is Premium at $1.65/mo (checked 2026-08-07).
Vibecode Bitwarden
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Bitwarden replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Bitwarden cost?
Bitwarden costs about $1.65/month (Premium, checked 2026-08-07), which is $19.799999999999997 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Bitwarden?
Honestly: managed hosting; premium support; emergency access; polished admin; reduced security maintenance risk. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Bitwarden?
Yes: Vaultwarden (Bitwarden’s clients pointed at your own server; unofficial, capable, and now you are the outage.) KeePassXC (A vault file on your disk with excellent autofill; syncing and sharing are deliberately somebody else’s job.) Passbolt Community Edition (A team password vault with real sharing and a server stack that expects an adult in the room.) All 5 curated free alternatives are at vibecodeit.com/bitwarden/alternatives. The prompt is for when you want it exactly your way.