A self-hosted, shared credential vault for personal usage. Secrets are encrypted at rest. Every action is recorded in an append-only audit log. Login is Google-only — no passwords.
VaultKey is an internal-tier shared vault. Everyone approved sees the same list of entries. Secrets are masked until revealed. Accountability comes from the audit log, not per-entry permissions.
It is not a zero-knowledge, Bitwarden-grade vault — that tradeoff is deliberate. See docs/02_security_model.md.
| Layer | Technology |
|---|---|
| Frontend | Vue 3 (Composition API) + Vite + Vue Router + Pinia + Tailwind CSS v4 |
| Backend | FastAPI + Uvicorn |
| Database | PostgreSQL (raw SQL via psycopg[binary], no ORM) |
| Auth | Google Identity Services + PyJWT |
| 2FA | pyotp + qrcode |
| Encryption | Fernet (cryptography) |
| Resend | |
| Infra | Docker + docker-compose (VPS) + Cloudflare Pages (frontend) |
Login Page
Main Dashboard
Admin Panel
MFA Device
Adding New Entry

vault_key/
├── backend/
│ ├── routers/
│ │ ├── auth.py # Google login, access request form
│ │ ├── vault.py # Vault CRUD with Fernet encryption
│ │ ├── totp.py # TOTP enroll / verify / unlock / reset
│ │ └── admin.py # Superadmin: users, requests, audit, settings
│ ├── audit.py # log_action() helper (append-only)
│ ├── config.py # pydantic-settings — reads .env
│ ├── crypto.py # encrypt / decrypt via Fernet
│ ├── db.py # psycopg connection pool + query helpers
│ ├── dependencies.py # FastAPI deps: get_current_user, require_totp_unlock
│ ├── main.py # App entry, CORS, router registration
│ ├── Dockerfile
│ └── requirements.txt
├── frontend/
│ └── src/
│ ├── components/
│ │ ├── AddEntryModal.vue
│ │ ├── EditEntryModal.vue
│ │ ├── TotpEnrollModal.vue
│ │ ├── TotpUnlockModal.vue
│ │ ├── ToastNotification.vue
│ │ └── UserMenu.vue
│ ├── pages/
│ │ ├── LoginPage.vue
│ │ ├── RequestAccessPage.vue
│ │ ├── VaultPage.vue
│ │ └── AdminPage.vue
│ ├── router/index.js
│ ├── services/api.js # Axios instance + TOTP interceptor
│ ├── stores/
│ │ ├── toast.js
│ │ └── totp.js # Promise-gate for TOTP unlock modal
│ └── utils/jwt.js # Safe JWT payload parser
├── db/
│ ├── create_db.sql # Creates the database (run once)
│ ├── init.sql # Creates all tables + seeds (idempotent)
│ └── migrations/ # Incremental changes (applied in order)
├── docs/ # Design decisions and architecture docs
├── .github/workflows/
│ └── deploy.yml # CI/CD: build image → push → SSH deploy
├── docker-compose.yml
└── .env.example
- Python 3.12+
- Node.js 18+
- PostgreSQL 15+
git clone https://github.com/your-username/vault_key.git
cd vault_key
cp .env.example .envEdit .env and fill in all values (see Environment variables below).
# Create the database (run once)
psql -U postgres -f db/create_db.sql
# Create all tables and seed superadmins (idempotent)
psql -U postgres -d vaultkey -f db/init.sqlBefore running db/init.sql, replace the two REPLACE_ME_admin*@example.com placeholders with real Google account emails.
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reloadBackend runs at http://localhost:8000.
cd frontend
cp .env.example .env # set VITE_API_BASE_URL and VITE_GOOGLE_CLIENT_ID
npm install
npm run devFrontend runs at http://localhost:5173.
Copy .env.example to .env at the repo root. Required keys:
| Variable | Purpose |
|---|---|
DATABASE_URL |
psycopg connection string |
FERNET_KEY |
Fernet encryption key — generate once with Fernet.generate_key() |
JWT_SECRET |
Secret for signing session JWTs |
GOOGLE_CLIENT_ID |
OAuth client ID from Google Cloud Console |
RESEND_API |
Resend API key for transactional email |
RESEND_EMAIL_FROM |
Verified sender address on Resend |
FRONTEND_URL |
Allowed CORS origin (e.g. https://yourapp.pages.dev) |
The frontend needs its own frontend/.env:
| Variable | Purpose |
|---|---|
VITE_API_BASE_URL |
Backend API base URL |
VITE_GOOGLE_CLIENT_ID |
Same Google OAuth client ID |
Never commit .env files. Never put FERNET_KEY in code or the database.
The backend and PostgreSQL run together via docker-compose. The frontend is deployed separately on Cloudflare Pages.
# Start backend + db
docker compose up -d
# Stream logs
docker compose logs -f
# psql into the db container
docker compose exec db psql -U postgres vaultkeyBrowser
→ Google Identity Services → Google ID token
→ POST /auth/google
→ verify token signature + audience + expiry
→ check users.status (approved / revoked)
→ issue 8h PyJWT session token
→ all subsequent requests: Authorization: Bearer <JWT>
→ vault actions require active TOTP unlock window (server-side, 30 min)
→ if expired: frontend prompts 6-digit code → backend verifies → window reset
→ Fernet decrypts (view) or encrypts (add/edit)
→ every action → INSERT into audit_log (append-only)
- Secrets are encrypted (Fernet), never hashed — the eye button needs the value back
- All SQL uses
%sparameterized queries — no f-strings into SQL, ever - The
FERNET_KEYlives only in env vars / secrets manager - The audit log is append-only — no
UPDATEorDELETEagainst it, ever - Login is Google-only — no username/password, no "forgot password"
- TOTP unlock is enforced server-side — the frontend only prompts, the backend enforces
Detailed design decisions are in /docs:
00_README.md— overview01_architecture.md— tech stack and request flow02_security_model.md— encryption vs hashing, key handling, security tier03_database_schema.md— all tables and columns04_audit_logging.md— append-only design05_development_roadmap.md— build order06_future_enhancements.md— deferred upgrades