Welcome to the Jose Madrid Salsa developer docs — explore features, APIs, and deployment guides.
Jose Madrid SalsaJMS Docs

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:

AppWorkspaceTechnologyInstaller
Windows 11apps/windows-adminElectron 44 + TypeScriptJoseMadridSalsaAdmin-Setup-<version>.exe (NSIS)
macOS (Apple silicon)apps/macos-adminSwiftUI + WKWebViewJoseMadridSalsaAdmin-<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:

SectionWhat it shows
DashboardToday's orders and revenue, live fundraisers, jars on hand, SKUs at or below reorder, the next shows
OrdersThe latest 250 orders with jar counts, channel, status and totals
ProductsCatalogue with retail, unit cost and the margin between them
InventoryOn hand, reserved, available and reorder point, flagged against the reorder threshold
CustomersRanked by lifetime value, with order counts and source
Purchase OrdersInbound supply by supplier, with goods, freight and what is still outstanding
InvoicesAccounts receivable, with what is open and what has run past its due date
FundraisersParticipants across every campaign, with sales and the share going to the group
Events & ShowsA month calendar plus every show on record, with booth fees and takings
WholesaleTrade accounts with their discount, minimum, terms and approval state
FinancialsThe general ledger — debit, credit, category, source and QuickBooks export state
Email MarketingCampaigns with audience, recipients, open and click rates
SocialScheduled and published posts with reach, engagement and clicks
Content & BlogBlog posts with their URL, status and SEO character budgets
Lead GenerationThe prospecting pipeline with the Google rating and review count behind each lead
ReviewsCustomer feedback awaiting moderation, with the average rating
AnalyticsTwelve months of revenue against the year before, channel mix, top products, retention by first-order quarter
Media & DocsThe library by kind and size, flagging images with no alt text
MessagesOne inbox merging support conversations, the contact form and live chat
Users & RolesStaff accounts with role, two-factor state and last sign-in
Audit LogsWho did what, most recent first
SettingsLive store, payment, shipping, QuickBooks and integration state (read only)
Database ConsoleRow 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].

PermissionSections it opens
orders:readOrders, Fundraisers
products:readProducts, Inventory
inventory:readPurchase Orders
financials:readInvoices, Financials
users:readCustomers, Wholesale, Users & Roles, Audit Logs
content:readEmail Marketing, Content & Blog
content:writeReviews, Media & Docs
social_media:composeSocial
messaging:readMessages
analytics:readAnalytics
settings:readSettings
developer:databaseDatabase Console
Dashboard, Events & Shows, Lead Generation (staff, matching their pages)

Working in the shell

KeyDoes
J / K or / Move the selection
Open the selected row in the web admin
/Focus the filter box
FCycle the filter chips
IShow or hide the inspector
Ctrl/⌘ KCommand palette — jump to a section or a row, or run a command
letters or digitsType-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

  1. Download JoseMadridSalsaAdmin-Setup-<version>.exe from the desktop-latest release.
  2. 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.
  3. 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

  1. Download JoseMadridSalsaAdmin-<version>.dmg from the same release and drag the app to Applications.
  2. 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:

ShortcutSectionShortcutSection
Ctrl/⌘ 1DashboardCtrl/⌘ 6Financials
Ctrl/⌘ 2OrdersCtrl/⌘ 7Fundraisers
Ctrl/⌘ 3ProductsCtrl/⌘ 8Events & Shows
Ctrl/⌘ 4InventoryCtrl/⌘ 9Email Marketing
Ctrl/⌘ 5CustomersCtrl/⌘ PPrint 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 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 SELECT executes inside a READ ONLY transaction 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, UPDATE or DELETE is executed against real data to get a true affected-row count and then rolled back. The console reports what it would change — shouting if an UPDATE or DELETE has no WHERE clause — 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 x reads like a SELECT but is treated as a write.
  • Schema changes are refused. CREATE, ALTER, DROP, TRUNCATE, GRANT, REVOKE, transaction control and EXPLAIN ANALYZE are 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 AuditLog row (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-admin

The 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-admin

macOS app

cd apps/macos-admin && ./build-app.sh

The 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_LINK and CSC_KEY_PASSWORD secrets on the workflow. No change to electron-builder.yml is needed.
  • macOS — replace the ad-hoc codesign --sign - at the end of build-app.sh with a Developer ID identity, then notarize with xcrun notarytool.

How is this guide?

Edit on GitHub

Last updated on

On this page