Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:api.github.com)",
"WebFetch(domain:tanstack.com)",
"WebFetch(domain:www.npmjs.com)"
"WebFetch(domain:www.npmjs.com)",
"Bash(gh api *)"
],
"deny": []
}
Expand Down
84 changes: 84 additions & 0 deletions packages/create/src/frameworks/react/add-ons/inngest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
## Inngest

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.

### Run it locally

Inngest needs two processes: your app and the Inngest Dev Server.

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.**

```bash
# Terminal 1 — your app
npm run dev # or INNGEST_DEV=1 npm run dev

# Terminal 2 — Inngest Dev Server (UI at http://localhost:8288)
npx inngest-cli@latest dev
```

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).

### Where things live

| File | What it does |
| ----------------------------- | ----------------------------------------------------------------------- |
| `src/inngest/client.ts` | Shared `inngest` client |
| `src/inngest/functions.ts` | Events + functions — ships with a `helloWorld` event and `helloWorldFn` |
| `src/routes/api/inngest.ts` | The `serve()` handler mounted at `/api/inngest` (GET/POST/PUT) |
| `src/routes/demo/inngest.tsx` | Demo page at `/demo/inngest` that sends the sample event |
| `.env.local` | Commented `INNGEST_DEV=1` and placeholders for production keys |

### Add your own function

1. Define an event type and function in `src/inngest/functions.ts`:

```ts
export const userSignedUp = eventType('app/user.signed-up', {
schema: staticSchema<{ userId: string }>(),
})

export const userSignedUpFn = inngest.createFunction(
{ id: 'user-signed-up', triggers: [userSignedUp] },
async ({ event, step }) => {
await step.run('send-welcome-email', async () => {
// event.data.userId is typed
})
},
)
```

2. Register it in `src/routes/api/inngest.ts`:

```ts
const handler = serve({
client: inngest,
functions: [helloWorldFn, userSignedUpFn],
})
```

3. Send the event from anywhere in your app:

```ts
await inngest.send(userSignedUp.create({ userId: '123' }))
```

`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).

### Deploying to production

1. [Sync your app](https://www.inngest.com/docs/apps/cloud) from the Inngest dashboard, pointing it at `https://your-domain.com/api/inngest`.
2. Set these environment variables in your deployment:
- `INNGEST_EVENT_KEY` — authorizes `inngest.send()` to deliver events
- `INNGEST_SIGNING_KEY` — lets the `serve()` handler verify requests from Inngest
3. Make sure `INNGEST_DEV` is **not** set.

> 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).

