LedgerMap · design options · 17/10/2026

Lock LedgerMap to one PC, with no user accounts

Three ways to make the desktop app run only on PCs that you approve. Each option has a diagram, its strong and weak points, and the work it needs. The recommendation and a rollout plan come at the end.

1Locked to one PCA copy of the app on another PC must not work.
2No loginOn an approved PC, the app simply opens.
3You approve each PCOnly you, as admin, give a PC permission to run the app.

First, the honest truth about hardware locks

A check inside the app is weak

The user controls their PC. A skilled person can edit the .exe to skip a check, or fake a hardware ID. No hardware lock that runs only on the PC is safe from this.

A check on our server is strong

LedgerMap already needs our server. It reads PDFs, matches ledgers and keeps the saved work. If the server serves only approved PCs, an edited app gets nothing. The server is our real lock.

Option 1 · Recommended

Device key with server approval

The PC makes a secret key inside its security chip. The key can never leave that PC. You approve the key once. After that, the PC proves who it is every time the app starts.

What the user sees on a new PC

 Ledger Map

This PC is not approved yet

Send this code to your LedgerMap contact.

4827 1936
Your name or company
Asmark Engineers
Copy codeSend request
Waiting for approval. The app opens by itself.

What you do

$ ssh ledgermap-server ledger-map-admin devices CODE NAME ASKED STATUS 4827 1936 Asmark Engineers 17/10 11:42 pending $ ledger-map-admin approve 48271936 \ --customer "Asmark Engineers" Approved. The app on that PC opens now. $ ledger-map-admin revoke 48271936 Revoked. That PC stops at its next request.

Later, the same list can be an admin screen in the desktop app. A faster start is also possible: you make a one-time 8-digit setup code for a customer. The first PC that enters it is approved at once, and the code then stops working.

Strong points

  • A copy on another PC cannot work. The key cannot leave the chip.
  • No login. The app signs in by itself at each start.
  • You approve each PC, and you can revoke one at any time. The change works at once.
  • An edited .exe does not help. The server refuses any PC it does not know.
  • Ready for the paid plan. Add "paid until" and "number of PCs" to the customer.

Weak points

  • It needs the internet. LedgerMap needs it already, so nothing new here.
  • Some old PCs have no TPM chip, or it is turned off. Those PCs use a key locked with Windows DPAPI instead. A full disk clone could copy that key.
  • A new Windows install or a new motherboard means a new key, so you approve that PC again.
  • It is the most work of the three to build.
Lock strength StrongWork about 3 PRsRunning cost noneWorks offline no
Option 2

Licence file locked to a hardware ID

The app reads the PC's hardware (motherboard ID, disk serial, Windows ID) and shows a Machine ID. You sign a licence file for that ID. The app checks the file each time it starts, without the internet.

Strong points

  • Simple to understand. No server work to check a licence.
  • Works offline. This only matters for apps that do not need a server.
  • Uses the signing method we already use for app updates (Ed25519).

Weak points

  • Only the app checks the lock. An edited app or a faked hardware ID gets past it.
  • It does not protect the server. We would still need Option 1 or passwords for that.
  • You cannot revoke a file. It works until it expires.
  • A new disk or motherboard changes the ID, so the user needs a new file.
  • Sending files back and forth by hand is slow for users.
Lock strength WeakWork about 2 PRsRunning cost noneWorks offline yes
Option 3

Licensing service (Keygen, Cryptlex, LicenseSpring)

A company that sells licence systems does the PC locking. It gives you a dashboard for customers, PCs and seat limits, and later for payments. Our server must still check that service's tokens.

Strong points

  • Ready-made: PC limits, offline grace days, expiry and a dashboard.
  • Connects to payments (for example Stripe) when LedgerMap becomes paid.
  • Their team keeps the licence system working, not ours.

Weak points

  • Two systems to keep in step: the service and our server, which still needs its own check.
  • A monthly cost that grows with the number of customers.
  • Customer names and PC details go to another company.
  • If the service is down, nobody can start the app.
  • Most of these services use a PC fingerprint, not a chip key. So copying is easier than in Option 1.
