Operación
Deploy
Synsema viene como un único binario estático — sin runtime en el destino. El bloque serve queda dev-clean en el repo; los knobs de deploy son flags del CLI, así el mismo archivo corre local y en prod sin ediciones.
synsema serve app.syn # dev: :8080, HTTP plano, sin setup
synsema serve app.syn --port 443 --domain example.com,www.example.com --tls-auto admin@example.com # prod: HTTPS
Flags de serve§
| Flag | Efecto |
|---|---|
--port N | Override de serve on N y concede serve(N). |
--domain d1,d2 | Dominios ACME (SAN). |
--tls-auto <email> | HTTPS automático (ACME) — es el switch dev↔prod; necesita un dominio. |
--tls-cert / --tls-key | TLS manual (mutuamente excluyente con --tls-auto). |
--bind <addr> | Dirección de bind (default 0.0.0.0). |
--sandbox / --cap-set "<lista>" | Techo del host para todo el server (engine v0.6.7+): handlers, ticks de cron y agentes spawneados sólo pueden require dentro de él. --sandbox = stdout,time + serve. |
Precedencia: flag del CLI > cláusula del archivo > default. Sin --tls-auto → HTTP plano (dev); con --tls-auto → TLS (prod).
Config por entorno (mantené el repo dev-clean)§
El archivo .syn no cambia entre tu laptop y el server, así que git pull en prod nunca genera conflictos. Todo lo que difiere vive fuera del código.
Knobs del server → flags del CLI. --port, --domain, --tls-auto (tabla de arriba). El flag gana sobre la cláusula del archivo.
Valores de la app → el entorno. Todo lo que leas con env("NAME", default) — p. ej. la URL pública canónica que alimenta tus tags canonical/OG/sitemap:
require env("SITE_URL")
let SITE be env("SITE_URL", "http://127.0.0.1:8080") -- default dev; prod lo pisa
Seteala en prod desde el entorno (Environment=SITE_URL=https://example.com en systemd, -e SITE_URL=… en Docker). La resolución es process env > .env > default del código, así que no hace falta editar el repo.
Knobs del runtime → el entorno del proceso (no .env). SYNSEMA_SERVE_WORKERS=N dimensiona el pool de workers del intérprete que atienden requests (default: uno por core, mín. 2). Subilo para handlers I/O-bound — más requests en vuelo, a costa de más RAM. A diferencia de los valores de la app, este knob configura el runtime mismo, así que se lee una sola vez, del entorno del proceso, antes de que arranque el primer server — Environment= en systemd, -e en Docker, export en una shell. Ponerlo en .env no hace nada: el .env sólo alimenta a env()/secret() dentro de tu programa.
La misma regla cubre todos los knobs del server (engine v0.6.7+): SYNSEMA_SHUTDOWN_GRACE (segundos de drain ante SIGINT/SIGTERM, default 10), SYNSEMA_SSE_KEEPALIVE (15), SYNSEMA_WS_SERVER_PING (30), SYNSEMA_WS_SUBPROTOCOLS, SYNSEMA_WS_MAX_MESSAGE (16MB), SYNSEMA_WS_MAX_CONNS (4096), SYNSEMA_PROC_MAX (64), SYNSEMA_WATCH_MAX (64, handles watch vivos por intérprete, v0.6.9+). synsema init los lista comentados en .env.example con esa advertencia; la tabla con significados está en Apps agénticas. systemctl stop / docker stop mandan SIGTERM → shutdown ordenado (drain, salida 0) — poné TimeoutStopSec por encima de la gracia.
¿Dos apps en un mismo host? Dale a cada una su puerto — el código queda idéntico: synsema serve api.syn --port 8081 y synsema serve admin.syn --port 8082. Como --port pisa serve on N y concede serve(N), no cambia nada en ninguno de los dos archivos.
HTTPS, paso a paso (certificado gratis y auto-renovable)§
--tls-auto obtiene un certificado gratis de Let's Encrypt (ACME) y lo auto-renueva — sin certbot, sin cron. Qué necesitás:
1. Un dominio apuntando a la IP del server (un registro DNS A/AAAA). 2. Puertos 80 y 443 alcanzables — el 80 responde el desafío ACME una vez, después redirige al 443. 3. Correr con el dominio + un email de contacto:
synsema serve app.syn --port 443 --domain example.com,www.example.com --tls-auto you@example.com
Al primer arranque Synsema obtiene el certificado, sirve HTTPS en 443, redirige HTTP→HTTPS, y auto-renueva ~30 días antes del vencimiento (90 días). Los certificados se guardan (SYNSEMA_CERT_DIR → ~/.synsema/certs), así que un restart los recarga (sin re-emisión → sin rate-limit).
El challenge de ACME escucha en el puerto 80 (HTTP-01), y la CA tiene que poder alcanzarlo desde internet. Si ese puerto ya está tomado —otro servidor, un mapeo de contenedor, un sidecar— movelo con SYNSEMA_ACME_HTTP_PORT=8080 y redirigí el :80 externo hacia ahí. Como el resto de los knobs de runtime se lee del entorno del proceso, no del .env.
¿Ya tenés un certificado? Usá --tls-cert cert.pem --tls-key key.pem (mutuamente excluyente con --tls-auto). Bajo systemd, dale al servicio un HOME/StateDirectory escribible (abajo) para que pueda guardar los certificados.
synsema daemon vs systemd — elegí uno§
synsema daemon start app.syn— manager de background integrado (status/logs/stop/restart). Sin config de SO, pero sin arranque al boot ni restart al crashear. Bueno para dev / máquinas sin systemd.- systemd — supervisor del SO: arranque al boot (
enable),Restart=always, logs en journald. Usalo para producción.
[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 # HOME escribible para el fallback ~/.synsema/certs
Varios sitios en un mismo host (Synsema es su propio edge proxy)§
Dos procesos no pueden bindear ambos :443, y no necesitás nginx/Caddy. Un proceso Synsema es el edge: termina TLS para cada dominio (un cert SAN) y enruta por Host a cada backend, que corre en HTTP plano en un puerto privado.
-- edge.syn — TLS + routing por Host para cada sitio del server
require serve(443)
require net("127.0.0.1") -- deny-by-default: el edge sólo habla con localhost
serve on 443
host "example.com"
route "GET /" -- raíz: /*path NO matchea "/"
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"
Corré el edge con un cert SAN para todos los dominios; cada backend en HTTP plano, sólo localhost, con su propio repo/versión/servicio:
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
- Gotcha de la raíz:
route "GET /*path"exige ≥1 segmento — no matchea/. Agregároute "GET /"también (por método) para que la home llegue al backend. - Por método:
routeliga método+path — declará cada método que reenvíes (GET, POST, …). proxy toreenvía status + content-type + body y los headers end-to-end del upstream (Location,Set-Cookie,Cache-Control,ETag, …), así redirects, cookies y caching funcionan a través del edge; los hop-by-hop se descartan.- Los streams cruzan el edge. Una ruta
stream(SSE) del backend llega evento a evento; una rutasocket(WebSocket) del backend se tuneliza después del101; las descargas grandes fluyen con suContent-Length. Así un backend de chat y una API común pueden ser cada uno su propio proceso — su propio contratorequire, su puerto, su deploy — detrás de un solo edge TLS. El edge agregaX-Forwarded-For/-Proto/-Host; los targets son sólohttp://(TLS termina en el edge — un targethttps://falla al arrancar, no por request). Túneles y respuestas sin tamaño cuentan contramax_streamsy los cierra el shutdown ordenado. - Deploys independientes: reiniciás un backend sin tocar los otros; cada uno puede correr su propia versión de Synsema detrás del mismo edge.
Un binario con tu programa horneado adentro (synsema build)§
synsema serve app.syn necesita el .syn (y sus módulos/templates/assets) presentes en runtime. synsema build app.syn -o app pliega todo eso dentro del ejecutable — el engine más tu programa, sellado con un sha256 — así el entregable es un único archivo que no lleva nada por fuera (ver CLI § synsema build):
synsema build app.syn -o app --include data/ # empaqueta assets extra (los mounts estáticos del serve entran solos, v0.6.16+)
synsema build app.syn -o app --cap-set "stdout,net=api.example.com,serve=8080" # hornea un techo
synsema build app.syn -o app --serve --bind 0.0.0.0 --port 8080 # un binario SERVER (v0.6.16+): runtime de serve + flags de deploy horneados
synsema build desk.syn -o desk --serve --no-console --icon icon.svg --bundle # una app de ESCRITORIO (v0.6.18+): ver "Tu app en el escritorio"
./app # corre el programa; el argv es el del programa
Como no hay nada que montar, la imagen puede ser FROM scratch/distroless:
FROM scratch
COPY app /app
ENTRYPOINT ["/app"]
Los --cap-set/--profile horneados viajan con el binario y no se pueden subir desde adentro; app --engine version llega al engine pelado; app --engine update se rechaza (reconstruí en su lugar). Para imágenes cross-target, construí contra un binario de engine donante con --engine-binary ./synsema-linux-x86_64. Este es el deploy más autocontenido: distribuir app es distribuir el programa entero y sus barandas como un solo artefacto. El mismo build, con bind "127.0.0.1", --no-console, --icon y --bundle, es una app de escritorio — Tu app en el escritorio.
Docker y Kubernetes§
La imagen es solo el binario de Synsema — el mismo binario estático prebuilt que instala el sitio. El Dockerfile del repo lo baja de la release de GitHub y verifica su checksum (no compila); vos montás tu programa .syn en /app:
docker build -t synsema . # o --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
En K8s, command: ["synsema", "serve", "/app/app.syn"] e inyectá claves vía secretKeyRef. Los secretos/config vienen del environment en prod (override de .env) — ver Secretos.
Actualizar§
Un server corriendo no se auto-actualiza. synsema update reemplaza el binario en disco; systemctl restart lo aplica. Los certs TLS persisten (guardados + auto-renovados), así que un restart los recarga — sin pegarle al rate-limit de Let's Encrypt.