synsema

One program, three apps — web, phone and desktop from the same Synsema file

A Synsema app is a website with automatic HTTPS, an installable phone app with native push, and a desktop app in its own window with your icon — from one server-rendered program and one API module. No native toolkit, no second codebase, no store required.

The usual way to reach three platforms is three codebases, or one framework that wraps a browser and asks you to learn it. Synsema takes the other road: the app is a server-rendered program, and each platform is a way of shipping the same program. The website, the phone app and the desktop app share the routes, the templates, the capabilities and the secrets, because they are one file with three entries.

Three targets, one file§

synsema init myapp --desktop && cd myapp                          # the scaffold: app.syn, desk.syn, api.syn, public/

synsema serve app.syn --domain app.example.com --tls-auto you@example.com   # the WEBSITE, with Let's Encrypt
                                                                  # the PHONE: the same site installs from the browser (PWA)
synsema build desk.syn -o desk --serve --no-console --icon public/icon.svg  # the DESKTOP app (Windows: desk.exe)
synsema build desk.syn -o desk --serve --icon public/icon.svg --bundle      # macOS .app / Linux folder + install.sh

The website is app.syn under serve: HTTPS in one flag, the page rendered on the server, every route also answering Markdown and JSON to agents. The phone app is the same site: a manifest, a service worker and icons make it install on Android and iOS with a home-screen icon, full screen, an honest offline shell and native Web Push — that is what --pwa scaffolds. The desktop app is desk.syn: the same page and the same API bound to 127.0.0.1, opened as an app window of the browser already on the machine, with your icon on the executable and no console; it quits when the last window closes.

The same API everywhere§

The API lives once, in api.syn, as a routes group:

-- api.syn
export routes api
    route "GET /api/ping"
        give ok({"pong": true, "at": now()})
    route "POST /api/push/subscribe"
        rate_limit 10 per minute
        expect body {endpoint: text, keys: map}
        ...

Both entries mount it — mount api.api — and each declares the capabilities the routes need (require net("fcm.googleapis.com"), require secret("VAPID_PRIVATE_KEY")): a module carries no require, the entry does. Per-route rate limits and timeouts travel with the group, so the public server and the local desktop app enforce the same rules. Under a --sandbox or --cap-set build, the ceiling travels with the binary and cannot be raised from inside.

What each target gets, honestly§

WebsitePhone (PWA)Desktop
How it runssynsema serve on your serverthe same server, installed from the browserone binary on the user's machine
Iconfaviconhome-screen icon from the manifestthe executable's icon (--icon), the window's from the page
Windowbrowser tabfull screen, no browser chromea browser app window, no address bar, no console
Pushnative Web Push (Android; iOS once installed, on HTTPS)the browser's notifications and Web Push, while the process runs
Offlinethe shell opens without networkthe server is local
Lifecycleyou run itthe phone runs the browserquits when the last window closes, or after 30 s with no window
Verifiedproductionreal Android and Windowslive on Windows 11; macOS by CI on Apple Silicon; Linux by tests

Where it stops: there is no system tray (no web API for it), Firefox has no app mode (a tab), iOS needs a trusted certificate for the service worker and push, and a downloaded macOS .app without a Developer ID meets Gatekeeper like any unsigned app. Stores are optional: the Microsoft Store takes a PWA directly, Play takes it through a Trusted Web Activity, and the App Store still wants a native wrapper — none of which the language pretends to hide.

Why it holds together§

Server-side rendering means the phone and the desktop show what the server decided, not a copy of the logic. Content negotiation means the same routes are an API for agents. Deny-by-default capabilities mean the desktop binary you hand to someone touches only what its require lines say, and secrets stay sealed inside the process on every platform. And synsema build means the whole thing — engine, program, templates, public/ — is one file to give away.

Try it: install Synsema (0.6.19 or newer), synsema init myapp --desktop, then synsema serve app.syn for the site and synsema serve desk.syn for the window. The two walkthroughs — Your app on the phone and Your app on the desktop — list every file the scaffold writes and every flag, with what is verified where.