Advanced Features
What sits above serving a JSON: real-time events, signed outgoing webhooks, the status endpoint, service mode, deploying into the installation and the test bench.
Server-Sent Events (SSE)
SSE lets the server push events to the client over an HTTP connection that stays open. The contract has three pieces, and all three are visible from the controller: the route has to end in /events and be declared, the client has to send Accept: text/event-stream or it gets a 406, and the backend declares which events it supports with res.events(). The framing, the retry:, the connected event, the pings and the replay through Last-Event-Id are the middleware’s job.
Enable in JSON:
"middleware": { "sse": { "enabled": true } }In the controller:
* La ruta TIENE que terminar en /events. Aqui no se escribe el
* framing a mano ni se hace un bucle: eso ocuparia uno de los pocos
* hilos VFP del pool durante todo el rato. Lo unico que hace este
* endpoint es DECLARAR que eventos existen.
PROCEDURE pSuscripcion(req, res) HELP "GET: pedidos/events public"
res.events("pedido.creado")
res.events("pedido.enviado")
RETURN THIS.Ok(.NULL.)
ENDPROC
* Y se emite desde cualquier otro endpoint, cuando pasa algo.
PROCEDURE pCrearPedido(req, res) HELP "POST: pedidos"
LOCAL lcId, loRes
lcId = THIS.CrearPedido(req.json)
res.emit("pedido.creado", '{"id":"' + lcId + '"}')
loRes = THIS.newObject("id")
loRes.id = lcId
RETURN THIS.Created(loRes, "/pedidos/" + lcId)
ENDPROCJavaScript client:
// EventSource manda Accept: text/event-stream por su cuenta.
// Sin esa cabecera, el middleware responde 406.
const es = new EventSource('/pedidos/events?events=pedido.creado');
// El middleware manda primero un evento 'connected' que dice que
// eventos hay, y despues los tuyos, cada uno con su id.
es.addEventListener('pedido.creado', (e) => {
console.log(JSON.parse(e.data).id);
});
// Si se corta, el navegador reconecta solo mandando Last-Event-Id,
// y el servidor repone lo que tenga en cache: la entrega es
// at-least-once, asi que deduplica por e.lastEventId.
es.onerror = () => es.close();Webhooks System
FoxServer implements Standard Webhooks in both directions, audited and with tests. The key thing about the model: outgoing ones are not fired from code, they are declared in the manifest, and when that endpoint is served the response body goes out signed to the active subscribers.
- Declared with
foxserver webhook-add-out, and tuned withwebhook-set --retries --timeout --backoff - HMAC-SHA256 signature in base64 over
{id}.{timestamp}.{body}, prefixedv1, - The secret can be
whsec_<base64>(the svix, Clerk or Resend format), and then the key is the decoded bytes retries: Nmeans N attempts in total, not N extra retries. And a 404 from the subscriber stops the retries at once- What runs out ends up in
<host>\queue\webhooks\dead-letters\ - Every attempt shares the same
webhook-id, so the receiver can deduplicate, and each one is re-signed with its own timestamp - You test it without leaving the console, with
foxserver webhook-test
Declaring an outgoing one:
REM Se declara con la CLI, no editando el manifiesto a mano.
foxserver webhook-add-out --project C:\proyectos\tienda ^
--route pedidos --method POST ^
--url https://socio.com/webhooks --secret whsec_...
REM Y se ajusta despues:
foxserver webhook-set --project C:\proyectos\tienda ^
--route pedidos --retries 3 --timeout 30 --backoff 5,15,30
REM Para probarlo sin salir de la consola:
foxserver webhook-test --project C:\proyectos\tienda --route pedidosAnd that is it: the endpoint is written like any other. Whatever it returns is what gets sent.
* Ni una linea de webhook aqui dentro. Lo que devuelve este endpoint
* es exactamente lo que sale firmado hacia los suscriptores.
PROCEDURE pCrearPedido(req, res) HELP "POST: pedidos"
LOCAL lcId, loRes
lcId = THIS.CrearPedido(req.json)
loRes = THIS.newObject("orderId,status")
loRes.orderId = lcId
loRes.status = "created"
RETURN THIS.Created(loRes, "/pedidos/" + lcId)
ENDPROCWhat reaches the subscriber:
POST https://socio.com/webhooks
webhook-id: 550e8400-e29b-41d4-a716-446655440000
webhook-timestamp: 1789476225
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
{"orderId":"ORD-001","status":"created"}The webhook-timestamp is epoch in seconds, and the signature is v1, plus the base64 of the HMAC-SHA256 over {id}.{timestamp}.{body}. If your controller gets to run with an INCOMING webhook, the middleware has already checked the signature and the five-minute anti-replay window: the webhook is already authentic. One thing to watch: a provider with its own signature, such as Stripe or GitHub, must not be declared as incoming, because the middleware would reject it for not being Standard Webhooks. That route is served as a normal endpoint and you verify it yourself.
Reloading without restarting
There is no automatic reload, and it is worth saying plainly because the phrase "hot reload" suggests otherwise: FoxServer does not watch the disk. What there is is a switch, and it is enough: after a foxserver build, a POST to the status endpoint releases the server’s COM objects, and the next request creates them clean, with your new DLL.
The full cycle:
- You edit and save your
.prg. - You rebuild with
foxserver build(or from FoxForge) and the project MTDLL is regenerated. Mind this: it needs an elevated console, becauseBUILD MTDLLregisters the coclass when it finishes. POST <status-endpoint>/reloadwith theX-Status-Pinheader.- The server drops its COM references. The next request creates the controllers again, now with the new code.
The call:
curl -X POST http://127.0.0.1:8099/status/reload ^
-H "X-Status-Pin: <tu PIN>"The status endpoint
It is declared in the manifest and asks for a PIN: without the X-Status-Pin header, or with the wrong PIN, it answers 401; if the server has no PIN configured, 403. Any verb other than GET on that route gives a 404, not a 405.
"server": {
"checkstatusenabled": true,
"endpoint": "status",
"statuspin": "<cifrado con middleware-set>"
}curl http://127.0.0.1:8099/status -H "X-Status-Pin: <tu PIN>"And what it returns:
{
"uptime": {
"raw": "...",
"formatted": "5d 3h 22m"
},
"memory": {
"raw": 257425408,
"formatted": "245 MB"
},
"connections": {
"current": 7
},
"config": {
"host": "127.0.0.1",
"port": 8099,
"middleware": {
"auth": true,
"logging": true
}
},
"license": {
"kind": "Runtime",
"holder": "Tu Empresa, S.L.",
"seats": 1,
"isMaintenanceActive": true,
"limitedByMaintenance": false
},
"metrics": {
"successRate": 99.97,
"rejectedRequests": 0,
"rejectionRate": 0
}
}Windows Service mode
FoxServer can be installed as a Windows Service for automatic startup and background execution without a console window.
Commands:
# Install as service
bin\FoxServer.Host.exe --install-service
# Uninstall service
bin\FoxServer.Host.exe --uninstall-service
# Manage via Windows Services
services.mscDeploying into the installation
foxserver serve starts the host pointing at your project's build\ folder: fine for testing, but verify.cmd does not see that server and the Windows service does not start it, because both look at config\servers\ in the installation root. When you want it to be part of it:
foxserver deploy --project C:\proyectos\almacenDesplegado Almacen en la instalacion:
C:\Program Files\FoxServer\config\servers\ALMACEN.json
clave: 127.0.0.1:8099:Rebuilding your application's environment
Your business logic needs what your ERP's main.prg sets up - the configuration, the connection, the master tables - and a controller is born with none of it. You do not have to rewrite the logic: you have to rebuild the environment once. That is what the lifecycle is for.
define class MiErpLifecycle as ApplicationLifecycle olepublic
procedure OnApplicationStarting
local loCfg
* Lo que hace el main.prg de tu aplicacion: SET PATH, abrir las
* maestras, leer la configuracion, conectar.
loCfg = createobject("Empty")
addproperty(loCfg, "empresa", "SbSoftware")
this.SetResource("config", loCfg)
endproc
procedure OnApplicationStopping
* Cerrar lo que se abrio.
endproc
enddefine"lifecycle": { "enabled": true, "classname": "MiErpLifecycleClass" }Debugging without compiling - FoxServerBench
The tool that saves the most time: it runs your controller with no MTDLL, no server and no http.sys, with the debugger open over your own source. You put SET STEP ON in your procedure, launch the bench, and stop wherever you want.
loBench = createobject("FoxServerBench")
loBench.SetProject("c:\misproyectos\ventas")
loBench.OnDeclare = "MiCarga" && tu rutina: SET PROCEDURE, abrir tablas...
loBench.SetResource("config", loFalsa) && simula lo que deja el lifecycle
loBench.Request("GET", "articulos/42")
loRes = loBench.Run()
? loRes.StatusCode
? loRes.BodyNext: API Reference →