Skip to content

Commit 7f6564e

Browse files
committed
feat: add Inngest framework
Add Inngest as a framework selectable for installation during `create`. Adds a new 'workflows' category because Inngest didn't fit cleanly into the others .
1 parent c37953e commit 7f6564e

11 files changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
## Inngest
2+
3+
This add-on wires up [Inngest](https://www.inngest.com) for durable workflows, background jobs, and AI agents — a client, a sample event and function, the `serve()` API route, and an interactive demo page.
4+
5+
### Run it locally
6+
7+
Inngest needs two processes: your app and the Inngest Dev Server.
8+
9+
First, uncomment `INNGEST_DEV=1` in `.env.local`. In v4 the SDK defaults to **cloud mode** and requires a signing key — `INNGEST_DEV=1` points it at your local Dev Server instead. **Don't set it in production.**
10+
11+
```bash
12+
# Terminal 1 — your app
13+
npm run dev # or INNGEST_DEV=1 npm run dev
14+
15+
# Terminal 2 — Inngest Dev Server (UI at http://localhost:8288)
16+
npx inngest-cli@latest dev
17+
```
18+
19+
Open [http://localhost:3000/demo/inngest](http://localhost:3000/demo/inngest), click the button to send an event, and watch it run in the [Dev Server UI](http://localhost:8288).
20+
21+
### Where things live
22+
23+
| File | What it does |
24+
| ----------------------------- | ----------------------------------------------------------------------- |
25+
| `src/inngest/client.ts` | Shared `inngest` client |
26+
| `src/inngest/functions.ts` | Events + functions — ships with a `helloWorld` event and `helloWorldFn` |
27+
| `src/routes/api/inngest.ts` | The `serve()` handler mounted at `/api/inngest` (GET/POST/PUT) |
28+
| `src/routes/demo/inngest.tsx` | Demo page at `/demo/inngest` that sends the sample event |
29+
| `.env.local` | Commented `INNGEST_DEV=1` and placeholders for production keys |
30+
31+
### Add your own function
32+
33+
1. Define an event type and function in `src/inngest/functions.ts`:
34+
35+
```ts
36+
export const userSignedUp = eventType('app/user.signed-up', {
37+
schema: staticSchema<{ userId: string }>(),
38+
})
39+
40+
export const userSignedUpFn = inngest.createFunction(
41+
{ id: 'user-signed-up', triggers: [userSignedUp] },
42+
async ({ event, step }) => {
43+
await step.run('send-welcome-email', async () => {
44+
// event.data.userId is typed
45+
})
46+
},
47+
)
48+
```
49+
50+
2. Register it in `src/routes/api/inngest.ts`:
51+
52+
```ts
53+
const handler = serve({
54+
client: inngest,
55+
functions: [helloWorldFn, userSignedUpFn],
56+
})
57+
```
58+
59+
3. Send the event from anywhere in your app:
60+
61+
```ts
62+
await inngest.send(userSignedUp.create({ userId: '123' }))
63+
```
64+
65+
`eventType()` ties the event name to its payload shape, so `event.data` is inferred in both the function handler and `inngest.send()`. Want runtime validation too? Swap `staticSchema` for a Zod schema — see the [trigger helpers reference](https://www.inngest.com/docs/reference/typescript/v4/functions/triggers).
66+
67+
### Deploying to production
68+
69+
1. [Sync your app](https://www.inngest.com/docs/apps/cloud) from the Inngest dashboard, pointing it at `https://your-domain.com/api/inngest`.
70+
2. Set these environment variables in your deployment:
71+
- `INNGEST_EVENT_KEY` — authorizes `inngest.send()` to deliver events
72+
- `INNGEST_SIGNING_KEY` — lets the `serve()` handler verify requests from Inngest
73+
3. Make sure `INNGEST_DEV` is **not** set.
74+
75+
> Running on serverless (Vercel, Netlify, etc.)? Set `checkpointing: { maxRuntime: '50s' }` on the client (~80% of your platform's max duration) so checkpointed steps don't get cut off mid-run. [More on checkpointing here](https://www.inngest.com/docs/setup/checkpointing).
76+
77+
See [Inngest's deploy guides](https://www.inngest.com/docs/platform/deployment) for platform specifics.
78+
79+
### Learn more
80+
81+
- [Writing functions](https://www.inngest.com/docs/functions)
82+
- [Steps & flow control](https://www.inngest.com/docs/features/inngest-functions/steps-workflows)
83+
- [Events & triggers](https://www.inngest.com/docs/features/events-triggers)
84+
- [TanStack Start quick start](https://www.inngest.com/docs/getting-started/tanstack-start-quick-start)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Inngest
2+
#
3+
# In development, uncomment INNGEST_DEV=1 so the SDK connects to the local
4+
# Inngest Dev Server (npx inngest-cli@latest dev on localhost:8288) instead
5+
# of defaulting to cloud mode and trying to authenticate with Inngest Cloud.
6+
# See the add-on README for details.
7+
# INNGEST_DEV=1
8+
#
9+
# Production — set these in your deployment environment, not here.
10+
# INNGEST_EVENT_KEY=
11+
# INNGEST_SIGNING_KEY=
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Inngest } from 'inngest'
2+
3+
export const inngest = new Inngest({
4+
id: '<%= projectName %>',
5+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { eventType, staticSchema } from 'inngest'
2+
import { inngest } from './client'
3+
4+
export const helloWorld = eventType('demo/hello.world', {
5+
schema: staticSchema<{ name: string }>(),
6+
})
7+
8+
export const helloWorldFn = inngest.createFunction(
9+
{ id: 'hello-world', triggers: [helloWorld] },
10+
async ({ event, step }) => {
11+
await step.sleep('wait-a-moment', '1s')
12+
return { message: `Hello ${event.data.name}!` }
13+
},
14+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { serve } from 'inngest/edge'
3+
import { inngest } from '../../inngest/client'
4+
import { helloWorldFn } from '../../inngest/functions'
5+
6+
const handler = serve({
7+
client: inngest,
8+
functions: [helloWorldFn],
9+
})
10+
11+
export const Route = createFileRoute('/api/inngest')({
12+
server: {
13+
handlers: {
14+
GET: ({ request }) => handler(request),
15+
POST: ({ request }) => handler(request),
16+
PUT: ({ request }) => handler(request),
17+
},
18+
},
19+
})
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { createServerFn } from '@tanstack/react-start'
3+
import { useState } from 'react'
4+
import { inngest } from '../../inngest/client'
5+
import { helloWorld } from '../../inngest/functions'
6+
7+
const sendHelloEvent = createServerFn({ method: 'POST' })
8+
.inputValidator((name: string) => name)
9+
.handler(async ({ data: name }) => {
10+
const { ids } = await inngest.send(helloWorld.create({ name }))
11+
return { eventId: ids[0] }
12+
})
13+
14+
export const Route = createFileRoute('/demo/inngest')({
15+
component: RouteComponent,
16+
})
17+
18+
function RouteComponent() {
19+
const [name, setName] = useState('world')
20+
const [result, setResult] = useState<string | null>(null)
21+
const [sending, setSending] = useState(false)
22+
const [error, setError] = useState<string | null>(null)
23+
24+
const onSend = async () => {
25+
setSending(true)
26+
setError(null)
27+
setResult(null)
28+
try {
29+
const { eventId } = await sendHelloEvent({ data: name })
30+
setResult(eventId)
31+
} catch (e) {
32+
setError(e instanceof Error ? e.message : 'Unknown error')
33+
} finally {
34+
setSending(false)
35+
}
36+
}
37+
38+
return (
39+
<main className="page-wrap px-4 pb-12 pt-14">
40+
<section className="island-shell rise-in relative overflow-hidden rounded-[2rem] px-6 py-10 sm:px-10 sm:py-12">
41+
<div className="pointer-events-none absolute -left-20 -top-24 h-56 w-56 rounded-full bg-[radial-gradient(circle,rgba(79,184,178,0.32),transparent_66%)]" />
42+
<div className="pointer-events-none absolute -bottom-20 -right-20 h-56 w-56 rounded-full bg-[radial-gradient(circle,rgba(47,106,74,0.18),transparent_66%)]" />
43+
<p className="island-kicker mb-3">Inngest Demo</p>
44+
<h1 className="display-title mb-4 text-4xl leading-[1.05] font-bold tracking-tight text-[var(--sea-ink)] sm:text-5xl">
45+
Send an event, trigger a durable function.
46+
</h1>
47+
<p className="mb-8 max-w-2xl text-base text-[var(--sea-ink-soft)] sm:text-lg">
48+
This page dispatches the <code>demo/hello.world</code> event. The
49+
Inngest Dev Server picks it up and runs <code>helloWorld</code>,
50+
which sleeps briefly and returns a greeting.
51+
</p>
52+
53+
<div className="max-w-xl space-y-4">
54+
<label className="block">
55+
<span className="text-sm font-medium text-[var(--sea-ink-soft)]">
56+
Name
57+
</span>
58+
<input
59+
type="text"
60+
value={name}
61+
onChange={(e) => setName(e.target.value)}
62+
className="mt-1 w-full rounded-xl border border-[var(--line)] bg-[var(--surface-strong)] px-4 py-2.5 text-[var(--sea-ink)] outline-none focus:border-[var(--lagoon-deep)]"
63+
/>
64+
</label>
65+
66+
<button
67+
type="button"
68+
onClick={onSend}
69+
disabled={sending}
70+
className="w-full rounded-full border border-[rgba(50,143,151,0.3)] bg-[rgba(79,184,178,0.14)] px-5 py-2.5 text-sm font-semibold text-[var(--lagoon-deep)] transition hover:-translate-y-0.5 hover:bg-[rgba(79,184,178,0.24)] disabled:cursor-not-allowed disabled:opacity-60"
71+
>
72+
{sending ? 'Sending…' : 'Send demo/hello.world'}
73+
</button>
74+
75+
{result && (
76+
<div className="rounded-xl border border-[var(--line)] bg-[var(--surface)] p-4">
77+
<p className="island-kicker mb-1">Event sent</p>
78+
<code className="break-all text-sm text-[var(--sea-ink)]">
79+
{result}
80+
</code>
81+
</div>
82+
)}
83+
84+
{error && (
85+
<div className="rounded-xl border border-[rgba(200,70,70,0.3)] bg-[rgba(200,70,70,0.08)] p-4 text-sm text-[#c04646]">
86+
{error}
87+
</div>
88+
)}
89+
</div>
90+
</section>
91+
92+
<section className="island-shell mt-8 rounded-2xl p-6">
93+
<p className="island-kicker mb-2">Quick Start</p>
94+
<ul className="m-0 list-disc space-y-2 pl-5 text-sm text-[var(--sea-ink-soft)]">
95+
<li>
96+
Uncomment <code>INNGEST_DEV=1</code> in <code>.env.local</code>{' '}
97+
during development — without it, the SDK defaults to cloud mode and
98+
will try to authenticate against Inngest Cloud.You can also use `INNGEST_DEV=1 pnpm dev` to start the application.
99+
</li>
100+
<li>
101+
Run the Dev Server in a second terminal:{' '}
102+
<code>npx inngest-cli@latest dev</code>.
103+
</li>
104+
<li>
105+
View traces and runs at{' '}
106+
<a
107+
href="http://localhost:8288"
108+
target="_blank"
109+
rel="noopener noreferrer"
110+
>
111+
localhost:8288
112+
</a>
113+
.
114+
</li>
115+
</ul>
116+
</section>
117+
</main>
118+
)
119+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "Inngest",
3+
"description": "Add Inngest for durable workflows, background jobs, and AI agents.",
4+
"phase": "add-on",
5+
"modes": ["file-router"],
6+
"type": "add-on",
7+
"category": "workflows",
8+
"color": "#2C9B63",
9+
"priority": 140,
10+
"link": "https://www.inngest.com",
11+
"routes": [
12+
{
13+
"url": "/demo/inngest",
14+
"name": "Inngest",
15+
"path": "src/routes/demo/inngest.tsx",
16+
"jsName": "InngestDemo"
17+
}
18+
]
19+
}
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"inngest": "^4.2.4"
4+
}
5+
}

0 commit comments

Comments
 (0)