# 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.

Published 2026-09-02 · https://synsema.org/blog/one-app-web-phone-desktop


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

```sh
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:

```synsema
-- 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

| | Website | Phone (PWA) | Desktop |
|---|---|---|---|
| How it runs | `synsema serve` on your server | the same server, installed from the browser | one binary on the user's machine |
| Icon | favicon | home-screen icon from the manifest | the executable's icon (`--icon`), the window's from the page |
| Window | browser tab | full screen, no browser chrome | a browser app window, no address bar, no console |
| Push | — | native Web Push (Android; iOS once installed, on HTTPS) | the browser's notifications and Web Push, while the process runs |
| Offline | — | the shell opens without network | the server is local |
| Lifecycle | you run it | the phone runs the browser | quits when the last window closes, or after 30 s with no window |
| Verified | production | real Android and Windows | live 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](/install) (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](https://synsema.dev/en/0.6.x/41b-pwa) and
[Your app on the desktop](https://synsema.dev/en/0.6.x/41c-desktop) — list every file the
scaffold writes and every flag, with what is verified where.

