Deploy
Synsema ships as a single static binary — no runtime on the target. The serve block stays dev-clean in the repo; deployment knobs are CLI flags, so the same file runs locally and in prod without edits.
synsema serve app.syn # dev: :8080, plain HTTP, no setup
synsema serve app.syn --port 443 --domain example.com,www.example.com --tls-auto admin@example.com # prod: HTTPS
Serve flags
| Flag | Effect |
|---|---|
--port N | Overrides serve on N and grants serve(N). |
--domain d1,d2 | ACME SAN domains. |
--tls-auto <email> | Automatic HTTPS (ACME) — this is the dev↔prod switch; needs a domain. |
--tls-cert / --tls-key | Manual TLS (mutually exclusive with --tls-auto). |
--bind <addr> | Bind address (default 0.0.0.0). |
Precedence: CLI flag > file clause > default. No --tls-auto → plain HTTP (dev); --tls-auto → TLS (prod).
Per-environment config (keep the repo dev-clean)
The .syn file never changes between your laptop and the server, so git pull on prod never conflicts. Everything that differs lives outside the code.
Server knobs → CLI flags. --port, --domain, --tls-auto (table above). The flag wins over the file clause.
App values → the environment. Anything you read with env("NAME", default) — e.g. the canonical public URL that feeds your canonical/OG/sitemap tags:
require env("SITE_URL")
let SITE be env("SITE_URL", "http://127.0.0.1:8080") -- dev default; prod overrides
Set it on prod in the environment (Environment=SITE_URL=https://example.com under systemd, -e SITE_URL=… in Docker). Resolution is process env > .env > code default, so no repo edit is needed.
Runtime knobs → the process environment (not .env). SYNSEMA_SERVE_WORKERS=N sizes the pool of interpreter workers that handle requests (default: one per core, min 2). Raise it for I/O-bound handlers — more requests in flight, at the cost of more RAM. Unlike app values, this knob configures the runtime itself, so it is read once, from the process environment, before the first server starts — Environment= under systemd, -e in Docker, export in a shell. Putting it in .env does nothing: .env only feeds env()/secret() inside your program.
Two apps on one host? Give each its own port — the code stays identical: synsema serve api.syn --port 8081 and synsema serve admin.syn --port 8082. Since --port overrides serve on N and grants serve(N), nothing in either file changes.
HTTPS, step by step (free, auto-renewing cert)
--tls-auto gets a free certificate from Let's Encrypt (ACME) and auto-renews it — no certbot, no cron. What you need:
1. A domain pointing to the server's IP (a DNS A/AAAA record).
2. Ports 80 and 443 reachable — port 80 answers the one-time ACME challenge, then redirects to 443.
3. Run with the domain + a contact email:
synsema serve app.syn --port 443 --domain example.com,www.example.com --tls-auto you@example.com
On first boot Synsema obtains the cert, serves HTTPS on 443, redirects HTTP→HTTPS, and auto-renews ~30 days before the 90-day expiry. Certs are stored (SYNSEMA_CERT_DIR → ~/.synsema/certs), so a restart reloads them (no re-issue → no rate-limit).
The ACME challenge listens on port 80 (HTTP-01), which the CA must reach from the public
internet. If something else already owns that port — another server, a container mapping,
a sidecar — move it with SYNSEMA_ACME_HTTP_PORT=8080 and forward external :80 to it.
Like the other runtime knobs it is read from the process environment, not .env.
Already have a cert? Use --tls-cert cert.pem --tls-key key.pem instead (mutually exclusive with --tls-auto). Under systemd, give the service a writable HOME/StateDirectory (below) so it can store certs.
synsema daemon vs systemd — pick one
synsema daemon start app.syn— built-in background manager (status/logs/stop/restart). No OS config, but no boot-start, no crash-restart. Good for dev / no-systemd boxes.- systemd — OS supervisor: boot-start (
enable),Restart=always, journald logs. Use for production.
[Service]
ExecStart=/usr/local/bin/synsema serve /opt/app/app.syn --port 443 --domain example.com --tls-auto admin@example.com
Restart=always
StateDirectory=synsema # writable HOME for ~/.synsema/certs fallback
Multiple sites on one host (Synsema is its own edge proxy)
Two processes can't both bind :443, and you don't need nginx/Caddy. One Synsema process is the edge: it terminates TLS for every domain (one SAN cert) and routes by Host to each backend, which runs plain-HTTP on a private port.
-- edge.syn — TLS + Host routing for every site on the box
require serve(443)
require net("127.0.0.1") -- deny-by-default: the edge only talks to localhost
serve on 443
host "example.com"
route "GET /" -- root: /*path does NOT match "/"
proxy to "http://127.0.0.1:8080"
route "GET /*path"
proxy to "http://127.0.0.1:8080"
route "POST /*path"
proxy to "http://127.0.0.1:8080"
host "docs.example.com"
route "GET /"
proxy to "http://127.0.0.1:8791"
route "GET /*path"
proxy to "http://127.0.0.1:8791"
route "POST /*path"
proxy to "http://127.0.0.1:8791"
Run the edge with a SAN cert for all domains; each backend runs plain-HTTP, localhost-only, with its own repo/version/service:
synsema serve edge.syn --port 443 --domain example.com,docs.example.com --tls-auto admin@example.com
synsema serve app.syn --port 8080 --bind 127.0.0.1
synsema serve docs.syn --port 8791 --bind 127.0.0.1
- Root gotcha:
route "GET /*path"needs ≥1 segment — it does not match/. Addroute "GET /"too (per method) so the home page reaches the backend. - Per method:
routebinds method+path — declare each method you forward (GET, POST, …). proxy toforwards status + content-type + body and the upstream's end-to-end headers (Location,Set-Cookie,Cache-Control,ETag, …), so redirects, cookies and caching work through the edge; hop-by-hop are dropped.- Independent deploys: restart one backend without touching the others; each can run its own Synsema version behind the same edge.
Docker & Kubernetes
The image is just the Synsema binary — the same prebuilt static binary the site installs. The Dockerfile in the repo fetches it from the GitHub release and verifies its checksum (it does not compile); you mount your .syn program into /app:
docker build -t synsema . # or --build-arg SYNSEMA_VERSION=v0.4.9
docker run -d --restart unless-stopped -e ANTHROPIC_API_KEY=sk-... \
-v "$PWD":/app -w /app -p 8080:8080 synsema serve app.syn
In K8s, command: ["synsema", "serve", "/app/app.syn"] and inject keys via secretKeyRef. Secrets/config come from the environment in prod (overrides .env) — see Secrets.
Updating
A running server does not auto-update. synsema update swaps the binary on disk; systemctl restart applies it. TLS certs persist (stored + auto-renewed), so a restart reloads them — no Let's Encrypt rate-limit hit.