How the connection works
127.0.0.1:8787 is not a public address. It is the loopback address of the machine running your executor. On a VPS, only processes on that VPS can connect to it.
Suppio never connects to 127.0.0.1. It connects to a public URL such as:
What Suppio expects
Your configured executor URL must meet every requirement below.Start with the complete Node.js executor
Choose how to publish the executor
VPS with Caddy
Cloudflare Tunnel
Managed hosting
Option 1: VPS with Caddy
This is the simplest direct VPS setup. The examples assume a Debian or Ubuntu server, a domain you control, and an executor application installed in/opt/customer-actions.
Before you start
You need:- a VPS with a public IPv4 address or properly configured public IPv6 address
- SSH access with
sudo - a domain or subdomain you control
- the complete Node.js executor example, or an equivalent implementation in your stack
- the signing secret shown by the Suppio dashboard
CUSTOMER_ACTION_ENCRYPTION_KEY in the executor. Your executor only needs the per-agent signing secret shown when you save or rotate the endpoint.
Bind the executor to loopback
127.0.0.1, not 0.0.0.0. A Node and Express application can finish its setup like this:{"ok":true}. It should fail when run from another machine because port 8787 is not public.Store the signing secret outside the application
ALLOWED_DISCORD_USER_IDS is only the example’s temporary global allowlist. Replace it with resource-level authorization in your handler. Never commit this file, paste its contents into support messages, or include it in application logs. If your application needs other credentials, store them in the same protected secret system or your provider’s secret manager.Run the executor as a service
package.json, its lockfile, and server.mjs in /opt/customer-actions, then install production dependencies with npm ci --omit=dev. Create /etc/systemd/system/customer-actions.service:StateDirectory=customer-actions creates /var/lib/customer-actions and keeps that path writable even with ProtectSystem=strict. Store the durable idempotency database there. Update ExecStart if your entry file uses a different path. Then enable the service:Point a hostname at the VPS
A record with your DNS provider:actions.example.com. Add an AAAA record only when the VPS has working public IPv6 and its firewall is configured for IPv6.Check DNS from a machine outside the VPS:Install and configure Caddy
/etc/caddy/Caddyfile with:80 and 443 reach Caddy. See the Caddy HTTPS reverse proxy guide for the underlying behavior.Configure the firewall
8787:Verify public HTTPS
404:customer_action.test event.Connect the endpoint in Suppio
/etc/customer-actions.env, restart the executor, and then select Test connection:Option 2: Cloudflare Tunnel
Cloudflare Tunnel is useful when you do not want to publish the VPS origin IP or open inbound ports80 and 443. The cloudflared service makes an outbound connection from your server to Cloudflare and forwards the public hostname to 127.0.0.1:8787.
You need a domain managed in Cloudflare and the executor already running on loopback.
Create a production tunnel
Publish the executor hostname
https://actions.example.com/suppio/actions.Keep the action path machine-accessible
/suppio/actions. If you use WAF or bot rules, create a narrow exception that allows POST requests to the action path to reach your executor. The executor must still verify every Suppio signature.Check tunnel connectivity
cloudflared uses outbound connections. If your egress firewall is restrictive, review Cloudflare’s connectivity pre-checks.Test and enable the endpoint
https://actions.example.com/suppio/actions in Suppio, install the displayed signing secret in your executor, select Test connection, and then turn on the endpoint.Option 3: Managed application hosting
A managed container, application, or serverless platform can work when it provides a stable public HTTPS hostname. Configure the platform so that:- the action route accepts direct HTTPS
POSTrequests without a browser login - the signing secret is stored in the platform’s secret manager, not in source code
- your framework exposes the original raw request body before JSON parsing
- the platform does not redirect the configured action path
- cold starts and action execution finish within 15 seconds
- the application accepts request bodies up to 64 KB
- the response body stays below 64 KB
- the application has a persistent database or durable store for
action_idandevent_iddeduplication - logs exclude signature headers, secrets, raw request bodies, arguments, and result data
JSON.parse.
Use the exact HTTPS URL shown by the platform, including the final action path. Custom domains are optional, but the URL must remain stable.
The complete request lifecycle
For every request, verify the shared envelope first:- Receive the HTTPS request at the exact configured path.
- Limit how much request data the server accepts.
- Read the timestamp and signature headers.
- Reject a timestamp more than five minutes away from the executor’s clock.
- Calculate the HMAC over the exact raw body and compare it in constant time.
- Parse JSON only after the signature succeeds.
customer_action.execute and customer_action.test:
- Confirm that
X-Suppio-Action-Idmatchesaction_id. - Return success without mutating data when
eventiscustomer_action.test. - Reject action names that are not in an explicit handler allowlist.
- Validate every argument again in your application.
- Authorize
actor.user_idfor the targeted account or resource. - Atomically claim
action_idin durable storage. Return the stored result if that ID already completed, and never repeat a result whose completion is uncertain. - Perform the action once.
- Store and return a
succeededorfailedJSON result within 15 seconds. Return both known outcomes with HTTP2xx.
ticket.resolved:
- Confirm that
X-Suppio-Event-Idmatchesevent_id. - Validate
source,actor,conversation, andagent. - Atomically store
event_idand the work your integration must perform. If the same ID arrives again, acknowledge it without creating duplicate work. - Return any
2xxresponse after the event is durably accepted. Suppio ignores the body.
408, HTTP 429, and 5xx responses. Your event_id claim prevents those retries from applying the same close operation twice.
The signature proves that Suppio sent an unchanged request. It does not prove that the Discord user is authorized to modify a record in your application. Your executor remains responsible for that authorization decision.
Security checklist
- Bind the executor to
127.0.0.1when a reverse proxy or tunnel runs on the same machine. - Run the executor as a dedicated non-root user.
- Keep the signing secret in a protected environment file or secret manager.
- Verify signatures against raw bytes before parsing JSON.
- Keep the server clock synchronized so timestamp checks are reliable.
- Persist
action_idandevent_idvalues before applying side effects. - Use explicit action handlers instead of dynamic code, commands, URLs, SQL, or unrestricted operation names.
- Validate and authorize arguments even though the action has a strict schema.
- Do not log secrets, signature headers, request bodies, action arguments, or executor result data.
- Rotate the signing secret immediately if it may have been exposed.
- Keep the operating system, runtime, reverse proxy, tunnel client, and dependencies updated.
Troubleshooting
Suppio rejects the endpoint URL
Suppio rejects the endpoint URL
https://. Suppio rejects IP-address URLs, localhost, loopback addresses, private network addresses, and hostnames whose DNS includes a private or reserved address.DNS does not resolve
DNS does not resolve
A or AAAA record with dig +short actions.example.com. Wait for the DNS record’s time to live after a change. Remove an incorrect AAAA record if the server does not have working public IPv6.HTTPS certificate creation fails
HTTPS certificate creation fails
80 and 443 reach Caddy, no other service occupies those ports, and the domain’s certificate authority authorization records permit a public certificate.The public URL returns 502
The public URL returns 502
curl http://127.0.0.1:8787/health on the server, inspect the executor service, and confirm the configured local port matches.The endpoint returns a redirect
The endpoint returns a redirect
Signature verification fails
Signature verification fails
Cloudflare returns a challenge or 403
Cloudflare returns a challenge or 403
The test works but actions are not available
The test works but actions are not available
Test connection succeeds but ticket closing returns HTTP 400
Test connection succeeds but ticket closing returns HTTP 400
customer_action.test, not ticket.resolved.
Make sure your route handles ticket.resolved before it requires
action_id or X-Suppio-Action-Id. Validate
X-Suppio-Event-Id against event_id, accept the documented nullable
fields, and return any 2xx response after durably storing the event. Run
the two-envelope smoke test in the
complete executor example
to verify both branches.Suppio reports an unknown result
Suppio reports an unknown result
action_id; do not repeat it automatically.Go-live checklist
Before enabling production actions, confirm that:- the public URL uses a valid HTTPS certificate
- the URL returns no redirect
- the executor listens only on loopback or through the intended private network
- the action path has no interactive access challenge
- the signing secret is stored outside source control
- signature and timestamp checks run before parsing, and the matching action ID or event ID is validated afterward
- every action name maps to one explicit handler
- durable idempotency is working
- known failures return HTTP
2xxwith structured failure results - the executor responds in less than 15 seconds
- Test connection succeeds
- the signed
ticket.resolvedsmoke test returns2xxwhen you use ticket closing notifications - the endpoint and intended actions are enabled
