Rebrand from Forte (music-store-specific) to LunarFront (any small business): - Package namespace @forte/* → @lunarfront/* - Database forte/forte_test → lunarfront/lunarfront_test - Docker containers, volumes, connection strings - UI branding, localStorage keys, test emails - All documentation and planning docs Generalize music-specific terminology: - instrumentDescription → itemDescription - instrumentCount → itemCount - instrumentType → itemCategory (on service templates) - New migration 0027_generalize_terminology for column renames - Seed data updated with generic examples - RBAC descriptions updated
10 KiB
Music Store Management Platform
Vault — On-Premise Password Manager
Version 1.0 | Draft
1. Overview
LunarFront includes a built-in password vault for on-premise deployments. Music stores deal with dozens of credentials — supplier portals, distributor accounts, payment processor dashboards, shipping services, insurance logins, ASCAP/BMI licensing portals — and most store owners are not technical enough to adopt a separate password manager.
The vault is a simple, encrypted credential store baked into LunarFront. It encrypts all secrets at rest using AES-256-GCM with a key derived from a master passphrase. The key exists only in server memory and is discarded on restart.
On-premise only. Cloud/hosted deployments would require a more sophisticated key management approach (HSM, envelope encryption) and are out of scope for this design.
2. Security Model
2.1 Encryption
- Algorithm: AES-256-GCM (authenticated encryption)
- Key derivation: PBKDF2 or Argon2id from master passphrase + stored salt
- Each entry encrypted individually (not a single encrypted blob)
- Each entry gets its own random IV/nonce
- Encrypted fields: username, password, notes
- Plaintext fields: entry name, URL, category, tags (searchable)
2.2 Key Lifecycle
- First-time setup: Admin sets master passphrase via the admin UI. System generates a random salt, derives the encryption key, encrypts a known verification string, and stores the salt + verification ciphertext in the database.
- Server start: Vault is locked. Encryption key is not in memory. All vault API endpoints return
423 Locked. - Unlock: Admin enters master passphrase via the UI. System re-derives the key using the stored salt, verifies it against the verification ciphertext. If correct, key is held in memory. Vault endpoints become functional.
- Server restart: Key is lost. Vault returns to locked state.
- Lock (manual): Admin can lock the vault without restarting. Key is wiped from memory.
2.3 What's Encrypted vs. Plaintext
| Field | Storage | Reason |
|---|---|---|
name |
Plaintext | Searchable, displayed in list |
url |
Plaintext | Searchable, displayed in list |
category |
Plaintext | Filterable |
username |
Encrypted | Credential |
password |
Encrypted | Credential |
notes |
Encrypted | May contain secrets |
created_at |
Plaintext | Metadata |
updated_at |
Plaintext | Metadata |
created_by |
Plaintext | Audit |
2.4 Development Mode
In development, support a VAULT_PASSPHRASE environment variable to auto-unlock on startup. This avoids blocking the dev workflow. Never use this in production.
3. Database Schema
3.1 vault_config
Stores the salt and verification data. One row per company.
vault_config
id uuid PK
company_id uuid FK → company
salt bytea -- random salt for key derivation
verification bytea -- encrypted known string for passphrase verification
kdf_algorithm varchar(20) -- 'argon2id' or 'pbkdf2'
kdf_iterations integer -- iteration count / cost parameter
created_at timestamptz
updated_at timestamptz
3.2 vault_entry
Individual credential entries.
vault_entry
id uuid PK
company_id uuid FK → company
name varchar(255) -- plaintext, searchable
url varchar(500) -- plaintext, searchable
category varchar(100) -- plaintext, filterable
username_enc bytea -- AES-256-GCM encrypted
username_iv bytea -- nonce for username
password_enc bytea -- AES-256-GCM encrypted
password_iv bytea -- nonce for password
notes_enc bytea -- AES-256-GCM encrypted (nullable)
notes_iv bytea -- nonce for notes (nullable)
created_by uuid FK → user
updated_by uuid FK → user
created_at timestamptz
updated_at timestamptz
3.3 vault_access_log
Append-only audit trail for credential access.
vault_access_log
id uuid PK
company_id uuid FK → company
entry_id uuid FK → vault_entry
user_id uuid FK → user
action varchar(20) -- 'view', 'copy', 'create', 'update', 'delete'
field varchar(20) -- 'password', 'username', 'notes' (for view/copy)
ip_address varchar(45)
created_at timestamptz
4. API Endpoints
All vault endpoints are prefixed with /v1/vault.
4.1 Vault Status & Management
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /vault/status |
Any authenticated | Returns { locked: bool, initialized: bool } |
| POST | /vault/initialize |
vault.admin |
Set master passphrase (first time only) |
| POST | /vault/unlock |
vault.admin |
Unlock vault with passphrase |
| POST | /vault/lock |
vault.admin |
Lock vault (wipe key from memory) |
| POST | /vault/change-passphrase |
vault.admin |
Change master passphrase (re-encrypts all entries) |
4.2 Entries
All return 423 Locked when vault is locked.
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /vault/entries |
vault.view |
List entries (paginated, name/url/category only) |
| POST | /vault/entries |
vault.edit |
Create entry |
| GET | /vault/entries/:id |
vault.view |
Get entry (decrypts username, password, notes) |
| PATCH | /vault/entries/:id |
vault.edit |
Update entry |
| DELETE | /vault/entries/:id |
vault.edit |
Delete entry |
| POST | /vault/entries/:id/copy |
vault.view |
Log that user copied a credential |
4.3 Audit
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /vault/audit |
vault.admin |
Access log (paginated, filterable by user/entry) |
4.4 Locked Response
When vault is locked, all entry/audit endpoints return:
{
"error": {
"message": "Vault is locked",
"statusCode": 423
}
}
5. Permissions
New permission domain: vault
| Slug | Description |
|---|---|
vault.view |
View entry list, decrypt and view credentials |
vault.edit |
Create, update, delete entries |
vault.admin |
Initialize, unlock, lock, change passphrase, view audit log |
Permission inheritance: vault.admin → vault.edit → vault.view
Default role assignments:
- Admin:
vault.admin - Manager:
vault.edit - All others: No vault access by default
6. Frontend
6.1 Sidebar
New "Vault" item in the Admin section (visible only with vault.view). Shows a lock icon when locked.
6.2 Pages
Vault List Page
- Search by name/URL
- Filter by category
- Table columns: Name, URL, Category, Updated
- Click row to view details
- "New Entry" button (requires
vault.edit)
Vault Entry Detail
- Shows name, URL, category
- Username field with copy button (masked by default, click to reveal)
- Password field with copy button (masked by default, click to reveal)
- Notes (hidden by default, click to reveal)
- Edit/Delete buttons (requires
vault.edit) - Every reveal/copy action is logged
Vault Locked Page
- Shown when navigating to vault and it's locked
- Passphrase input + "Unlock" button (requires
vault.admin) - If not initialized: "Set Master Passphrase" flow with confirmation input
Vault Setup (First Time)
- Enter master passphrase (min 16 characters)
- Confirm passphrase
- Warning: "This passphrase cannot be recovered. If lost, all vault entries are permanently inaccessible."
6.3 UI Behaviors
- Password fields use
type="password"with a toggle eye icon - Copy-to-clipboard auto-clears after 30 seconds
- Toast notification on copy: "Password copied — clipboard will clear in 30s"
- Reveal auto-hides after 30 seconds
- No browser autocomplete on vault forms (
autocomplete="off")
7. Operational Considerations
7.1 Passphrase Recovery
There is no recovery mechanism by design. If the master passphrase is lost, all encrypted entries are permanently inaccessible. The vault_config and vault_entry rows can be deleted to start fresh, but the old data is gone.
Document this clearly in the UI and in the setup flow.
7.2 Backup
Database backups include encrypted vault data. Without the master passphrase, the encrypted fields are useless. This is the correct behavior — a stolen backup should not expose credentials.
7.3 Passphrase Change
Changing the master passphrase requires:
- Verify old passphrase
- Derive new key from new passphrase + new salt
- Decrypt every entry with old key, re-encrypt with new key (transaction)
- Update vault_config with new salt + verification
- Replace in-memory key
This is an expensive operation (scales with entry count) but should be rare.
7.4 Server Restart Behavior
After restart:
- Vault status: locked
- All vault API calls: 423
- Everything else in LunarFront: fully functional
- Admin unlocks when ready — no urgency, no data loss
7.5 Multiple Servers
If running multiple backend instances behind a load balancer, each instance needs to be unlocked independently (since the key is in-process memory). For multi-instance deployments, consider a shared unlock via Redis (store the derived key in Valkey with a TTL matching the server session). This is an optional enhancement — most on-prem music stores run a single instance.
8. Categories (Defaults)
Pre-populated categories that stores can customize:
- Suppliers
- Distributors
- Payment Processing
- Shipping & Freight
- Insurance
- Licensing (ASCAP, BMI, SESAC)
- Banking
- Software & Services
- Utilities
- Social Media
- Website & Hosting
- Other
9. Implementation Order
- Database schema + migration
- Encryption service (encrypt, decrypt, key derivation, verification)
- Vault status/initialize/unlock/lock endpoints
- Entry CRUD endpoints
- Audit logging
- Add
vault.*permissions to RBAC seed - Frontend: locked page + unlock flow
- Frontend: entry list + detail + create/edit
- Frontend: audit log page
- Tests: encryption unit tests, API integration tests