Lock strength Medium – strongWork about 2 PRs + vendor setupRunning cost monthly feeWorks offline with grace days

Side by side

1 · Device key + approval2 · Licence file3 · Licensing service
Copy to another PC is stoppedYes. The key cannot leave the chip.Partly. Only the app checks.Mostly. Fingerprint only.
No login for the userYesYesA licence key once
You approve each PCYes, with one command or a setup codeYes, by signing a fileYes, in their dashboard
Revoke a PC at onceYesNo. It works until it expires.Yes
Protects our serverYesNoOnly if our server checks tokens
An edited .exe gets inNoYesNo, when our server checks
Data stays with usYesYesNo
Running costNoneNoneMonthly fee
Ready for paid plansYes. Add expiry and PC count.Expiry onlyYes, with payments
Work to buildMost (about 3 PRs)Least (about 2 PRs)Medium + vendor setup

How hard is it to get around the lock?

1 · Device key + approval
Strong
3 · Licensing service
Good
2 · Licence file
Weak

Recommendation: Option 1

LedgerMap already depends on our server, so the server is where the lock belongs. Option 1 meets all three requirements. It costs nothing to run, keeps customer data with us, and later becomes the paid plan without a rebuild. Option 3 is worth a look later, only if you want a ready-made payment and licence dashboard.

Rollout plan

Server: customers and devicesA customer is an account with no password. Each customer has one or more approved PCs (public keys). New API routes: ask for approval, get a challenge, sign in with a signature. Admin commands: devices, approve, revoke, setup-code. The server records each request, approval and revocation in usage tracking.
Desktop app: the key and the approval screenThe Go side makes the key in the TPM chip, or with DPAPI if there is no chip, and signs in by itself. The login page becomes the approval screen shown above. The browser never sees the key; the session cookie stays in Go, as it does today.
Move everyone acrossExisting accounts become customers, so their parties and imports stay. Each PC asks once, and you add it to the right customer. Older apps keep password login until a required update moves them over. Then password login closes.

Build log

Updated as the work goes on. Nothing merges into main until you approve it.

