A web-based password manager with zero-knowledge architecture. All cryptography happens client-side in the browser using OpenPGP.js. The server stores only encrypted blobs — it never sees plaintext passwords or private keys.
- Client-side encryption — PGP encryption/decryption in the browser with OpenPGP.js
- Zero-knowledge architecture — Server never sees plaintext passwords or private keys
- Multi-user support — Each user identified by their PGP key fingerprint
- Two-factor authentication — Optional TOTP (2FA) for server access
- Import/Export — Compatible with standard
.password-storedirectory format (pass CLI compatible) - Password generator — Configurable random password generation
- Session management — 5-minute JWT sessions with automatic expiry
- Git sync — Backup and sync encrypted entries to any Git repository (GitHub, GitLab, Gitea)
- TOTP codes — Store and generate TOTP codes using
otpauth://URI format (pass-otp compatible) - Theme toggle — Auto-switching light/dark themes based on time of day (8AM-10PM)
- Account management — Clear local data or permanently delete account with passphrase confirmation
- Auto-hide sensitive content — Password and notes auto-hide after 15 seconds with countdown timer
- PGP key auto-lock — Decrypted PGP key auto-locks after 30s of inactivity
- Rate limiting — Sliding window rate limiting on authentication endpoints (5 attempts / 15 min default)
- Registration protection — TOTP-based registration codes to prevent unauthorized account creation
┌─────────────────────────────────────┐
│ Browser (SPA) │
│ + OpenPGP.js + IndexedDB │
│ + PGP encrypt/decrypt │
│ + TOTP code generation │
└────────────────────────────────────┘
│ HTTPS + CORS + JWT
┌──────────────▼──────────────────────┐
│ Go API Server (SQLite backend) │
│ + JWT Auth + bcrypt + TOTP │
│ + Rate limiting │
│ + Git sync (go-git) │
└─────────────────────────────────────┘
│ HTTPS (PAT) / SSH (key)
┌──────────────▼──────────────────────┐
│ Remote Git Repo (optional) │
│ └── *.gpg (encrypted blobs) │
└─────────────────────────────────────┘
| Layer | Technology |
|---|---|
| Frontend | TypeScript + Preact + Vite |
| Crypto | OpenPGP.js + Web Crypto API (PBKDF2) |
| Backend | Go 1.26 + SQLite (pure-Go, no CGO) |
| Git | go-git (pure Go, no Git CLI needed) |
| Auth | bcrypt + JWT (5-min) + TOTP (2FA) |
| Testing | Playwright (81 E2E tests) + Vitest |
| Deploy | Docker (single container) |
- Go 1.26+
- Node.js 24+
- npm
- Docker (for container deployment)
# Build backend
go build -o webpass-server ./cmd/srv
# Build frontend (optional, for static serving)cd frontend && npm run build# Set environment variablesexport JWT_SECRET=$(openssl rand -hex 32)export DB_PATH=./db.sqlite3
# Run server (development)
go run ./cmd/srv
# Or run the compiled binary
./webpass-serverServer listens on :8080 by default.
# Build image
docker build -t webpass:latest .# Run container
docker run -d \
--name webpass \
-p 8080:8080 \
-v webpass-data:/data \
-e JWT_SECRET="$(openssl rand -hex 32)" \
--read-only \
--security-opt no-new-privileges:true \
webpass:latest# Configure environment
cp .env.example .env
# Edit .env and set JWT_SECRET# Start
docker compose up -dMigrations are applied automatically on startup via embedded SQL files in db/migrations/.
They are numbered (NNN-name.sql) and tracked in a migrations table — each runs exactly once.
Forward upgrade ✅ — Fully supported. All migrations only add columns/tables,
never remove or rename. Upgrading from older versions (e.g. v0.4.6) to latest is safe.
Rollback ❌ — Not supported. Because sqlc generates SELECT * queries, the old
binary will fail to scan rows if new columns were added. To downgrade, restore both
the binary and the database from backup.
The container runs with:
- Non-root user (UID/GID 8080)
- Read-only filesystem (
--read-only) - No privilege escalation (
--security-opt no-new-privileges:true) - Writable path only
/data(SQLite + git repos)
# Backend tests
go test ./...
# Frontend unit testscd frontend && npm test81 browser-based integration tests across 5 phases:
# Run comprehensive test suite
./frontend/playwright-e2e-test.sh
# Or use npx directlycd frontend && npx playwright test# Run with interactive UI
npx playwright test --ui
# Run with visible browser
npx playwright test --headed
# View HTML report
npx playwright show-reportTest Coverage:
- Phase 1: Rate limit tests (3 tests)
- Phase 2: All tests in Protected mode (62 tests)
- Phase 3: Registration tests in Open mode (6 tests)
- Phase 4: Registration tests in Protected mode (8 tests)
- Phase 5: Registration tests in Disabled mode (2 tests)
Git sync E2E tests require a real Git repository. Set credentials via:
# Option 1: .env file (gitignored)
WEBPASS_REPO_URL="https://gitea.example.com/user/password-store.git"
WEBPASS_REPO_PAT="your-personal-access-token"# Option 2: Environment variablesexport WEBPASS_REPO_URL="..."export WEBPASS_REPO_PAT="..."# Option 3: GitHub Secrets (CI/CD)# Add WEBPASS_REPO_URL and WEBPASS_REPO_PAT to repository secretsAll endpoints require JWT authentication except where noted.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api | No | Create user (first-time setup) |
| GET | /api/{fingerprint} | JWT | Get user info |
| POST | /api/{fingerprint}/login | No | Login → returns JWT or 2FA challenge |
| POST | /api/{fingerprint}/login/2fa | No | Complete 2FA login |
| DELETE | /api/{fingerprint}/account | JWT | Delete account permanently |
| POST | /api/{fingerprint}/password | JWT | Change password |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/registration/mode | No | Get registration mode (open/protected/disabled) |
| POST | /api/registration/validate | No | Validate registration TOTP code |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/{fingerprint}/totp/setup | JWT | Begin TOTP 2FA setup |
| POST | /api/{fingerprint}/totp/confirm | JWT | Confirm TOTP 2FA setup |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/{fingerprint}/entries | JWT | List all entry paths |
| GET | /api/{fingerprint}/entries/* | JWT | Download encrypted blob |
| PUT | /api/{fingerprint}/entries/* | JWT | Upload encrypted blob |
| DELETE | /api/{fingerprint}/entries/* | JWT | Delete entry |
| POST | /api/{fingerprint}/entries/move | JWT | Rename/move entry |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/{fingerprint}/export | JWT | Export all entries as .tar.gz |
| POST | /api/{fingerprint}/import | JWT | Import password store (JSON or .tar.gz) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/{fingerprint}/git/status | JWT | Get git sync status |
| GET | /api/{fingerprint}/git/config | JWT | Get git configuration |
| POST | /api/{fingerprint}/git/config | JWT | Configure git sync |
| POST | /api/{fingerprint}/git/session | JWT | Set git token for current session |
| POST | /api/{fingerprint}/git/push | JWT | Manual push to remote (force overwrite) |
| POST | /api/{fingerprint}/git/pull | JWT | Manual pull from remote (fresh clone) |
| POST | /api/{fingerprint}/git/toggle-sync | JWT | Enable/disable git sync |
| GET | /api/{fingerprint}/git/log | JWT | Get sync operation history (last 50) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/health | No | Health check |
| GET | /api/version | No | Get server version |
- Private keys never leave the browser — Stored AES-wrapped in IndexedDB
- Server stores only PGP-encrypted blobs — Database leak reveals nothing
- Password validates locally first — Wrong password fails before network call
- 5-minute sessions — JWT expiry enforced server-side
- PGP key auto-lock — Private key automatically locked after 30s of inactivity (hardcoded in
frontend/src/lib/session.ts) - CORS locked to specific origins via
CORS_ORIGINSenv var - All traffic over HTTPS required in production
- Rate limiting on authentication endpoints (sliding window, 5 attempts / 15 min default)
- Registration protection with TOTP-based codes (optional, configurable)
Please do not open public issues for security vulnerabilities.
- Email: Send details to the maintainers privately
- GitHub: Use private vulnerability reporting
Response Timeline:
- Acknowledgment: Within 48 hours
- Initial Assessment: Within 1 week
- Fix Timeline: Depends on severity (Critical: 24-72h, High: 1-2 weeks, Medium: 2-4 weeks)
See .env.example for all available options with detailed comments.
| Variable | Required | Description |
|---|---|---|
JWT_SECRET | Yes | 32-byte hex string for JWT signing |
DB_PATH | No | Path to SQLite database (default: /data/db/db.sqlite3) |
STATIC_DIR | No | Path to frontend dist/ directory |
CORS_ORIGINS | No | Comma-separated allowed origins |
PORT | No | HTTP listen port (default: 8080) |
GIT_REPO_ROOT | No | Git repos directory (default: /data/git-repos) |
SESSION_HARDLIMIT_MINUTES | No | JWT hard limit (max session time) in minutes (default: 30, range: 5-480) |
SESSION_SOFTLIMIT_MINUTES | No | JWT soft limit (browser close detection) in minutes (default: 5, range: 1-60) |
DISABLE_FRONTEND | No | Disable frontend (1 or true) |
BCRYPT_COST | No | Password hashing cost factor (default: 12, range: 10-15) |
RATE_LIMIT_ATTEMPTS | No | Max requests per window (default: 5) |
RATE_LIMIT_WINDOW_MINUTES | No | Time window in minutes (default: 15) |
| Variable | Default | Description |
|---|---|---|
REGISTRATION_ENABLED | false | Enable registration (1 or true) |
REGISTRATION_TOTP_SECRET | (empty) | Base32 TOTP secret (required for protected mode) |
REGISTRATION_TOTP_PERIOD | 3600 | Code validity period in seconds (15-86400) |
REGISTRATION_TOTP_ALGO | SHA1 | Hash algorithm (SHA1/SHA256/SHA512) |
REGISTRATION_CODE_FILE | /data/registration_code.txt | Path to write current code |
Registration Modes:
REGISTRATION_ENABLED | REGISTRATION_TOTP_SECRET | Mode |
|---|---|---|
false | (any) | Disabled — No registration allowed |
true | (not set) | Open — No code required |
true | (set) | Protected — 6-digit TOTP code required |
One-way overwrite sync with fresh clone/export:
- Pull: Remote → Local (clone remote, replace local DB)
- Push: Local → Remote (export local DB, force-push to remote)
- No merge conflicts — Last write wins
- Fresh operations — Local git repo is temporary, cleaned before/after each operation
- Per-user configuration — Each user has their own repo URL
- Two authentication methods:
- HTTPS — PGP-encrypted Personal Access Token
- SSH — PGP-encrypted SSH private key with TOFU host key verification
repo-root/
├── .git/
├── email.gpg
├── work/
│ └── database.gpg
└── social/
└── twitter.gpg
- All files are
.gpgencrypted blobs (client-side PGP encryption) - Files at repo root (no
.password-store/subdirectory) - Compatible with standard
passCLI after cloning
Default branch auto-detection (branch = "HEAD"):
- Read remote HEAD symbolic reference
- If HEAD not found → try
main - If
mainnot found → trymaster
Auto-switching theme system with manual override:
| Theme | Description |
|---|---|
| Ocean | Dark blue professional theme (default for night) |
| Daylight | Clean white/blue light theme (default for day) |
Auto mode (default):
- 8:00 AM - 10:00 PM → Daylight theme
- 10:00 PM - 8:00 AM → Ocean theme
Manual override: Click theme toggle button to cycle: 🔄 Auto → 🌙 Ocean → ☀️ Daylight → Auto
Preference saved to localStorage and persists across sessions.
.
├── .github/workflows/ # CI/CD pipelines
├── cmd/srv/ # Main binary entrypoint
── srv/ # HTTP server + handlers
├── db/ # Database migrations + sqlc queries
├── frontend/ # Preact + TypeScript SPA
│ ├── src/
│ ├── index.html
│ └── package.json
── Dockerfile
├── docker-compose.yml
└── .github/ # GitHub Actions workflows
Contributions are welcome! Please read CONTRIBUTING.md for guidelines and AGENTS.md for the development guide.
# Clone repository
git clone https://github.com/johnwmail/webpass.git
cd webpass
# Backend
go mod download
go test ./...
# Frontendcd frontend
npm install
npm run dev- Backend:
go test ./...must pass - Frontend unit:
npm testmust pass - E2E tests:
./frontend/playwright-e2e-test.shmust pass (81 tests) - Type check:
npm run typecheckmust pass
Go:
- Follow Effective Go
- Use
gofmtorgoimports - Add tests for new packages
TypeScript:
- Use TypeScript for all new code
- Follow existing code style
- Add types for function signatures
General:
- Write self-documenting code
- Keep PRs under 400 lines when possible
- Update tests with code changes
Never commit:
- Passwords or secrets
- API keys or tokens
- Private encryption keys
- Production database files
MIT License — see LICENSE for details.
- Inspired by pass
- Built with OpenPGP.js
- Backend powered by Go and SQLite
- Git operations via go-git