-
-
Notifications
You must be signed in to change notification settings - Fork 168
feat: add Inngest framework #440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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] } | ||
| }) | ||
|
|
||
| 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> | ||
|
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> | ||
| ) | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.tsxRepository: TanStack/cli Length of output: 50368 🏁 Script executed: cat packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/demo/inngest.tsxRepository: TanStack/cli Length of output: 4858
The route file exports 💡 Proposed fix- "jsName": "InngestDemo"
+ "jsName": "Route"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| } | ||||||||||
| ] | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "dependencies": { | ||
| "inngest": "^4.2.4" | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.