Push queue deliveries into Express or Fastify: the URL is the credential
Infrai pushes queued jobs to any HTTPS URL you register, and there's no unsubscribe route. What that means for authenticating deliveries, plus two Node receivers that do it right.
Registering a push subscription on an Infrai queue takes one call: POST /v1/queue/push_subscribe/{queue} with an https:// URL, and from then on messages arrive at your Express or Fastify handler as POST bodies instead of waiting for a poll. The security question is narrower than it looks, because there is no header you get to choose. Whatever authenticates the delivery has to be reachable from the URL itself, or from the optional secret.
And the queue namespace publishes no route that removes a subscription — so the URL you register is, for practical purposes, permanent. Design around that before you send the call, not after.
What “securely” means here versus on Pub/Sub
Google’s push subscriptions attach an OIDC token minted for a service account; your handler verifies the JWT against Google’s keys and knows who called. Cloud Tasks does the same for HTTP targets. That model gives you rotation for free — revoke the service account and the deliveries stop authenticating.
Infrai’s model is simpler and gives you less. You supply a secret at subscribe time, which is never returned in any response, and you supply the URL. That’s the whole authentication surface.
| Sender | What proves the call is real | Rotate without touching the receiver? |
|---|---|---|
| Pub/Sub push | OIDC JWT verified against Google JWKS | Yes — rotate the service account |
| Cloud Tasks HTTP target | OIDC or OAuth token on the request | Yes |
Infrai push_subscribe | secret set at subscribe time, plus an unguessable URL | No route to change either |
Infrai queue.consume (pull) | Your outbound key; no inbound surface at all | Yes — rotate the API key |
Read the bottom two rows together. If revocation matters more to you than latency, polling isn’t the lesser option — it’s the one with a rotation story.
Subscribing, once
curl -X POST https://api.infrai.cc/v1/queue/push_subscribe/push-jobs-secure \
-H "Authorization: Bearer $INFRAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://worker.example.com/hooks/q/8f2c1d6b5a094e7f",
"secret": "generate-32-bytes-and-store-it-in-your-vault",
"max_retries": 3,
"visibility_timeout": 60,
"dead_letter_queue": "push-jobs-secure.dlq"
}'
The subscription handle that comes back is small:
{
"ok": true,
"data": {
"subscription_id": "sub_9dK2mQvT4bXsR7yLpZ1a",
"queue": "push-jobs-secure",
"account_id": "acct_email_77c768e42148275b",
"concurrency": 1,
"max_retries": 3,
"dead_letter_queue": "push-jobs-secure.dlq",
"active": true,
"started_at": "2026-07-26T05:46:19.291291Z",
"stopped_at": null
}
}
There’s a stopped_at in that schema and no documented route that sets it. Treat subscriptions as append-only: in our testing, deliveries went to the first endpoint registered against a queue, so subscribing a second URL is not a way to move traffic. The only lever the API actually gives you is DELETE /v1/queue/delete/{queue} — destroy the queue, create it again, subscribe the new URL. Plan a queue name you’re willing to burn.
One rule that follows directly, and it’s the reason this article shows a request rather than a live result: never point a push subscription at a webhook-catcher service you don’t own. The registration can’t be undone, and every future message on that queue — payloads, account id, whatever you put in them — goes to a third party forever.
The Express receiver
The credential lives in the path segment. Compare it in constant time, cap the body, answer fast, and make the work idempotent on message_id — push delivery is at-least-once, same as pull.
import express from "express";
import { timingSafeEqual } from "node:crypto";
const app = express();
const TOKEN = process.env.PUSH_PATH_TOKEN;
if (!TOKEN) throw new Error("set PUSH_PATH_TOKEN");
const seen = new Set(); // swap for Redis or a UNIQUE column in real use
function tokenOk(given) {
const a = Buffer.from(String(given ?? ""));
const b = Buffer.from(TOKEN);
return a.length === b.length && timingSafeEqual(a, b);
}
app.post("/hooks/q/:token", express.json({ limit: "512kb" }), async (req, res) => {
if (!tokenOk(req.params.token)) return res.status(404).end();
const { message_id: messageId, payload } = req.body ?? {};
if (!messageId) return res.status(400).json({ error: "no message_id" });
if (seen.has(messageId)) return res.status(200).json({ duplicate: true });
seen.add(messageId);
res.status(200).json({ received: messageId });
try {
await handleJob(payload);
} catch (err) {
seen.delete(messageId);
console.error("job failed", messageId, String(err.message ?? err));
}
});
async function handleJob(payload) {
console.log("working", payload);
}
app.listen(8080, () => console.log("push receiver on :8080"));
Answering 404 rather than 401 on a bad token is deliberate — an unguessable path that returns 401 confirms it’s a real endpoint to anyone scanning.
The res.status(200) before the work is the part people argue about. Push delivery treats a non-2xx as a failure and retries, and max_receive_count on an Infrai queue is fixed at 3, so a job that takes longer than the visibility timeout gets redelivered while you’re still working on it. Acknowledge receipt, then work, and let your own retry logic own the outcome. If instead you want the queue to own retries, hold the response until the work finishes and keep the job well under visibility_timeout.
The Fastify version
Same rules, less ceremony, and a body limit set at the server rather than per route:
import Fastify from "fastify";
import { timingSafeEqual } from "node:crypto";
const TOKEN = process.env.PUSH_PATH_TOKEN;
if (!TOKEN) throw new Error("set PUSH_PATH_TOKEN");
const app = Fastify({ bodyLimit: 512 * 1024, logger: true });
const seen = new Set();
function tokenOk(given) {
const a = Buffer.from(String(given ?? ""));
const b = Buffer.from(TOKEN);
return a.length === b.length && timingSafeEqual(a, b);
}
app.post("/hooks/q/:token", async (request, reply) => {
if (!tokenOk(request.params.token)) return reply.code(404).send();
const { message_id: messageId, payload } = request.body ?? {};
if (!messageId) return reply.code(400).send({ error: "no message_id" });
if (seen.has(messageId)) return reply.code(200).send({ duplicate: true });
seen.add(messageId);
reply.code(200).send({ received: messageId });
try {
await Promise.resolve(payload);
} catch (err) {
seen.delete(messageId);
request.log.error({ messageId, err: String(err) }, "job failed");
}
});
await app.listen({ port: 8080, host: "0.0.0.0" });
Keep the pull path alive
Even with push working, keep a consumer you can run by hand. It’s how you drain a queue whose subscription has drifted, and it needs no inbound surface at all — useful when the worker sits behind a VPN or on a laptop.
const BASE = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("set INFRAI_API_KEY (your_infrai_api_key)");
const H = { authorization: `Bearer ${KEY}`, "content-type": "application/json" };
async function post(path, body) {
const r = await fetch(`${BASE}${path}`, { method: "POST", headers: H, body: JSON.stringify(body) });
if (!r.ok) throw new Error(`${path} → HTTP ${r.status}`);
return (await r.json()).data;
}
const { items } = await post("/v1/queue/consume", { queue: "push-jobs-secure", max_messages: 10 });
console.log(`claimed ${items.length}`);
for (const msg of items) {
const { acked } = await post("/v1/queue/ack", { queue: "push-jobs-secure", message_id: msg.message_id });
if (!acked) console.warn("still in flight:", msg.message_id);
}
Check that boolean. Acking an id the broker doesn’t hold returns HTTP 200 with "acked": false, and nacking a message that isn’t in flight behaves the same way — the status code alone tells you nothing.
Confirm the queue’s own settings any time:
curl -s https://api.infrai.cc/v1/queue/get/push-jobs-secure \
-H "Authorization: Bearer $INFRAI_API_KEY"
visibility_timeout_default and max_receive_count in that response are the two numbers that decide whether your handler’s runtime is safe.
Cost and the honest boundary
Receiving costs nothing: consume, ack, nack, get and stats are all free and rate-limited. Publishing is billable at $0.00002 per message, verified 2026-07-26, with $2 of free credit on a new account. Rates trend downward and campaigns run, so pull today’s number instead of quoting this line:
curl -s "https://api.infrai.cc/v1/discovery" -H "Authorization: Bearer $INFRAI_API_KEY"
The limitation to weigh: no unsubscribe route, one active endpoint per queue, and no verifiable identity token on the delivery. If your compliance story needs cryptographic proof of sender identity per request, Pub/Sub push with OIDC is a better fit and you’d be better off there. If your workers already run against Redis, BullMQ gives you in-process handlers with no public endpoint to defend at all, and on AWS the equivalent is SQS with a Lambda trigger — also no inbound URL of your own to protect. What you get here instead is that the same key already reaches storage, email and error capture, so the job that just arrived can write its artefact and report its own failure without a second vendor.