Synsemadocsv0.6.xENES

Operación

Deploy

Synsema viene como un único binario autocontenido — sin runtime en el destino (Linux: un binario glibc con piso 2.17 desde v0.6.21, más abajo). 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
¿Preferís alojado? Todo lo de esta página es para correr Synsema en una máquina tuya. La Plataforma Synsema es el otro camino: syn deploy desde la carpeta, una URL de vuelta, y de los secretos, los logs, la auditoría de capacidades y los gates humanos se ocupa la plataforma. El programa es el mismo en los dos casos.

Flags de serve§

FlagEfecto
--port NOverride de serve on N y concede serve(N).
--domain d1,d2Dominios ACME (un certificado por nombre desde v0.6.42).
--tls-auto <email>HTTPS automático (ACME) — es el switch dev↔prod; necesita un dominio.
--tls-cert / --tls-keyTLS manual (mutuamente excluyente con --tls-auto).
--bind <addr>Dirección de bind (default 0.0.0.0).
--health <path>(engine v0.6.20+) Endpoint de salud opt-in para el host: GET <path> → 200 {ok, uptime_s, in_flight, engine}, sin auth ni rate limit, ausente del descubrimiento. Lo mismo que SYNSEMA_HEALTH_PATH en el entorno; una ruta declarada en ese path gana (aviso al arrancar).
--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")   -- dev default; prod overrides

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. Desde el motor v0.6.40 los workers comparten una sola copia congelada de las globales del programa, así que un worker más cuesta unos pocos MB; hasta v0.6.39 cada worker armaba su propia copia y la RAM crecía con globales × workers — ahí, con datos grandes en memoria mantené N bajo (ver Rendimiento y memoria).

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. En esa tabla viven también dos knobs del host que no son del server: SYNSEMA_STATE_DIR (dónde memory("NAME") guarda su .db — apuntalo a un volumen y dejá el código en solo lectura; solo environ del proceso) y SYNSEMA_LLM_BUDGET (techo de tokens LLM por proceso; las ops degradan a un marker, nunca un error) — ver Memoria y Primitivas LLM. Tres más desde engine v0.6.20: SYNSEMA_LLM_BUDGET_PER_IDENTITY="id=N,…" (techos de tokens por identidad autenticada bajo serve), SYNSEMA_HEALTH_PATH (el endpoint de salud de la tabla de arriba) y SYNSEMA_AUDIT=json|<path>|fd:N|unix:<path> (el stream de audit de capacidades sin flag — así lo activa un contenedor, también para un binario de synsema build). 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 los certificados, sirve HTTPS en 443, redirige HTTP→HTTPS, y auto-renueva cada uno ~30 días antes de que venza. Los certificados, sus claves y la cuenta ACME se guardan (SYNSEMA_CERT_DIR → ~/.synsema/certs), así que un restart los recarga (sin re-emisión → sin rate-limit).

Desde el motor v0.6.42:

emite sólo ese. En el primer arranque después de actualizar, los nombres que estaban sólo en el certificado compartido viejo obtienen el suyo — ojo con los límites de la CA si son muchos.

reintenta en segundo plano (cada minuto mientras falte un nombre de domain; cada nombre espera más tras un fallo, de 1 min duplicando hasta 1 h). Cada emisión tiene un límite de 2 minutos. Un certificado en disco se reusa sólo si nombra al dominio y no venció; las claves se escriben de forma atómica, 0600 en Unix (dejá SYNSEMA_CERT_DIR en el perfil del usuario del servicio — en Windows rige la ACL de la carpeta), y se verifican contra su certificado.

a tu task (name, value, action): con "set" publica un registro TXT name = value en tu proveedor de DNS, con "clear" lo borra. El motor no conoce ningún proveedor — la task llama a su API HTTP, así que necesita su propio require net(...). SYNSEMA_ACME_DNS_WAIT (20 s) es la espera a que el registro se propague. Un domain comodín sin tls dns se rechaza.

nombre sin certificado, el motor le pregunta a la task; true lo emite en el momento (HTTP-01 en :80), cualquier otra cosa — un error, o ninguna respuesta en 10 s — lo rechaza. No hay emisión bajo demanda sin ask: aprobá sólo nombres que sabés que son tuyos (buscalos en tu base), o cualquiera puede hacerte pedir certificados. ask corre de a 8 como máximo y 20 nombres nuevos por segundo, y SYNSEMA_ACME_MAX_PER_HOUR (20) limita la emisión bajo demanda. A un nombre se le vuelve a preguntar antes de renovarlo.

