diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1192fca8..731a1df1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] } diff --git a/packages/create/src/frameworks/react/add-ons/inngest/README.md b/packages/create/src/frameworks/react/add-ons/inngest/README.md new file mode 100644 index 00000000..42d8f393 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/README.md @@ -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) diff --git a/packages/create/src/frameworks/react/add-ons/inngest/assets/_dot_env.local.append b/packages/create/src/frameworks/react/add-ons/inngest/assets/_dot_env.local.append new file mode 100644 index 00000000..19c62c67 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/assets/_dot_env.local.append @@ -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= diff --git a/packages/create/src/frameworks/react/add-ons/inngest/assets/src/inngest/client.ts.ejs b/packages/create/src/frameworks/react/add-ons/inngest/assets/src/inngest/client.ts.ejs new file mode 100644 index 00000000..b58d4f23 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/assets/src/inngest/client.ts.ejs @@ -0,0 +1,5 @@ +import { Inngest } from 'inngest' + +export const inngest = new Inngest({ + id: '<%= projectName %>', +}) diff --git a/packages/create/src/frameworks/react/add-ons/inngest/assets/src/inngest/functions.ts b/packages/create/src/frameworks/react/add-ons/inngest/assets/src/inngest/functions.ts new file mode 100644 index 00000000..f0029d8d --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/assets/src/inngest/functions.ts @@ -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}!` } + }, +) diff --git a/packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/api/inngest.ts b/packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/api/inngest.ts new file mode 100644 index 00000000..176c6277 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/api/inngest.ts @@ -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), + }, + }, +}) diff --git a/packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/demo/inngest.tsx b/packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/demo/inngest.tsx new file mode 100644 index 00000000..2f7c0805 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/assets/src/routes/demo/inngest.tsx @@ -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(null) + const [sending, setSending] = useState(false) + const [error, setError] = useState(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 ( +
+
+
+
+

Inngest Demo

+

+ Send an event, trigger a durable function. +

+

+ This page dispatches the demo/hello.world event. The + Inngest Dev Server picks it up and runs helloWorld, + which sleeps briefly and returns a greeting. +

+ +
+ + + + + {result && ( +
+

Event sent

+ + {result} + +
+ )} + + {error && ( +
+ {error} +
+ )} +
+
+ +
+

Quick Start

+
    +
  • + Uncomment INNGEST_DEV=1 in .env.local{' '} + 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. +
  • +
  • + Run the Dev Server in a second terminal:{' '} + npx inngest-cli@latest dev. +
  • +
  • + View traces and runs at{' '} + + localhost:8288 + + . +
  • +
+
+
+ ) +} diff --git a/packages/create/src/frameworks/react/add-ons/inngest/info.json b/packages/create/src/frameworks/react/add-ons/inngest/info.json new file mode 100644 index 00000000..58ac0f72 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/info.json @@ -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" + } + ] +} diff --git a/packages/create/src/frameworks/react/add-ons/inngest/logo.svg b/packages/create/src/frameworks/react/add-ons/inngest/logo.svg new file mode 100644 index 00000000..3a6e3b7e --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/create/src/frameworks/react/add-ons/inngest/package.json b/packages/create/src/frameworks/react/add-ons/inngest/package.json new file mode 100644 index 00000000..8c0db34a --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "inngest": "^4.2.4" + } +} diff --git a/packages/create/src/frameworks/react/add-ons/inngest/small-logo.svg b/packages/create/src/frameworks/react/add-ons/inngest/small-logo.svg new file mode 100644 index 00000000..7238711f --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/inngest/small-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/create/src/types.ts b/packages/create/src/types.ts index a36cfb06..6baf04fe 100644 --- a/packages/create/src/types.ts +++ b/packages/create/src/types.ts @@ -53,6 +53,7 @@ export const AddOnBaseSchema = z.object({ 'analytics', 'i18n', 'tooling', + 'workflows', 'other', ]) .optional(),