Developer guide
Background jobs
Run asynchronous work with Inngest for email, PDFs, imports, and notifications.
Prolifto uses Inngest for asynchronous work. The
admin app serves the Inngest endpoint at /api/inngest and registers every
function in apps/admin/inngest/functions.
Local development
pnpm dev:admin starts the Inngest dev server on port 8310 alongside the
admin app. Open the Inngest dev UI to inspect runs, replay events, and debug
steps.
Environment:
INNGEST_DEV="http://127.0.0.1:8310"In production, set INNGEST_EVENT_KEY and INNGEST_SIGNING_KEY.
Run categories
| Category | Functions |
|---|---|
| Notifications | Order created, order status changed, customer created, low stock, out of stock |
| Email/SMS/WhatsApp | Order confirmation email, order SMS, resend SMS, WhatsApp order messages and status callbacks |
| Documents | Invoice PDF, receipt PDF |
| Catalog | Product CSV import, product search indexing and removal |
| AI | Product description generation |
| Billing | Subscription renewals and lifecycle |
| Domains | Storefront domain revalidation |
| Token health, template provisioning, inbound messages |
Events
Functions are triggered by events such as:
order/created
order/status.changed
customer/created
invoice/pdf.generate
receipt/pdf.generate
product/import
product/search.index
product/search.remove
product/description.generateEvents carry the organization ID and the entity ID. Functions should re-check the entity in the database rather than trusting stale event data.
Writing a function
- Create a file in
apps/admin/inngest/functions/<area>/. - Define the function with
inngest.createFunction. - Use
step.runfor each unit of work so retries are safe. - Register it in
apps/admin/app/api/inngest/route.ts. - Send the event from the service that owns the change.
- Add tests for the business logic where it matters.
Example shape:
export const exampleFn = inngest.createFunction(
{
id: "example",
triggers: { event: "example/created" },
},
async ({ event, step }) => {
const result = await step.run("load", () =>
loadSomething(event.data.id as string)
);
await step.run("process", () => processSomething(result));
}
);Reliability
- Steps should be idempotent; Inngest retries failed steps.
- Do not perform side effects before the step that owns them.
- Use separate steps for external calls (email, storage, PDFs) so a failure does not repeat database writes.
- Keep payloads small; pass IDs and reload data inside the function.
- Log with the shared logger and include the organization and entity IDs.
Debugging
- Check the Inngest dev UI for the failed step and its output.
- Verify the event was sent and the function is registered.
- Check the relevant environment variables (SMTP, storage, Stripe, WhatsApp).
- Use the system log or the entity's activity timeline to confirm the expected side effect.