Recipe 1 · Stripe payment lifecycle
A payment goes pending → authorized → captured, or failed at any point, or refunded from captured. Stateledger holds the source of truth so a duplicated Stripe webhook can’t double-capture.
export const paymentMachine = defineMachine({
name: "payment",
states: ["pending", "authorized", "captured", "refunded", "failed"] as const,
initialState: "pending",
transitions: [
{ from: "pending", to: "authorized" },
{ from: "pending", to: "failed" },
{ from: "authorized", to: "captured" },
{ from: "authorized", to: "failed" },
{ from: "captured", to: "refunded" },
] as const,
});
// Inside your Stripe webhook handler:
export async function onStripeEvent(event: Stripe.Event) {
await prisma.$transaction(async (tx) => {
const payment = paymentMachine.for(event.data.object.metadata.paymentId, {
adapter: createPrismaAdapter(tx),
actor: "stripe:webhook",
});
switch (event.type) {
case "payment_intent.succeeded":
await payment.transitionTo("captured", { metadata: { intent: event.data.object.id } });
break;
case "payment_intent.payment_failed":
await payment.transitionTo("failed", { metadata: { reason: event.data.object.last_payment_error?.message } });
break;
case "charge.refunded":
await payment.transitionTo("refunded", { metadata: { refundId: event.data.object.refunds?.data[0]?.id } });
break;
}
});
}Duplicated webhooks are harmless: the second event tries to move an already-captured payment to captured, and stateledger throws InvalidTransition. Handle it and return 200 to Stripe.
Recipe 2 · Subscription dunning
Recurring subscriptions cycle through trial → active → past_due (on failed renewal) → cancelled. Each dunning attempt writes an audit row.
export const subscriptionMachine = defineMachine({
name: "subscription",
states: ["trialing", "active", "past_due", "cancelled"] as const,
initialState: "trialing",
transitions: [
{ from: "trialing", to: "active" },
{ from: "trialing", to: "cancelled" },
{ from: "active", to: "past_due" },
{ from: "active", to: "cancelled" },
{ from: "past_due", to: "active" }, // successful retry
{ from: "past_due", to: "cancelled" }, // grace period expired
] as const,
});
// Cron worker: attempt renewal for every past_due subscription
export async function retryPastDue() {
const subs = await prisma.subscription.findMany({ where: { state: "past_due" } });
for (const sub of subs) {
await prisma.$transaction(async (tx) => {
const machine = subscriptionMachine.for(sub.id, {
adapter: createPrismaAdapter(tx),
actor: "dunning:worker",
});
const result = await chargeStripe(sub.customerId, sub.amount);
if (result.ok) {
await machine.transitionTo("active", { metadata: { chargeId: result.chargeId } });
} else if (Date.now() - sub.pastDueSince > GRACE_PERIOD_MS) {
await machine.transitionTo("cancelled", { metadata: { reason: "grace_expired" } });
}
// else: leave in past_due for next attempt
});
}
}Recipe 3 · Order fulfilment
E-commerce orders move through a longer pipeline. Each stage typically has a different actor (system vs warehouse vs carrier).
export const orderMachine = defineMachine({
name: "order",
states: ["cart", "paid", "picked", "packed", "shipped", "delivered", "returned"] as const,
initialState: "cart",
transitions: [
{ from: "cart", to: "paid" },
{ from: "paid", to: "picked" },
{ from: "picked", to: "packed" },
{ from: "packed", to: "shipped" },
{ from: "shipped", to: "delivered" },
{ from: "delivered", to: "returned" },
] as const,
});
// Warehouse endpoint:
export async function markPicked(orderId: string, warehouseUser: string) {
return prisma.$transaction(async (tx) => {
const order = orderMachine.for(orderId, {
adapter: createPrismaAdapter(tx),
actor: warehouseUser,
});
return order.transitionTo("picked", {
metadata: { pickedAt: new Date().toISOString() },
});
});
}Recipe 4 · KYC & onboarding
Regulated flows need airtight audit trails: who approved this user, when, based on what evidence. Stateledger records all of it.
export const kycMachine = defineMachine({
name: "kyc",
states: ["unverified", "pending", "approved", "rejected", "suspended"] as const,
initialState: "unverified",
transitions: [
{ from: "unverified", to: "pending" },
{ from: "pending", to: "approved" },
{ from: "pending", to: "rejected" },
{ from: "approved", to: "suspended" },
{ from: "suspended", to: "approved" },
] as const,
});
// Compliance officer approves:
await prisma.$transaction(async (tx) => {
const kyc = kycMachine.for(userId, {
adapter: createPrismaAdapter(tx),
actor: `officer:${officerId}`,
});
await kyc.transitionTo("approved", {
metadata: {
documentIds: ["dl-01", "utility-02"],
reviewedAt: new Date().toISOString(),
notes: "Address matches DL. Approved.",
},
});
});Recipe 5 · Content moderation queue
Two moderators shouldn’t accidentally act on the same submission. The advisory lock in stateledger prevents this without any coordination code.
export const moderationMachine = defineMachine({
name: "content_moderation",
states: ["submitted", "in_review", "published", "flagged", "removed"] as const,
initialState: "submitted",
transitions: [
{ from: "submitted", to: "in_review" },
{ from: "in_review", to: "published" },
{ from: "in_review", to: "flagged" },
{ from: "flagged", to: "removed" },
{ from: "flagged", to: "published" }, // re-approved on appeal
] as const,
});
// If moderator A and B both hit "start review" at the same instant,
// one succeeds; the other gets InvalidTransition when it retries.
export async function startReview(itemId: string, moderator: string) {
return prisma.$transaction(async (tx) => {
const item = moderationMachine.for(itemId, {
adapter: createPrismaAdapter(tx),
actor: moderator,
});
return item.transitionTo("in_review");
});
}