Desktop Apps
The Windows and macOS admin apps — what they are, how to install them, how they are built, and the guarded database console they carry
There are two desktop applications for running the business away from a browser tab:
| App | Workspace | Technology | Installer |
|---|---|---|---|
| Windows 11 | apps/windows-admin | Electron 44 + TypeScript | JoseMadridSalsaAdmin-Setup-<version>.exe (NSIS) |
| macOS (Apple silicon) | apps/macos-admin | SwiftUI + WKWebView | JoseMadridSalsaAdmin-<version>.dmg |
Both open on the desktop shell at https://www.josemadrid.net/admin-desktop — the same
database, drawn as a dense keyboard-driven window instead of the web panel's page chrome — and
both can reach every other page of the admin panel from there.
That split is deliberate. The admin panel is 163 pages and 352 API routes, and rebuilding it natively per platform would mean writing every future admin feature three times. Because the apps load the real thing, everything the admin panel can do, they can do — orders, products, inventory, customers, financials, fundraisers, events and shows, email marketing, social publishing, media and documents, analytics, settings — on the day the feature ships, with no desktop release required. The shell sits on top of that as the fast path for the work done every day, and hands off to the web page for everything else.
What the desktop apps add over a browser tab is the native layer: their own menus and keyboard shortcuts, a save dialog for every export, real printing, a persistent signed-in session, an offline screen that explains itself, and — on Windows — automatic updates.
The desktop shell
/admin-desktop is a single full-window view: a 44px title bar the window itself drags by, a
232px sidebar grouping every section under Operations, Programs, Money,
Marketing and System, one content pane, a detail inspector on the right, and a status
bar of keyboard hints. It follows the system's light or dark appearance, and the appearance
button in the title bar cycles light → dark → follow-system for that machine.
Both windows are frameless so the shell's own title bar runs to the top edge: macOS floats its traffic lights over the left of it, Windows paints its minimise/maximise/close buttons over the right.
All 23 sections read live data in the shell:
| Section | What it shows |
|---|---|
| Dashboard | Today's orders and revenue, live fundraisers, jars on hand, SKUs at or below reorder, the next shows |
| Orders | The latest 250 orders with jar counts, channel, status and totals |
| Products | Catalogue with retail, unit cost and the margin between them |
| Inventory | On hand, reserved, available and reorder point, flagged against the reorder threshold |
| Customers | Ranked by lifetime value, with order counts and source |
| Purchase Orders | Inbound supply by supplier, with goods, freight and what is still outstanding |
| Invoices | Accounts receivable, with what is open and what has run past its due date |
| Fundraisers | Participants across every campaign, with sales and the share going to the group |
| Events & Shows | A month calendar plus every show on record, with booth fees and takings |
| Wholesale | Trade accounts with their discount, minimum, terms and approval state |
| Financials | The general ledger — debit, credit, category, source and QuickBooks export state |
| Email Marketing | Campaigns with audience, recipients, open and click rates |
| Social | Scheduled and published posts with reach, engagement and clicks |
| Content & Blog | Blog posts with their URL, status and SEO character budgets |
| Lead Generation | The prospecting pipeline with the Google rating and review count behind each lead |
| Reviews | Customer feedback awaiting moderation, with the average rating |
| Analytics | Twelve months of revenue against the year before, channel mix, top products, retention by first-order quarter |
| Media & Docs | The library by kind and size, flagging images with no alt text |
| Messages | One inbox merging support conversations, the contact form and live chat |
| Users & Roles | Staff accounts with role, two-factor state and last sign-in |
| Audit Logs | Who did what, most recent first |
| Settings | Live store, payment, shipping, QuickBooks and integration state (read only) |
| Database Console | Row counts for the core tables |
Some columns differ from the design they were drawn to, because the schema does not carry the
field: Products shows unit cost rather than a wholesale price, Customers shows acquisition source
rather than a mailing address, Fundraisers counts orders rather than jars, Wholesale shows the
account's own discount and minimum rather than a price tier, Lead Generation shows the stored
Google rating rather than a computed score, and the Database Console reports row counts only —
on-disk size and index counts need pg_catalog queries, and this codebase goes through Prisma
rather than raw SQL.
Content & Blog lists the blog. Pages, banners, FAQs and redirects are edited through
/admin/content but are not one queryable list, so they are reachable from the command palette
rather than drawn as a table.
What each section needs to open
The shell is a second door onto the same data, so it refuses exactly what the web panel refuses.
Each section is gated on the permission its /admin page checks, and the sidebar is built from
the sections the signed-in account may actually load — nothing is offered that would then be
denied. Requesting a gated section directly returns 403 from
/api/admin/desktop/[section].
| Permission | Sections it opens |
|---|---|
orders:read | Orders, Fundraisers |
products:read | Products, Inventory |
inventory:read | Purchase Orders |
financials:read | Invoices, Financials |
users:read | Customers, Wholesale, Users & Roles, Audit Logs |
content:read | Email Marketing, Content & Blog |
content:write | Reviews, Media & Docs |
social_media:compose | Social |
messaging:read | Messages |
analytics:read | Analytics |
settings:read | Settings |
developer:database | Database Console |
| — | Dashboard, Events & Shows, Lead Generation (staff, matching their pages) |
Working in the shell
| Key | Does |
|---|---|
J / K or ↑ / ↓ | Move the selection |
↵ | Open the selected row in the web admin |
/ | Focus the filter box |
F | Cycle the filter chips |
I | Show or hide the inspector |
Ctrl/⌘ K | Command palette — jump to a section or a row, or run a command |
| letters or digits | Type-ahead: jump to the first row starting with what you typed |
Selecting a row fills the inspector on the right, and the buttons at its foot are the jobs you
can do with that row — each opens the admin page that performs it, on the key its label shows.
A label ending in … is the promise that a page opens rather than something happening in place.
Those keys are checked against the native menus: a combination either window's menu bar already
owns (⌘R reload, ⌘P print, ⌘A, ⌘V, ⌘M and the rest) is never advertised, because the
menu handles it before the page ever sees the key.
The shell is read-only. Editing still happens on the admin pages it opens — the header buttons,
the inspector actions and ↵ are how you get there.
Opening a section directly
/admin-desktop?section=<id> opens the window at a section — ?section=invoices,
?section=ledger, and so on, using the ids in
apps/storefront/lib/admin-desktop/sections.ts. Both menu bars navigate this way, so choosing
Orders from the menu stays in the shell instead of leaving it for the web page, and the
shell keeps the query in step as you move so a reload comes back to the same section. The one
exception is Developer Console, which has no view in the shell and opens the web page.
A section id that is unknown, or one the account may not open, falls back to the first section it can see rather than erroring — a menu item or an old bookmark should not be a dead end.
Installing
Windows 11
- Download
JoseMadridSalsaAdmin-Setup-<version>.exefrom thedesktop-latestrelease. - Run it. The build is not code-signed yet, so SmartScreen shows "Windows protected your PC" — choose More info → Run anyway. This is expected once.
- The installer offers a folder choice and creates desktop and Start menu shortcuts.
Afterwards the app updates itself: it checks on launch and every six hours, downloads in the background, and offers to restart when an update is ready. Help → Check for Updates… forces a check.
macOS
- Download
JoseMadridSalsaAdmin-<version>.dmgfrom the same release and drag the app to Applications. - The build carries an ad-hoc signature rather than a Developer ID, so the first launch needs right-click → Open → Open. Afterwards it opens normally.
The macOS app does not update itself. Help → Check for Updates… opens the releases page.
Using them
Sign in once with the same account used on the web. The session persists across restarts.
Keyboard shortcuts
Both apps share one section list, so the shortcuts match:
| Shortcut | Section | Shortcut | Section | |
|---|---|---|---|---|
Ctrl/⌘ 1 | Dashboard | Ctrl/⌘ 6 | Financials | |
Ctrl/⌘ 2 | Orders | Ctrl/⌘ 7 | Fundraisers | |
Ctrl/⌘ 3 | Products | Ctrl/⌘ 8 | Events & Shows | |
Ctrl/⌘ 4 | Inventory | Ctrl/⌘ 9 | Email Marketing | |
Ctrl/⌘ 5 | Customers | Ctrl/⌘ P | Print the current page |
The rest of the sections — Purchase Orders, Invoices, Wholesale, Social, Content & Blog, Lead Generation, Reviews, Analytics, Media & Documents, Messages, Users & Roles, Audit Logs, Settings, Developer Console, Database Console — are in the Go menu, grouped the same way the shell's sidebar groups them.
Downloads and printing
CSV exports, invoices, packing slips and shipping labels open the operating system's save dialog. On Windows a finished download raises a notification; clicking it reveals the file in Explorer. On macOS the file is revealed in Finder. Print… prints whatever the window is showing.
Which server the app talks to
File → Server Settings… on Windows, Settings on macOS. HTTPS is required except for a
localhost development server, which is how you point either app at npm run dev. A bare origin
is treated as /admin-desktop; any other path on the same origin is accepted as typed, so
https://www.josemadrid.net/admin still opens the web panel directly.
Links and sign-in
Links to other sites — Stripe, QuickBooks, Vercel, a customer's website — open in the default browser rather than inside the app. Identity providers are the exception: Google, GitHub, Facebook and Apple sign-in redirects stay in the app window, because an OAuth round trip that finishes in a browser sets the session cookie there and leaves the app on the sign-in page.
The database console
Both apps reach /admin/developer/database, a guarded SQL console restricted to the
developer:database permission (the DEVELOPER role). It exists for direct data work that has
no admin screen. It is not a replacement for the admin panel: normal editing should go through
the pages, which enforce validation, permissions and the transactional side effects that make
inventory, payments and email behave correctly.
The console never exposes database credentials to the desktop app. Statements are posted to
POST /api/developer/admin/sql and run on the server. The guards are:
- One statement per request. Comments and string literals are stripped before parsing, so a
'; DROP TABLE orders; --'inside a value cannot smuggle a second statement through. - Reads run read-only. A
SELECTexecutes inside aREAD ONLYtransaction with a 15-second statement timeout and a row cap (200 by default, 1000 maximum). Results export to CSV. - Writes are previewed before they commit. An
INSERT,UPDATEorDELETEis executed against real data to get a true affected-row count and then rolled back. The console reports what it would change — shouting if anUPDATEorDELETEhas noWHEREclause — and commits only when you confirm. The confirmation is signed against that exact statement and user and expires in five minutes, so it cannot be replayed against a different query. - Data-modifying CTEs count as writes.
WITH x AS (DELETE ... RETURNING *) SELECT * FROM xreads like aSELECTbut is treated as a write. - Schema changes are refused.
CREATE,ALTER,DROP,TRUNCATE,GRANT,REVOKE, transaction control andEXPLAIN ANALYZEare rejected by name. Schema changes belong in a Prisma migration, which keeps the migration history and the generated client honest. - Everything is audited. Reads, previews, commits, refusals and errors all write an
AuditLogrow (developer.sql.*) with the statement, the affected-row count and the duration. They are visible in Audit Logs.
Building
Windows installer
npm run dist:win --workspace=@jose-madrid/windows-adminThe installer and the latest.yml update feed land in apps/windows-admin/release/. This
cross-compiles from macOS and Linux as well as Windows.
Day-to-day work on the shell:
npm run start --workspace=@jose-madrid/windows-admin # run it
npm run test --workspace=@jose-madrid/windows-admin # endpoint and navigation rules
npm run type-check --workspace=@jose-madrid/windows-admin
npm run lint --workspace=@jose-madrid/windows-adminmacOS app
cd apps/macos-admin && ./build-app.shThe script first compiles and runs the endpoint checks in Tests/main.swift — the rules for
where the app may point and what it hands to the browser — then builds the bundle into dist/.
A full Xcode installation is required. The standalone Command Line Tools do not ship the
SwiftUI macro plugins that @State expands through, and the build fails with
plugin for module 'SwiftUIMacros' not found. The pure-logic files still compile without
Xcode, which is why the endpoint checks run first.
Releasing both
The Desktop Apps workflow (.github/workflows/desktop-release.yml) builds the Windows
installer on windows-latest and the macOS app on macos-15, then publishes both to the
rolling desktop-latest release. Run it from the Actions tab, or push a desktop-v* tag to
also keep a fixed copy under that tag.
The update feed is pinned to the desktop-latest tag by name rather than to GitHub's "latest
release", because this repository also cuts platform releases (v1.0.0, v2.0.0) that carry no
installer — whichever of those was newest would otherwise become the feed and every update check
would come back empty.
Code signing
Neither app is signed yet, which is the source of the SmartScreen and right-click-to-open prompts above. Both builds are structured for it:
- Windows — set the
CSC_LINKandCSC_KEY_PASSWORDsecrets on the workflow. No change toelectron-builder.ymlis needed. - macOS — replace the ad-hoc
codesign --sign -at the end ofbuild-app.shwith a Developer ID identity, then notarize withxcrun notarytool.
How is this guide?
Last updated on