#154 · Device access → mainMerged into mainMerged with your approval. Not deployed; app 0.4.0 not published. PR #154
Part 2 · ServerMerged into device-lockDevices, challenges, setup codes, admin commands, rate limits, tracking, PASSWORD_LOGIN switch. PR #140. All server checks pass, including PostgreSQL integration tests (full flow, setup code used once under 4 parallel requests, revoke ends sessions, limits, cleanup). OpenQodex: passed, no findings.
Part 3 · Desktop app and approval screenMerged into device-lockTPM key through Windows CNG with DPAPI fallback, automatic sign-in in the Go side, approval screen, profile and menu changes. PR #144. Backend tests, Windows build, 552 frontend tests pass. OpenQodex: passed after 6 fix rounds.
Part 4 · RolloutMerged into device-lockRollout steps, clearer message when password login is closed, version 0.4.0, fixes for two OpenQodex findings. PR #145 (replaces #141).
Independent review (Fable)Done: all findings fixedRound 1: no blocker, 1 major and 7 minor findings. Round 2: all 8 fixes confirmed, no new defects from them, and 2 new minor findings, both fixed.
#158 · Admin panel → mainMerged into mainWeb panel at app.ledgermap.in/admin/: Requests, PCs, Customers with setup codes, Activity. Built as #156 (server) and #157 (web page, deploy, nginx). Not deployed. PR #158
  1. You chose Option 1 with 8-digit numeric codes.
  2. Wrote the shared contract (wire format, routes, admin commands). OpenQodex review: passed, no findings. Opened PR #138.
  3. Started two sub-agents in their own worktrees: one for the server, one for the desktop app and the screen.
  4. Server part merged (PR #140): server/device.go, server/device_admin.go, schema changes, admin commands devices / approve / revoke / setup-code / customers.
  5. Contract gap: the contract did not say what a malformed public key returns. The server answers 400 device_key_invalid. The desktop side must handle it; this is checked in the review.
  6. Small gaps the server agent decided: a revoked pending request is never cleaned up; --customer refuses a name that already exists; requests that keep racing can get 409 after 5 tries. All documented in docs/device-access.md.
  7. OpenQodex's golangci-lint scanner failed to run (exit 2). The reviewer itself ran and passed the change.
  8. Wrote part 4 (PR #141). Rollout order: deploy server → give existing accounts setup codes, or approve with --user <email> to keep their data → publish 0.4.0 as a required update → set PASSWORD_LOGIN=off.
  9. OpenQodex found a race: two admin commands at the same moment could create two customers with one name. Fixed with a database lock.
  10. OpenQodex flagged that version 0.4.0 must not ship without the desktop code. So PR #141 merges only after part 3.
  11. The first PostgreSQL test run used wrong credentials. A rerun with the local development database settings passed.
  12. Desktop part merged (PR #144). OpenQodex needed 6 fix rounds. Fixed issues included a Windows memory-safety gap in the CNG call wrapper, an ended password session being replaced by the PC's account, and focus moving to a disabled field.
  13. The TPM and DPAPI code could not run here (Linux). It builds and vets for Windows, but it needs a test on a real Windows PC with a TPM, one without, and a restart.
  14. Roaming Windows profiles: the DPAPI fallback key lives in %APPDATA%. On another PC it cannot be decrypted, so the app shows an error instead of making a new key. Rare in small offices; documented.
  15. OpenQodex found that a large PDF upload (over 1 MiB) that got a 401 skipped session renewal, so uploads kept failing. Fixed: the session renews and the row's Retry works; a blocked PC sees the approval screen.
  16. Updating PR #141 after its rebase would need a force push, which needs your approval. Opened #145 instead and closed #141.
  17. All 4 parts merged into device-lock. main moved ahead 3 commits; the branch rebases on it with no conflicts. On the rebased code all checks pass: server with PostgreSQL, backend with Windows build, 568 frontend tests, root tooling.
  18. Started the independent review with a Claude Fable 5.1 sub-agent.
  19. Fable round 1 found no blocker and said the auth design is sound. It confirmed the signature check, single-use challenges, setup-code locking, revoke, client IP handling behind Cloudflare, and that tracking never stores secrets.
  20. Major (fixed): 3 office PCs waiting behind one internet address used up the per-address status limit, so an approval could show up minutes late. Status checks now count per PC key, with a high per-address cap. The screen also says when the server slows its checks.
  21. Minor (fixed): Windows key paths. Key files moved out of the roaming profile to %LOCALAPPDATA%. A PC that never used the TPM falls back to DPAPI on any TPM error. A PC that did use it never switches silently, so its approval is never lost by accident.
  22. Minor (fixed): the schema change locked the users table at every server start; it now runs only once. Device sign-ins got their own rate limit. Browsers hide the password form when password login is closed. The docs quote ssh commands correctly for names like "Ramesh & Co".
  23. After the fixes, all checks pass: server with PostgreSQL, backend with Windows build, 570 frontend tests. OpenQodex on the whole branch against main: passed, no findings. Opened PR #151; closed #138.
  24. Started Fable round 2 to check the fixes.
  25. Fable round 2 confirmed all 8 fixes and found no new defects from them. It checked every Windows key path: no path silently replaces an approved key or leaks a handle.
  26. Minor (fixed): after a 429 the screen still checked every 5 seconds although its note said otherwise. It now checks once a minute. The Windows key folder now comes from Windows' known-folder API, so a launcher with a cleared environment cannot move it.
  27. main moved on again. Its new Settings pop-up (#146) moved the profile page, so "This PC", the hidden email field and the hidden Password section were moved into the new profile-settings.tsx and settings-dialog.tsx.
  28. Final checks on the current main: server with PostgreSQL, backend with the Windows build, 578 frontend tests, root tooling (37). All pass. OpenQodex: passed, no findings. Opened PR #154; closed #151.
  29. Still untested: the TPM and DPAPI code on a real Windows PC, an end-to-end run of the native app, and the 0.3.0 app against the new server. Do this before publishing 0.4.0.
  30. You asked for an online admin panel instead of SSH commands. Your choices: password-only sign-in, admin@ledgermap.in, customers and setup codes, usage activity, then merge #154 and the panel PR with no deploy.
  31. The project rule said app.ledgermap.in serves only the API. Your request overrides it; the rule changes to allow exactly this panel at /admin/.
  32. Risk to note: with password-only sign-in, the admin password is the only thing that guards PC approvals. Use a long, unique password. An authenticator code can be added later.
  33. Wrote the panel contract (docs/admin-panel.md). OpenQodex: passed. Started two sub-agents.
  34. Admin panel part 1 (server API) merged into admin-panel (PR #156): /api/admin/* routes, admin login that works even when password login is closed, shared logic with the SSH commands, a create-admin that asks for the password on the terminal, and PostgreSQL tests. OpenQodex: passed.
  35. Found in #154 (fixed): party connections are tied to the session, so the app's 23-hour session renewal broke an open party connection early, and the user had to reconnect the folder. Now the server extends the PC's own live session instead of making a new one. Tests on both sides; OpenQodex on all of #154 against main: passed, no findings.
  36. Admin panel part 2 (web page and deployment) merged into admin-panel (PR #157). 596 tests. A test keeps the admin and desktop bundles apart. nginx -t passes. OpenQodex found one major issue (a failed logout looked like a sign-out); fixed.
  37. The strict Content-Security-Policy blocks the style tag that dialogs add at run time. The panel does the same job from its own stylesheet; the browser may log a harmless console warning.
  38. Checked #154 together with the latest main (all checks pass), then merged it (#154). Moved the panel onto the new main with no conflicts; all checks pass (612 frontend tests, PostgreSQL server tests, root tooling). OpenQodex: passed, no findings. Merged as #158. Nothing deployed.
  39. To go live, in this order: bun run server:deploy → run deploy/setup-server.sh on the server (nginx) → ssh -t ledgermap-server ledger-map-admin create-admin with admin@ledgermap.in. Then test the TPM on a real Windows PC before publishing app 0.4.0.

Questions you may have

Is an 8-digit number safe enough?

The request code (PC → you) is only a label for a pending request. The chip key holds the security. The server picks the number at random, no two pending requests share it, and the list shows the name and time beside it. So 8 digits are plenty.

The setup code (you → PC) is a secret. It has 100 million choices, works once, expires after 24 hours, and the server allows only a few wrong tries per address and about 100 wrong tries per hour in total. Against one code, that gives a guesser roughly a 1 in 40,000 chance. Bank OTPs use 6 digits with the same kind of limits.

The user gets a new PC or reinstalls Windows. What then?

The app makes a new key and shows a new code. You approve it for the same customer, and all their work is there. You revoke the old PC.

Can one customer use two PCs?

Yes, if you allow it. Each PC has its own key, and all of them belong to one customer. They share the same parties. When LedgerMap becomes paid, the number of PCs can be part of the plan.

What if a PC has no TPM chip?

All Windows 11 PCs have one. Most Windows 10 PCs from about 2016 on have one too, but it is sometimes turned off. Without it, the key is locked with Windows DPAPI for that PC. Copying the app folder still does not work, but a full disk image clone could carry the key. The server can mark these PCs so you can see them.

Can someone just copy the app folder or the installer?

Yes, but the copy is useless. The key lives in the chip (or in DPAPI), not in the app folder. On the new PC the app has no key and shows a new pending request.

Can an attacker spam the server with requests?

Requests are rate-limited per address, and a pending request expires after a few days. Pending PCs cannot use the API until you approve them.

What changes when we start to charge?

Add "paid until" and "PCs allowed" to each customer. At sign-in, the server checks them and tells the app when a plan has expired. There is no new lock to build.

Words used on this page

TPM chip
A small security chip on the motherboard. It can make keys that never leave it.
DPAPI
A Windows feature that encrypts data so only that PC can decrypt it. It is the fallback when there is no TPM.
Public / private key
A pair of keys. The private key signs and stays secret on the PC. The public key checks signatures and can be shared with the server.
Challenge
A random value that the server sends. The PC signs it, so an old signature cannot be used again.
Fingerprint / Machine ID
A value made from hardware serial numbers. It is easy to read, so it is also easy to fake.