Reference
.NET producer: Queuey.Client
The .NET package a producer publishes with: one client, one PublishAsync per event, each request signed for you. It is what queuey advise means by "Add Queuey.Client", and it also verifies the deliveries Queuey makes to you. Where the producer has local disk that survives a restart, use Queuey Edge instead. Outside .NET there is no package: publishing is one POST to the ingress, as in the quickstart.
Install
dotnet add package Queuey.Client --prereleaseEvery version on NuGet is a preview for now, so --prerelease is needed: without it, dotnet add package finds no version. The package targets netstandard2.0 and net8.0.
--write user-secrets, credentials generate, UseSettings, .env in Development and QueueyDeliveryVerifier.FromEnvironment() are not in 0.1.0-preview.8.The key a producer publishes with
A producer publishes with a signing key: a key id and a secret, QUEUEY_SIGNING_KEY_ID and QUEUEY_SIGNING_SECRET. The client signs every request with them (the X-Queuey signature headers, see Signed requests), so the secret itself is never sent. Make it with a queuey login, straight into the place the app reads it:
queuey keys mint --write user-secrets # a .NET project with a UserSecretsId
queuey keys mint --write .env # otherwise: a file git ignores- The key reaches every queue in the workspace, also queues made later.
--queue <queue>makes one for a single queue. - The secret is never printed, and the value in the app is the value Queuey holds.
user-secretsneeds aUserSecretsId(dotnet user-secrets initadds one); a.envin a git repository must be one git ignores. - Without
--writenothing is minted: the command names the variables the app needs, the--writethat fits the folder, and the console page where a person makes or looks at the key.--show-secretprints the secret once instead, and warns. - In a prod workspace — and a workspace without an environment counts as prod — a login gets
approval_required: the command exits 5 with the link where a person makes the key, and nothing is minted. --type api-keygivesQUEUEY_API_KEYinstead, a key that can only publish there.
Ask for the signature on the workspace, and every queue in it inherits that. In queuey.deploy.json: "workspace": { "ingress": { "authMode": "SignedRequest", "signedRequest": { "template": "queuey" } } }, which the file queuey advise --write-files writes already has. Until the workspace or the queue asks for something, a new queue is open: it takes events from anyone who knows its address.
A license-wide API key (qak_…) is not the producer's. It belongs to a person, who mints it in the console (the license's Home page → API keys) to operate Queuey. When ApiKey is set, the client publishes with it instead of the signing key, so leave it out of the producer.
Configure QueueyOptions
using Queuey.Client;
// Program.cs. builder.Configuration holds user secrets (in Development) and environment variables.
builder.Services.AddQueueyClient(o =>
{
o.TenantPublicId = "ten_…";
o.UseSettings(key => builder.Configuration[key]); // QUEUEY_SIGNING_KEY_ID, QUEUEY_SIGNING_SECRET
// No ApiKey: with one set, the client publishes with it instead of the signing key.
});
// Then take QueueyClient from DI.UseSettings fills each setting that is not set in code from the QUEUEY_* name you pass it, from .NET configuration: user secrets in Development, environment variables everywhere. Or read the environment directly:
// The environment, and in Development the signing key from .env as well.
builder.Services.AddQueueyClient(o =>
{
o.TenantPublicId = "ten_…";
o.UseEnvironmentVariables();
});UseEnvironmentVariables() reads QUEUEY_SIGNING_KEY_ID, QUEUEY_SIGNING_SECRET, QUEUEY_DELIVERY_SECRET, QUEUEY_TENANT, QUEUEY_LICENSE, QUEUEY_API_BASE and QUEUEY_INGRESS_BASE from the environment. In Development (DOTNET_ENVIRONMENT or ASPNETCORE_ENVIRONMENT) it also reads the signing key, as a pair, and QUEUEY_DELIVERY_SECRET from .env in the working folder, and nothing else: only from a regular file of your own that git does not track, and the environment always wins. Outside Development it never reads .env: in production the values come from environment variables. It reads QUEUEY_API_KEY only when the signing key and its secret are not both set, so a key left in the environment never wins over the signing key.
.env is read only in Development, and .NET loads user secrets only there too. dotnet run without a launch profile runs as Production, and then reads neither. Set the environment when you run it:ASPNETCORE_ENVIRONMENT=Development dotnet run # ASP.NET Core
DOTNET_ENVIRONMENT=Development dotnet run # a worker or console hostdotnet run uses by default:{
"profiles": {
"dev": {
"commandName": "Project",
"environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }
}
}
}The client talks to https://ingress.queuey.ai to publish and https://api.queuey.ai for the control plane. For a local or self-hosted Queuey, set both hosts:
builder.Services.AddQueueyClient(o =>
{
// A local or self-hosted Queuey. Leave both out for queuey.ai.
o.ApiBaseAddress = new Uri("http://localhost:5223");
o.IngressBaseAddress = new Uri("http://localhost:5084");
// …the tenant and the signing key as above
});Publish with an event type
public sealed class OrderEvents(QueueyClient queuey)
{
public async Task OrderCreated(Order order)
{
PublishResult result = await queuey.Ingress.PublishAsync("orders", order, new PublishOptions
{
EventType = "order.created", // X-Queuey-Event-Type
GroupKey = order.CustomerId, // X-Queuey-Group-Key: the lane, for per-key ordering
IdempotencyKey = order.Id, // Idempotency-Key: a retried call is stored once
});
// result.EventId (evt_…) once Queuey has stored it; result.Replayed on a repeat.
}
}PublishAsync<T> sends the payload as JSON (camelCase); the byte[] overload sends bytes as they are. It returns once Queuey has stored the event, with its EventId, QueuePublicId, ReceivedAtUtc, Mode and Replayed. A refused publish throws a QueueyException with the HTTP status and Queuey's error code (ErrorCode); the codes are in Headers & status codes.
EventType is sent as X-Queuey-Event-Type, which every queue reads unless its ingress takes the event type from somewhere else, such as a body field.
Verify what Queuey delivers to you
A receiver checks the signature on every delivery, with a secret that is the same value in Queuey and in the receiver. Make it once:
queuey credentials generate orders-signing --write user-secrets # or --write .envIt stores a random value in Queuey as the credential deliveries are signed with, and writes the same value as QUEUEY_DELIVERY_SECRET, never showing it. Point the queue's delivery at it in queuey.deploy.json, then apply: "delivery": { "signing": { "enabled": true, "templateKey": "queuey", "credentialRef": "orders-signing" } }. Write templateKey out: queuey is Queuey's own signature, the one QueueyDeliveryVerifier checks. The only other value is stripe, for a queue whose ingress verifies Stripe and passes Stripe's signature on (see Stripe webhooks); it signs with the ingress secret and takes no credentialRef. A name Queuey already holds is refused; --replace makes a new value for every queue that uses the name, so it is a person's decision.
using System.Collections.Concurrent;
// QUEUEY_DELIVERY_SECRET from the environment, or in Development from .env.
var seenNonces = new ConcurrentDictionary<string, byte>(); // keep them a little over five minutes in a real app
var verifier = QueueyDeliveryVerifier.FromEnvironment(new QueueyDeliveryVerifierOptions
{
NonceAlreadySeen = nonce => !seenNonces.TryAdd(nonce, 0),
});
// From user secrets instead:
// var verifier = new QueueyDeliveryVerifier(
// new QueueyOptions().UseSettings(key => builder.Configuration[key]).DeliverySecret!,
// new QueueyDeliveryVerifierOptions { NonceAlreadySeen = nonce => !seenNonces.TryAdd(nonce, 0) });
app.MapPost("/webhooks/queuey", async (HttpRequest request) =>
{
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer); // the raw bytes, before anything deserializes them
var body = buffer.ToArray();
var result = verifier.Verify(
request.Method,
new Uri($"https://{request.Host}{request.Path}{request.QueryString}"),
name => request.Headers[name],
body);
if (!result.IsValid) return Results.Unauthorized();
await Handle(body, result.EventId); // deduplicate on the event id (below)
return Results.Ok();
});What a receiver checks
- The signing headers must be there:
X-Queuey-Key-Id,X-Queuey-Timestamp,X-Queuey-Nonce,X-Queuey-Content-SHA256, and the signature —X-Queuey-Signaturesfor v2,X-Queuey-Signaturefor v1. - The timestamp must be within five minutes of your clock.
- The SHA-256 of the raw body must equal
X-Queuey-Content-SHA256. The signature covers that header, not the bytes, so this check is what ties the signature to the body. - v1, in
X-Queuey-Signatureas hex: an HMAC-SHA256 with your secret over six lines,METHOD\nPATH\nQUERY\nTIMESTAMP\nNONCE\nCONTENT_SHA256. PATH is percent-decoded as ASP.NET Core reads a request path, except%2F, which stays encoded:/hooks/caf%C3%A9/order%2F1/a%20bis signed as/hooks/café/order%2F1/a b. QUERY is normalized: each key and value decoded, sorted, and re-encoded. - v2, in
X-Queuey-Signaturesasv2=<hex>(a comma-separated list, one entry per version): the same HMAC over eight lines, the six of v1 and thenEVENT_ID(X-Queuey-Event-Id) andIDEMPOTENCY_KEY(Idempotency-Key), each trimmed, and empty when the delivery has none. A delivery carries both versions, over the same timestamp, nonce and body hash. - With a nonce guard (
NonceAlreadySeenin .NET), a nonce seen before is rejected. Without one, the five-minute window is the only protection against the same delivery sent again.
Deduplicate on the event id or the Idempotency-Key only with v2. A retry or a redelivery of one event carries the same id, each time with a new nonce, so the id is what to deduplicate on. v2 covers both values: a delivery with another event id or key needs a new valid signature, which takes the secret. v1 covers neither, so with v1 alone whoever captured a delivery can send it again within the five minutes under another event id, and it verifies; a nonce guard narrows that, since the capture's nonce is then spent.
The check outside .NET is in Verify deliveries.
Streams: PushEventAsync
A producer that publishes event streams to integration partners uses Queuey.Client.Waas, which adds IQueueyService on top of the same client:
dotnet add package Queuey.Client.Waas --prereleasebuilder.Services.AddQueuey(
o => { /* the same QueueyOptions as above */ },
b => b.AddStream<OrderCreated>());
// IQueueyService from DI. The event type is a required argument; the key is the group key.
await queuey.PushEventAsync("order-events", "order.created", order.CustomerId, order);PushEventAsync publishes with the same signing key. A new stream asks for an API key unless its workspace already asks for a key or a signature, so set the signature on the workspace before you declare the stream, and the stream inherits it. Declaring the streams (SyncStreamsAsync, or queuey sync) changes the workspace, so it needs a queuey login or a person's API key, not the producer's signing key. See the WaaS producer API and Connect.
Queuey.Edge reads the same QUEUEY_SIGNING_KEY_ID and QUEUEY_SIGNING_SECRET by the same rules, and signs each transfer when it sends it, so a workspace that takes signed requests accepts Edge as it accepts this client. Never put a key in a command's arguments, as a literal in Program.cs, or through /tmp. The daemon and its environment file are on the Edge page.Related
- Set up with your AI — the prompt that installs and wires this up for you.
- Signed requests (HMAC) — what the client signs, for a producer outside .NET.
- Queuey Edge — the package for producers that must survive being offline.