npm install kompensaRequires Node 18+. Works in modern browsers and React Native (Hermes).
For durable state across process restarts you'll also want one of the adapters:
npm install kompensa pg # Postgres (recommended for most backends)
npm install kompensa ioredis # Redis (good when you already run Redis)Both are peer dependencies — kompensa itself has zero runtime dependencies.
import { createFlow } from 'kompensa';
const signup = createFlow<{ email: string }>('signup')
.step('createUser', {
run: async (ctx) => ({ id: `u_${Date.now()}`, email: ctx.input.email }),
compensate: async (_ctx, user) => console.log('delete user', user.id),
})
.step('sendWelcomeEmail', {
run: async (ctx) => {
console.log('→ email', ctx.results.createUser.id);
return { messageId: 'm-1' };
},
});
const result = await signup.execute({ email: 'you@example.com' });
console.log(result);
// { createUser: { id: 'u_...', email: 'you@example.com' },
// sendWelcomeEmail: { messageId: 'm-1' } }Three things just happened:
- Type inference. Inside
sendWelcomeEmail, TypeScript knowsctx.results.createUser.idis a string — becausecreateUser'srunreturned{ id, email }. - Compensation registered. If
sendWelcomeEmailhad thrown,createUser.compensatewould have fired automatically. - State persisted. By default kompensa uses
MemoryStorage. Swap it for Postgres and the flow survives restarts.
Pass an idempotencyKey. Re-running with the same key returns the cached result:
await signup.execute(
{ email: 'you@example.com' },
{ idempotencyKey: 'signup-you@example.com' },
);
// second call — no steps re-run, returns cached result
await signup.execute(
{ email: 'you@example.com' },
{ idempotencyKey: 'signup-you@example.com' },
);Perfect for:
- Webhook handlers (provider retries the same event)
- HTTP endpoints with an
Idempotency-Keyheader - Message queue consumers (broker may re-deliver)
Plug in a durable adapter:
import { Pool } from 'pg';
import { PostgresStorage } from 'kompensa/storage/postgres';
const storage = new PostgresStorage({
pool: new Pool({ connectionString: process.env.DATABASE_URL }),
});
await storage.ensureSchema(); // one-time table creation
const signup = createFlow<{ email: string }>('signup', { storage })
.step(/* ... */)
.step(/* ... */);Now if the process crashes between createUser and sendWelcomeEmail, a retry with the same idempotencyKey resumes from sendWelcomeEmail — createUser won't run twice.
.step('callPaymentGateway', {
run: async (ctx) => api.charge(ctx.input.amount),
compensate: async (_ctx, charge) => api.refund(charge.id),
retry: {
maxAttempts: 3,
backoff: 'exponential',
initialDelayMs: 200,
jitter: true,
},
timeout: 5_000, // fail the attempt if it takes longer than 5s
})Throw PermanentError to stop retries immediately (validation errors, 4xx responses). Throw TransientError to explicitly opt-in (429, 503, timeouts).
- Concepts — deeper dive on state, compensation, and retry semantics
- Storage adapters — choosing between memory / Postgres / Redis
- Recipes — copy-pasteable patterns for common scenarios