Project-specific instructions for KeyDesk. Read these before making changes.
Self-hosted corporate credential manager. Employees use company accounts (LinkedIn, Gmail, Reddit, AWS, Stripe, etc.) without seeing passwords via Chrome extension. One-click offboarding revokes all access.
Phase 1 priority: traffic and adoption, not revenue. No paid tiers, no license keys.
src/cmd/keydesk.go ← daemon entry (flags, PID, signals, SIGHUP reload)
src/app/app.go ← Initialize / Run / Stop / DeInitialize lifecycle
src/app/api/server.go ← chi router, middleware, static file serving
src/app/api/handlers/ ← HTTP handlers (one file per resource)
src/app/database/ ← SQLite operations (one file per table)
src/app/vault/vault.go ← AES-256-GCM encryption
src/frontend/ ← TypeScript + esbuild
src/install/public/ ← HTML + CSS + compiled JS
extension/ ← Chrome Manifest V3 extension
packaging/ ← systemd service + postinst
- Go 1.25 + chi v5 router
- SQLite via
mattn/go-sqlite3(foreign keys ON) - gofastogt for HTTP response wrappers (
NewOkResponse,NewErrorResponse,ErrorJson) - logrus for logging
- JWT via
golang-jwt/jwt/v5(HS256) - bcrypt for admin passwords (
golang.org/x/crypto/bcrypt) - AES-256-GCM for credential vault
- TypeScript + esbuild for frontend (NO React, NO npm framework)
- Custom CSS (NO Tailwind, NO Bootstrap)
- Chrome Manifest V3 for extension
- nfpm + systemd for packaging (NOT Docker)
// WRONG
db.CreateAccount(name, ..., req.LoginPassword, ...)
// RIGHT
encrypted, err := h.vault.Encrypt(req.LoginPassword)
if err != nil { ... }
db.CreateAccount(name, ..., encrypted, ...)What MUST be encrypted via vault.Encrypt:
accounts.login_passwordaccounts.totp_secretcredentials.key_valuecredentials.secret_value
What is NOT encrypted (plain in SQLite):
- Names, emails, URLs, types, departments, descriptions
- Audit log entries
- Admin password hashes (bcrypt, separate from vault)
Every Create*, Update*, Delete*, Rotate*, Reveal*, Offboard* handler must call:
h.db.LogAudit(action, entityType, entityID, personID, performedBy, details)Examples in src/app/api/handlers/people.go, src/app/api/handlers/accounts.go.
The whole point: employees never see passwords through the extension. So when offboarding:
- Revoke assignments (set
revoked_at = NOW) - Mark person status = "offboarded"
- Optionally reassign service ownership
- DO NOT rotate passwords — other users still need them and the offboarded employee never knew them
Password rotation is a separate manual action (button on Account detail page).
// Success
respondJSON(w, http.StatusOK, data)
// → {"data": {...}}
// Error
respondError(w, http.StatusBadRequest, "message")
// → {"error": {"code": 400, "message": "..."}}Helpers in src/app/api/handlers/utils.go wrap gofastogt.NewOkResponse / gofastogt.NewErrorResponse / gofastogt.ErrorJson.
Frontend unwraps via apiCall in src/frontend/core/api.ts.
- Admin JWT (web UI) —
auth.gomiddleware, claimadmin_id, 24h expiry - Extension JWT (employee Chrome extension) —
ext.gomiddleware, claimperson_id+type: "extension", 8h expiry
Never mix tokens — extension routes check claims["type"] == "extension".
In every ext.go handler that returns or uses credentials:
assignments, _ := h.db.GetActiveAssignmentsByPerson(personID)
hasAccess := false
for _, a := range assignments {
if a.AccountID == accountID { hasAccess = true; break }
}
if !hasAccess {
respondError(w, http.StatusForbidden, "Access denied")
return
}Never trust the account_id from the request alone.
- All IDs are
uuid.New().String() - Timestamps stored as
TEXTin RFC3339 UTC (nowUTC()helper) - Foreign keys with
ON DELETE CASCADEwhere deletion should propagate,ON DELETE SET NULLfor soft links (e.g.,services.owner_id) - Always paginate or filter — never
SELECT * FROM tablewithout limit on user-facing endpoints
- Each page = one TypeScript file in
src/frontend/+ one HTML insrc/install/public/ - Shared logic in
src/frontend/core/(api.ts,storage.ts,sidebar.ts,utils.ts,types.ts) - Always
esc()user-supplied strings before injecting into innerHTML - Use
esbuildconfig inbuild.js— add new entry point when adding a new page - TypeScript strict mode, no
anyexcept for API responses (typefrom server is dynamic)
- Manifest V3 (service worker, not background page)
- Background service worker handles all API calls (centralized auth)
- Content script only talks to background via
chrome.runtime.sendMessage - Never store passwords in
chrome.storage— fetch from server, fill form, discard - Auto-detect: content script polls
/api/ext/matchon page load, shows banner if URL matches an assigned account
make build # local build
make build-linux-amd64 # cross-compile
make package-deb # create .deb
make package-rpm # create .rpm
make package-all # both
make frontend-build # rebuild frontend only
make frontend-watch # watch mode for development
make frontend-check # TypeScript type check
make fmt # gofmt
make vet # go vet
make test # go testVersion is generated into src/app/version/version.go by scripts/generate_version.sh. Never edit version.go directly — it's regenerated on every build.
The extension hides passwords from employees ONLY when:
- Corporate laptop with managed Chrome (Group Policy / MDM)
- DevTools disabled via
DeveloperToolsAvailability=2policy - Extension force-installed (employee can't uninstall)
- Password manager save disabled via Chrome policy
On unmanaged personal devices, a determined user can still extract the password via DevTools. This is a known limitation. Document it — don't pretend otherwise.
- Don't add Docker. Stack is
.deb+ systemd, like nginx. - Don't add React/Tailwind. Plain TypeScript + custom CSS.
- Don't add Google OAuth login. MVP uses local email/password (admin) and person ID (extension).
- Don't add background expiry checker / Slack alerts. Not in MVP. Dashboard query is enough.
- Don't add multi-admin RBAC. Single admin tier in MVP.
- Don't replace
gofastogtresponse wrapping. All FastoCloud Go projects use it. - Don't commit
src/go.sum. It's in.gitignore. - Don't use Co-Authored-By in git commits. User's global rule.
- Don't push or commit unless explicitly asked.
If you add a new entity (e.g., "tags", "groups"):
- Create migration in src/app/database/database.go
migrate()function - Create CRUD file
src/app/database/<resource>.go - Create handler file
src/app/api/handlers/<resource>.go - Wire routes in src/app/api/server.go
Routes() - Audit log every write action
- Add TypeScript page in
src/frontend/<resource>.ts - Add HTML page in
src/install/public/<resource>.html - Add esbuild entry in src/frontend/build.js
- Add navigation link in src/frontend/core/sidebar.ts
- Add type in src/frontend/core/types.ts
- Schema: 7 tables (admins, people, accounts, services, credentials, assignments, audit_log)
- 9 web pages (login, dashboard, people list/detail, accounts list/detail, services list/detail, settings)
- API base:
/api/*(admin) and/api/ext/*(extension) - Default config:
/etc/keydesk.conf, port6690, data in/var/lib/keydesk/ - Repository: https://github.com/fastogt/keydesk
- License: Apache 2.0