Aspire: the Inngest server in your AppHost
Aspire starts the parts of an app on a developer machine, which are projects and containers, and
connects them. Inngest.Aspire.Hosting adds the Inngest server to that: the AppHost runs it as a container, links its
UI in the Aspire dashboard, gives the projects the settings the SDK reads to find it, and tells it where the apps
that serve functions are. There is no docker run and no port to copy between terminals.
It is for local development. It starts the dev server, or optionally a self-hosted server that signs its requests, and nothing here deploys Inngest.
Install
Section titled “Install”Add the package to the AppHost project, not to the apps:
dotnet add package Inngest.Aspire.HostingThe package needs Aspire 13.6.4 or later. It depends on Aspire.Hosting 13.6.4, so an AppHost on an older 13.x is
raised to that version. It targets net8.0 and net10.0, and it has no dependency on the Inngest package, because
the AppHost only has to know the names of the settings, not the SDK. It runs in the AppHost, which is never trimmed or
published as a native binary, so unlike the other packages it is not marked as trim or Native AOT compatible;
Aspire.Hosting is not either.
nuget.org reserves the Aspire. prefix for the Aspire team and its partners, so a community package cannot be called
Aspire.Hosting.Inngest; hence Inngest.Aspire.Hosting, like CommunityToolkit.Aspire.Hosting.*.
Add the server
Section titled “Add the server”AddInngest adds the dev server. WithReference gives a project the settings that make its SDK use that server, and
WithApp tells the server to sync the project, which serves its functions with MapInngest:
var builder = DistributedApplication.CreateBuilder(args);
var inngest = builder.AddInngest("inngest");
var shop = builder.AddProject<Projects.Shop>("shop") .WithReference(inngest) .WaitFor(inngest);
inngest.WithApp(shop);
builder.Build().Run();WithApp builds the address from the endpoint of the project that is named http. A project made from a template has
one from the http profile of its launchSettings.json. Without one the server fails to start with “The endpoint
http is not defined for the resource”: give the project one with WithHttpEndpoint(name: "http"), or pass the name
of another endpoint to WithApp.
Run the AppHost and the dashboard shows two resources. inngest is healthy once the server answers GET /health
and its Connect gateway answers GET /ready, and its row links to the server’s UI as Inngest UI. shop starts
once the server is healthy, because of WaitFor. The server then syncs shop and lists its functions in the UI;
sending an event runs them. The repository has such an AppHost for its sample app, in
samples/Inngest.Sample.AppHost.
What the three calls do:
AddInngestruns theinngest/inngestimage at the tagv1.49.0, the Inngest CLI version the tests of this SDK run against, asinngest dev --no-discovery. Discovery would only scan the container’s own ports. It has two endpoints:http, which is the REST API, the event API and the UI, andconnect, which is the gateway of Connect workers. Aspire proxies both on ports it picks. Change the tag withWithImageTag, and fix the ports with theportandconnectPortarguments.WithReferencesets the environment variables below on the project. It does not make the project wait for the server.WithAppadds the project’s URL to the arguments of the server (-u). The server calls it to sync the functions, and again every 5 seconds, which is the dev server’s polling interval, so a project that starts after the server, or restarts with other functions, is found. Until the first sync the server knows no function of the project, so an event that reaches it by then starts no run.
Do not make the server wait for the project with WaitFor: the project would wait for the server and the server for
the project.
What a project gets
Section titled “What a project gets”WithReference(inngest) sets these variables, which the SDK reads as described in
Configuration:
| Variable | Dev server | Self-hosted server |
|---|---|---|
INNGEST_DEV |
the address of the server, which turns on dev mode and points the SDK at it | 0 |
INNGEST_BASE_URL |
the address of the server | the address of the server |
INNGEST_API_BASE_URL |
the address of the server | the address of the server |
INNGEST_EVENT_API_BASE_URL |
the address of the server | the address of the server |
INNGEST_SIGNING_KEY |
the signing key | |
INNGEST_EVENT_KEY |
the event key | |
INNGEST_CONNECT_GATEWAY_URL |
the address of the gateway | the address of the gateway |
The addresses are worked out when the project starts, for that project. A project on the host gets
http://localhost:<port>, where the port is the one Aspire proxies the server on. A container gets the name of the
server on the container network. The same WithReference is right for both, and nothing in the app has to know about
Aspire: the app is an ordinary Inngest app that reads its settings from the environment.
INNGEST_DEV is set to the address in dev mode so that any SDK that reads the address from it, such as the TypeScript
one, finds the server too; INNGEST_BASE_URL has the same address for SDKs that read only that. The SDK reads
INNGEST_API_BASE_URL and INNGEST_EVENT_API_BASE_URL before INNGEST_BASE_URL, so they are set as well. A
self-hosted server sets INNGEST_DEV to 0 on purpose.
Projects inherit the environment of the AppHost, and the settings above win over the same variables in it, so an
INNGEST_DEV or an INNGEST_API_BASE_URL left in a shell does not send a project elsewhere or switch off the checks
of signatures. Every other INNGEST_* variable of that environment still reaches the projects, and some of them break
a run without saying so. An INNGEST_SERVE_ORIGIN makes the server call an address that is not the project’s, so its
runs cannot be delivered. Unset such a variable before you start the AppHost, or replace it for one project with
WithEnvironment.
How the server reaches an app
Section titled “How the server reaches an app”The server runs in a container, where localhost is the container itself. WithApp does not write an address; it
gives the server a reference to the endpoint of the project, and Aspire resolves that for the server’s container when
it starts:
- A project that runs on the host is reached through the container tunnel that Aspire 13 starts, at an address like
aspire.dev.internal:<port>. The tunnel does not depend onhost.docker.internal, so the integration adds no--add-host=host.docker.internal:host-gateway, which the plain Docker command of Getting started needs on Linux. Where the tunnel is turned off, Aspire falls back to the host name of the container runtime, which ishost.docker.internalon Docker Desktop. The tests of this repository have run on Docker Desktop for Mac only. - An app that is itself a container is reached by its name on the container network.
The SDK registers the address that the server synced it through, so each step of a run calls the app at the same address, and the server needs no other.
WithApp takes the path the app serves Inngest on and the name of its endpoint, /api/inngest and http by default:
var inngest = builder.AddInngest("inngest");
var billing = builder.AddProject<Projects.Billing>("billing") .WithHttpEndpoint(name: "internal") .WithReference(inngest);
inngest.WithApp(billing, "/webhooks/inngest", endpointName: "internal");Call it once for every app that serves functions. An app that only sends events needs no WithApp, and neither does
a Connect worker.
Use an http endpoint. An https endpoint of a project uses the ASP.NET Core development certificate, which the
container does not trust unless you set that up with Aspire’s certificate trust settings.
Connect workers
Section titled “Connect workers”A Connect worker has no endpoint, so there is nothing to register with WithApp.
It only needs WithReference:
builder.AddProject<Projects.Worker>("worker") .WithReference(inngest) .WaitFor(inngest);The worker calls the API of the server to start a connection, and the server answers with the address of its gateway.
That address is made of the host the worker called and the port the gateway has inside the container, and Aspire
publishes the gateway on another one, so a worker that used it would dial a port that nothing on the host listens on.
That is why WithReference sets INNGEST_CONNECT_GATEWAY_URL, which the SDK dials instead. The address is that of
the connect endpoint, resolved for the worker like the others. A worker that is started by other means than this
reference gets the server’s answer and has to set the variable itself.
A self-hosted server
Section titled “A self-hosted server”The dev server checks nothing: it does not sign its requests and accepts any event key. AddInngestSelfHosted runs
inngest start instead, which signs every request to an app with a signing key, wants the key on its REST API and
refuses events that do not carry its event key. A project that references it runs in cloud mode with the same keys, as
it would in production:
var signingKey = builder.AddParameter("inngest-signing-key", secret: true);var eventKey = builder.AddParameter("inngest-event-key", secret: true);
var inngest = builder.AddInngestSelfHosted("inngest", signingKey, eventKey);
var shop = builder.AddProject<Projects.Shop>("shop") .WithReference(inngest) .WaitFor(inngest);
inngest.WithApp(shop);The keys are secret parameters, which Aspire reads from the user secrets or the configuration of the AppHost, or
asks for in the dashboard. The server takes them from INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY in its environment,
so they are not in its command line. inngest start accepts only a signing key that is a hex string of even length,
without a signkey- prefix, such as the output of openssl rand -hex 32. The SDK accepts that key as it is. When the
server starts, the AppHost checks the keys and reports one that inngest start would refuse, instead of leaving you
with a container that stopped.
Leave the keys out and the integration generates them, as Aspire does for the passwords of other containers:
var inngest = builder.AddInngestSelfHosted("inngest");A generated signing key has 64 digits, which are valid hex, and a generated event key is letters and digits. Both are
kept in the user secrets of the AppHost, when it has them, as Parameters:inngest-signing-key and
Parameters:inngest-event-key, so that they stay the same between runs, and a value you put there wins.
Unlike the dev server, the self-hosted server polls for apps only when it is told to, so the integration starts it with a polling interval of 5 seconds.
Keeping the state
Section titled “Keeping the state”The dev server holds its runs and events in memory, and the self-hosted server writes a SQLite database, main.db, to
/.inngest inside its container. Both lose their state when the container is removed, because the integration adds
no volume. Inngest v1.49.0 can keep it: inngest dev --persist writes the dev server’s database to the same
directory, and a volume at that directory survives the container. Pass the flag with WithArgs and add the volume:
var inngest = builder.AddInngest("inngest") .WithArgs("--persist") .WithVolume("inngest-data", "/.inngest");The self-hosted server always writes its database, so it needs only the volume. inngest start also takes
--sqlite-dir <directory> to move the database, and then the volume goes to that directory:
var inngest = builder.AddInngestSelfHosted("inngest") .WithVolume("inngest-data", "/.inngest");WithLifetime(ContainerLifetime.Persistent) keeps the container itself, memory and all, across runs of the AppHost. The
integration starts no Postgres or Redis for the self-hosted server; inngest start takes their addresses as
--postgres-uri and --redis-uri.
In a manifest
Section titled “In a manifest”aspire publish writes a manifest that deployment tools turn into a deployment. What it holds depends on the server:
- The dev server is not in it. It checks no signatures, so a deployment must not run it.
WithReferenceto the dev server adds nothing to a manifest either, becauseINNGEST_DEVwould put the deployed app into dev mode. The app deploys in cloud mode, with the keys and addresses that you give it where it is deployed. - The self-hosted server is in it, as a container resource: the image and tag, the entrypoint and the arguments
(
start --poll-interval 5and a-ufor each app, as an expression of the app’s address), thehttpandconnectendpoints, and the keys as the secret parameters<name>-signing-keyand<name>-event-key, which the deployment asks for or generates. The projects that reference it getINNGEST_DEV=0, the addresses, the keys and the gateway address as references to that resource and those parameters.
The self-hosted server keeps its state inside its container, as it does on your machine, so a deployment that replaces the container loses it, and the manifest has no volume or database for it. Treat the integration as a way to run Inngest while you develop; Deploying covers production.
What it does not do
Section titled “What it does not do”- It keeps no state beyond its container. See Keeping the state.
- It does not deploy Inngest. The manifest has the self-hosted server as described above, but Inngest in production is Inngest Cloud or a server you run and size yourself.
- It cannot be called from an AppHost written in another language. The methods are not exported for Aspire’s polyglot AppHosts.
Testing the AppHost
Section titled “Testing the AppHost”The integration works under Aspire.Hosting.Testing, which is how this repository tests it. Its tests start the dev
server, a small app and a Connect worker, wait for them to be healthy and run a function through the server, and do the
same against the self-hosted server. They need Docker. Other tests write the manifest of both servers and read it.
inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.