Trigger reliable, step-by-step user provisioning from Clerk sign-up events through Inngest functions that write to Supabase and handle downstream operations.
The onboarding problem with synchronous code
Most SaaS onboarding flows still run as a single API handler that creates an auth user, inserts rows, sends emails, and provisions resources. When any step fails or times out, the user ends up in a partial state that requires manual cleanup. Event-driven execution separates each concern so failures can be retried independently without blocking the sign-up response.
Clerk emits webhooks on user creation, Supabase provides the durable store, and Inngest turns those events into durable functions with built-in retries and idempotency keys. The pattern matches the operational spine described in Event-Driven SaaS Automation.
Clerk webhook configuration
Create a Clerk webhook endpoint that forwards the user.created event to an Inngest function. In the Clerk dashboard, add the endpoint URL and select only the events you need; sending every event increases noise and cost. Sign the payload with the Clerk webhook secret so Inngest can verify it before processing.
Store the Clerk user ID as the primary key in your Supabase users table. This removes any need to sync Clerk and Supabase auth systems and gives you a stable identifier for subsequent steps.
Inngest function skeleton
Define a single Inngest function that listens for clerk/user.created. The function receives the Clerk user object and an idempotency key derived from the Clerk user ID. Inngest automatically deduplicates retries within the configured window.
import { inngest } from './client';
export const onboardUser = inngest.createFunction(
{ id: 'onboard-user', name: 'Onboard new user' },
{ event: 'clerk/user.created' },
async ({ event, step }) => {
const { id: clerkId, email_addresses } = event.data;
const email = email_addresses[0].email_address;
await step.run('create-supabase-record', async () => {
// Supabase insert here
});
// additional steps follow
}
);
Supabase provisioning steps
Inside the first step, insert the user record and create an initial workspace row with a foreign key to the user. Use a single transaction or Supabase RPC so the two writes are atomic. If the insert fails because the user already exists, treat it as success—this is the idempotency contract.
Follow the insert with a second step that grants default role permissions or seeds starter data. Separate steps let you add or remove provisioning logic later without touching the auth path.
Retries, timeouts, and partial failures
Configure Inngest step timeouts and retry policies per step rather than globally. Database writes usually need only two retries; email delivery or external API calls may need exponential backoff with a longer window. See the patterns in Resilient API Workflows for concrete settings.
When a step permanently fails, emit a compensating event (e.g., onboarding.failed) that triggers a cleanup function or Slack alert. Never rely on the original request context to recover state.
Production rollout and observability
Deploy the Inngest functions on the same Cloudflare Workers or Vercel edge runtime as the rest of the application. Use Inngest’s built-in dashboard to inspect function runs, replay failed steps, and set up alerts on error rate. Add a simple health check that queries recent successful onboarding events from Supabase.
Start with a small cohort of new sign-ups behind a feature flag. Monitor the ratio of completed versus started functions; anything below 99 % indicates a step that needs hardening before full rollout.