See [Inngest's deploy guides](https://www.inngest.com/docs/platform/deployment) for platform specifics.

### Learn more

- [Writing functions](https://www.inngest.com/docs/functions)
- [Steps & flow control](https://www.inngest.com/docs/features/inngest-functions/steps-workflows)
- [Events & triggers](https://www.inngest.com/docs/features/events-triggers)
- [TanStack Start quick start](https://www.inngest.com/docs/getting-started/tanstack-start-quick-start)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Inngest
#
# In development, uncomment INNGEST_DEV=1 so the SDK connects to the local
# Inngest Dev Server (npx inngest-cli@latest dev on localhost:8288) instead
# of defaulting to cloud mode and trying to authenticate with Inngest Cloud.
# See the add-on README for details.
# INNGEST_DEV=1
#
# Production — set these in your deployment environment, not here.
# INNGEST_EVENT_KEY=
# INNGEST_SIGNING_KEY=
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Inngest } from 'inngest'

export const inngest = new Inngest({
id: '<%= projectName %>',
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { eventType, staticSchema } from 'inngest'
import { inngest } from './client'

export const helloWorld = eventType('demo/hello.world', {
schema: staticSchema<{ name: string }>(),
})

export const helloWorldFn = inngest.createFunction(
{ id: 'hello-world', triggers: [helloWorld] },
async ({ event, step }) => {
await step.sleep('wait-a-moment', '1s')
return { message: `Hello ${event.data.name}!` }
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createFileRoute } from '@tanstack/react-router'
import { serve } from 'inngest/edge'
import { inngest } from '../../inngest/client'
import { helloWorldFn } from '../../inngest/functions'

const handler = serve({
client: inngest,
functions: [helloWorldFn],
})

export const Route = createFileRoute('/api/inngest')({
server: {
handlers: {
GET: ({ request }) => handler(request),
POST: ({ request }) => handler(request),
PUT: ({ request }) => handler(request),
},
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { useState } from 'react'
import { inngest } from '../../inngest/client'
import { helloWorld } from '../../inngest/functions'

const sendHelloEvent = createServerFn({ method: 'POST' })
.inputValidator((name: unknown) => {
if (typeof name !== 'string') throw new Error('Name must be a string')
const trimmed = name.trim()
if (!trimmed) throw new Error('Name is required')
return trimmed
})
.handler(async ({ data: name }) => {
const { ids } = await inngest.send(helloWorld.create({ name }))
return { eventId: ids[0] }
})
Comment thread
Linell marked this conversation as resolved.

export const Route = createFileRoute('/demo/inngest')({
component: RouteComponent,
})

function RouteComponent() {
const [name, setName] = useState('world')
const [result, setResult] = useState<string | null>(null)
const [sending, setSending] = useState(false)
const [error, setError] = useState<string | null>(null)

const onSend = async () => {
setSending(true)
setError(null)
setResult(null)
try {
const { eventId } = await sendHelloEvent({ data: name })
setResult(eventId)
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error')
} finally {
setSending(false)
}
}

return (
<main className="page-wrap px-4 pb-12 pt-14">
<section className="island-shell rise-in relative overflow-hidden rounded-[2rem] px-6 py-10 sm:px-10 sm:py-12">
<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%)]" />
<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%)]" />
<p className="island-kicker mb-3">Inngest Demo</p>
<h1 className="display-title mb-4 text-4xl leading-[1.05] font-bold tracking-tight text-[var(--sea-ink)] sm:text-5xl">
Send an event, trigger a durable function.
</h1>
<p className="mb-8 max-w-2xl text-base text-[var(--sea-ink-soft)] sm:text-lg">
This page dispatches the <code>demo/hello.world</code> event. The
Inngest Dev Server picks it up and runs <code>helloWorld</code>,
which sleeps briefly and returns a greeting.
</p>

<div className="max-w-xl space-y-4">
<label className="block">
<span className="text-sm font-medium text-[var(--sea-ink-soft)]">
Name
</span>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
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)]"
/>
</label>

<button
type="button"
onClick={onSend}
disabled={sending}
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"
>
{sending ? 'Sending…' : 'Send demo/hello.world'}
</button>

{result && (
<div className="rounded-xl border border-[var(--line)] bg-[var(--surface)] p-4">
<p className="island-kicker mb-1">Event sent</p>
<code className="break-all text-sm text-[var(--sea-ink)]">
{result}
</code>
</div>
)}

{error && (
<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]">
{error}
</div>
)}
</div>
</section>

<section className="island-shell mt-8 rounded-2xl p-6">
<p className="island-kicker mb-2">Quick Start</p>
<ul className="m-0 list-disc space-y-2 pl-5 text-sm text-[var(--sea-ink-soft)]">
<li>
Uncomment <code>INNGEST_DEV=1</code> in <code>.env.local</code>{' '}
during development — without it, the SDK defaults to cloud mode and
will try to authenticate against Inngest Cloud. You can also use `INNGEST_DEV=1 pnpm dev` to start the application.
</li>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<li>
Run the Dev Server in a second terminal:{' '}
<code>npx inngest-cli@latest dev</code>.
</li>
<li>
View traces and runs at{' '}
<a
href="http://localhost:8288"
target="_blank"
rel="noopener noreferrer"
>
localhost:8288
</a>
.
</li>
</ul>
</section>
</main>
)
}
19 changes: 19 additions & 0 deletions packages/create/src/frameworks/react/add-ons/inngest/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "Inngest",
"description": "Add Inngest for durable workflows, background jobs, and AI agents.",
"phase": "add-on",
"modes": ["file-router"],
"type": "add-on",
"category": "workflows",
"color": "#2C9B63",
"priority": 140,
"link": "https://www.inngest.com",
"routes": [
{
"url": "/demo/inngest",
"name": "Inngest",
"path": "src/routes/demo/inngest.tsx",
"jsName": "InngestDemo"
Comment on lines +15 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how routes[].jsName is consumed and whether the target symbol is exported.

set -euo pipefail

echo "1) Find jsName consumers:"
rg -n -C3 '\bjsName\b'

echo
echo "2) Check declared jsName in this add-on:"
sed -n '1,120p' packages/create/src/frameworks/react/add-ons/inngest/info.json

echo
echo "3) Check exports in target route file:"
rg -n -C2 'export\s+(const|function|class)\s+' \
  packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/demo/inngest.tsx

Repository: TanStack/cli

Length of output: 50368


🏁 Script executed:

cat packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/demo/inngest.tsx

Repository: TanStack/cli

Length of output: 4858


jsName "InngestDemo" does not match the exported symbol "Route" in the target file.

The route file exports Route, but jsName is set to "InngestDemo". When templates are generated, this will produce import InngestDemo from '...' which will fail because that export does not exist.

💡 Proposed fix
-      "jsName": "InngestDemo"
+      "jsName": "Route"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"path": "src/routes/demo/inngest.tsx",
"jsName": "InngestDemo"
"path": "src/routes/demo/inngest.tsx",
"jsName": "Route"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/create/src/frameworks/react/add-ons/inngest/info.json` around lines
15 - 16, jsName is set to "InngestDemo" but the target file exports the symbol
"Route", causing import failures; update jsName to match the exported symbol or
change the export to match jsName. Fix by either setting jsName to "Route" in
the info.json entry (so generated imports use the existing exported symbol
Route) or alter the route module to export default InngestDemo (so the current
jsName is valid); refer to the jsName property and the exported symbol Route
when making the change.

}
]
}
3 changes: 3 additions & 0 deletions packages/create/src/frameworks/react/add-ons/inngest/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"inngest": "^4.2.4"
}
}
Loading