require serve(443)
require net("api.cloudflare.com")

task publish_txt(name, value, action)
    -- call your DNS provider's API: create ("set") or delete ("clear") the TXT record
    give true

task allow_host(host)
    give ends_with(host, ".customers.example")   -- or: is it in your table of custom domains?

serve on 443
    tls auto "admin@example.com"
    domain ["example.com", "*.example.com"]
    tls dns publish_txt
    domain ask allow_host
    route "GET /"
        give "hello"

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). Los flags son del operador y no necesitan capability. Los mismos archivos se pueden nombrar en el programa, como cláusula del bloque serve — entonces el programa los lee, así que declara los dos (engine v0.6.45+; sin eso el server no arranca: Capability not granted: file_read("certs/cert.pem")):

require serve(443)
require file.read("./certs/cert.pem")
require file.read("./certs/key.pem")

serve on 443
    tls cert "./certs/cert.pem" key "./certs/key.pem"
    route "GET /"
        give "hello over TLS"

Bajo systemd, dale al servicio un HOME/StateDirectory escribible (abajo) para que pueda guardar los certificados.

synsema daemon vs systemd — elegí uno§

[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

Jobs y horarios. Un job que corre con synsema run (una unidad de systemd, un contenedor, una línea de cron) escribe print, log y show línea por línea desde v0.6.29, así que journald y docker logs ven cada línea cuando ocurre en vez de todas juntas al salir. Para un horario dentro de un serve de larga vida, cron_every(expr, task, {"tz": "America/Santiago"}) (v0.6.30+) dispara a la hora local de esa zona todo el año, tenga el reloj del server la zona que tenga — ver Cron.

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 certificado por nombre desde v0.6.42) y enruta por Host a cada backend, que corre en HTTP plano en un puerto privado.

-- 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"

Corré el edge con un certificado para cada dominio; 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

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 --audit json               # hornea el audit de capacidades (o SYNSEMA_AUDIT=json ./app al correr, v0.6.20+)
synsema build job.syn -o job --deterministic            # perfil pure + techo de solo stdout: corridas reproducibles (v0.6.20+)
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 una base mínima que traiga una libc (el binario está enlazado contra glibc — FROM scratch no tiene libc y no funciona):

FROM gcr.io/distroless/cc-debian12
COPY app /app
ENTRYPOINT ["/app"]

El binario Linux y glibc. La release de Linux es un binario dinámico contra glibc, no uno estático. Desde engine v0.6.21 se compila contra glibc 2.17, así que corre en cualquier distribución desde 2014 (Ubuntu 20.04/22.04/24.04, Debian 11/12/13, RHEL 7+, Amazon Linux 2, las imágenes debian:*-slim y gcr.io/distroless/cc) y el release falla si ese piso llegara a subir. v0.6.20 fue la excepción: quedó enlazada con la glibc 2.39 del runner y sólo arranca en Ubuntu 24.04+/Debian 13+ — en algo más viejo falla con GLIBC_2.39 not found; actualizá a v0.6.21. Los binarios de macOS y Windows no tienen ese piso. FROM scratch no funciona (adentro no hay libc) — usá gcr.io/distroless/cc-debian12 o debian:bookworm-slim; install.sh corre el binario una vez al instalar y te dice claro cuando un sistema es demasiado viejo.

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 prebuilt que instala el sitio, sobre una base con glibc (debian:bookworm-slim; necesita engine v0.6.21+ — el binario de v0.6.20 pedía glibc 2.39 y no arranca en bookworm); es el 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. Para un liveness/readiness probe, poné SYNSEMA_HEALTH_PATH=/healthz en el entorno del contenedor y apuntá el probe ahí — el programa no declara nada (engine v0.6.20+). Los secretos/config vienen del environment en prod (override de .env) — ver Secretos.

Rotar secretos sin reiniciar (Unix, engine v0.6.20+)§

En Linux y macOS, kill -HUP <pid> (bajo systemd: ExecReload=/bin/kill -HUP $MAINPID y después systemctl reload) hace que un server corriendo relea su .env en la próxima resolución de secret()/env() y reconstruya el provider LLM con la clave nueva — sin reinicio, sin cortar conexiones. Un secret que copiaste a un let al arrancar es un valor y conserva el contenido viejo: resolvé los secretos donde los usás. El entorno del proceso se lee en vivo y no necesita señal. Windows no tiene SIGHUP: ahí el camino sigue siendo el reinicio con drenado.

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.