Code Examples
Complete, copy-paste ready examples for integrating tachles-pay into your applications.
📦 Installation
Install the core package and its peer dependency:
npm install tachles-pay effectOptional Dependencies
# For Upstash Redis storage
npm install @upstash/redis
# For Cloudflare Workers types
npm install -D @cloudflare/workers-types🔷 TypeScript Usage
Basic usage with the Effect pattern. This example shows how to create apps and payment intents using the in-memory providers (perfect for development and testing).
import { Effect } from "effect";
import {
createInfraLayer,
runWithInfra,
Database,
} from "tachles-pay";
// Create infrastructure with in-memory providers (great for dev/testing)
const layer = createInfraLayer();
// Define your payment flow using Effect
const createPayment = Effect.gen(function* () {
const db = yield* Database;
// 1. Create or get your app (represents a merchant/integration)
const app = yield* db.createApp({
name: "My E-Commerce Store",
provider: "stripe",
apiKey: process.env.STRIPE_SECRET_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
webhookUrl: "https://mystore.com/webhooks/stripe",
});
// 2. Create a payup (payment intent)
const payup = yield* db.createPayup({
appId: app.id,
amount: 4999, // $49.99 in cents
currency: "USD",
customerEmail: "customer@example.com",
customerName: "John Doe",
description: "Premium Plan - Monthly",
returnUrl: "https://mystore.com/success",
cancelUrl: "https://mystore.com/cancel",
expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 minutes
});
console.log("Payment created:", payup.id);
console.log("Checkout URL:", `https://checkout.tachles.dev/${payup.id}`);
return payup;
});
// Execute the effect with infrastructure
const payup = await runWithInfra(layer, createPayment);⚡ Cloudflare Workers
Deploy a payment API to Cloudflare Workers with KV storage. The runtime handles request-scoped dependency injection automatically.
// worker.ts - Cloudflare Workers deployment
import { Effect } from "effect";
import {
makeFetchRuntime,
Database,
createCloudflareKVStorage,
createMemoryStorage,
type CloudflareEnv,
} from "tachles-pay/adapters/cloudflare";
// Request body types
interface CreatePayupBody {
amount: number;
currency?: string;
customerEmail?: string;
description?: string;
}
// Create request-scoped runtime
const runtime = makeFetchRuntime({
// Use KV for persistence, fallback to memory for dev
makeStorage: (env) =>
env.TACHLES_KV
? createCloudflareKVStorage(env.TACHLES_KV)
: createMemoryStorage(),
});
// Define your API handler
const handler = (request: Request, _env: CloudflareEnv) =>
Effect.gen(function* () {
const url = new URL(request.url);
const db = yield* Database;
// POST /api/payups - Create a new payment
if (url.pathname === "/api/payups" && request.method === "POST") {
const body = (yield* Effect.promise(() => request.json())) as CreatePayupBody;
const payup = yield* db.createPayup({
appId: "default-app",
amount: body.amount,
currency: body.currency || "USD",
customerEmail: body.customerEmail,
description: body.description,
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
});
return Response.json(payup, { status: 201 });
}
// GET /api/payups/:id - Get payment status
if (url.pathname.startsWith("/api/payups/") && request.method === "GET") {
const id = url.pathname.split("/").pop()!;
const payup = yield* db.getPayup(id);
if (!payup) {
return Response.json({ error: "Not found" }, { status: 404 });
}
return Response.json(payup);
}
return Response.json({ error: "Not found" }, { status: 404 });
});
// Export the worker
export default { fetch: runtime(handler) };wrangler.toml
name = "tachles-api"
main = "src/worker.ts"
compatibility_date = "2024-01-01"
[[kv_namespaces]]
binding = "TACHLES_KV"
id = "your-kv-namespace-id"🟢 Node.js Server
Run a standalone HTTP server using Node.js. Great for self-hosting or containerized deployments.
// server.ts - Node.js HTTP server
import { Effect } from "effect";
import { startServer, Database } from "tachles-pay/adapters/node";
interface CreatePayupBody {
amount: number;
currency?: string;
customerEmail?: string;
description?: string;
}
const handler = (request: Request) =>
Effect.gen(function* () {
const url = new URL(request.url);
const db = yield* Database;
// Health check
if (url.pathname === "/health") {
return Response.json({ status: "ok", timestamp: new Date().toISOString() });
}
// Create payment
if (url.pathname === "/api/payups" && request.method === "POST") {
const body = (yield* Effect.promise(() => request.json())) as CreatePayupBody;
const payup = yield* db.createPayup({
appId: "default-app",
amount: body.amount,
currency: body.currency || "USD",
customerEmail: body.customerEmail,
description: body.description,
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
});
return Response.json(payup, { status: 201 });
}
// List payments
if (url.pathname === "/api/payups" && request.method === "GET") {
const payups = yield* db.listPayups();
return Response.json(payups);
}
return Response.json({ error: "Not found" }, { status: 404 });
}).pipe(
Effect.catchAll((error) =>
Effect.succeed(Response.json({ error: String(error) }, { status: 500 }))
)
);
// Start the server
const PORT = parseInt(process.env.PORT || "3000");
startServer({ port: PORT, handler }).then(({ url }) => {
console.log(`🚀 Server running at ${url}`);
});⚛️ React 19 Hooks
Modern React hooks using React 19 features: use(),useOptimistic, and useTransition. These hooks require React 19.x.
npm install react@^19 tachles-payData Fetching with use()
React 19's use() hook suspends rendering while data loads. Wrap components in <Suspense> to show loading states.
// Using React 19's use() hook with Suspense
import { Suspense } from "react";
import { usePaymentsData, usePaymentData, usePaymentStatsData } from "tachles-pay/react";
// Component that fetches payments - MUST be inside Suspense boundary
function PaymentsList() {
// use() hook - throws promise, caught by Suspense
const payments = usePaymentsData({ status: "completed", limit: 10 });
return (
<ul className="space-y-2">
{payments.map(payment => (
<li key={payment.id} className="flex justify-between">
<span>{payment.customerEmail || "Anonymous"}</span>
<span>${(payment.amount / 100).toFixed(2)}</span>
</li>
))}
</ul>
);
}
// Wrapper with Suspense boundary
function PaymentsPage() {
return (
<Suspense fallback={<div className="animate-pulse">Loading payments...</div>}>
<PaymentsList />
</Suspense>
);
}🎮 Live Hook Demos
Interactive demos showing the React 19 hooks in action. These demos use simulated data to demonstrate the hook patterns without requiring a backend.
useOptimistic Demo
Click "Cancel" to see optimistic UI update instantly, then revert after 1.5s (simulated failure).
useTransition Demo
Create payments with non-blocking UI. The form stays responsive during submission.
use() + Suspense Demo
Click "Reload" to see Suspense fallback while data loads.
Utility Hooks Demo
Live countdown timer and dynamic formatting.
🧩 React Components
Ready-to-use React components for common payment flows.
A simple checkout button that creates a payment and redirects to the hosted checkout page.
// components/CheckoutButton.tsx
"use client";
import { useState } from "react";
import { useTachles } from "../hooks/useTachles";
interface CheckoutButtonProps {
amount: number;
currency?: string;
productName: string;
customerEmail?: string;
onSuccess?: (checkoutUrl: string) => void;
onError?: (error: string) => void;
}
export function CheckoutButton({
amount,
currency = "USD",
productName,
customerEmail,
onSuccess,
onError,
}: CheckoutButtonProps) {
const { createPayup, loading, error } = useTachles();
const [checkoutUrl, setCheckoutUrl] = useState<string | null>(null);
const handleCheckout = async () => {
const payup = await createPayup({
amount,
currency,
customerEmail,
description: productName,
});
if (payup) {
const url = `https://checkout.tachles.dev/${payup.id}`;
setCheckoutUrl(url);
onSuccess?.(url);
// Redirect to checkout
window.location.href = url;
} else if (error) {
onError?.(error);
}
};
const formattedAmount = new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount / 100);
return (
<button
onClick={handleCheckout}
disabled={loading}
className="relative px-6 py-3 bg-indigo-600 hover:bg-indigo-700
disabled:bg-indigo-400 disabled:cursor-not-allowed
text-white font-medium rounded-lg transition-colors
flex items-center justify-center gap-2"
>
{loading ? (
<>
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
Processing...
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
Pay {formattedAmount}
</>
)}
</button>
);
}Usage
<CheckoutButton
amount={4999}
currency="USD"
productName="Premium Plan"
customerEmail="customer@example.com"
onSuccess={(url) => console.log("Redirecting to:", url)}
onError={(err) => console.error("Payment failed:", err)}
/>🔔 Webhook Handler
Handle incoming webhooks from payment providers like Stripe. This example shows a Next.js App Router implementation with signature verification.
// api/webhooks/stripe/route.ts (Next.js App Router)
import { Effect } from "effect";
import { createInfraLayer, runWithInfra, Database } from "tachles-pay";
import { verifyWebhookSignature } from "tachles-pay";
const layer = createInfraLayer();
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature) {
return Response.json({ error: "Missing signature" }, { status: 400 });
}
const program = Effect.gen(function* () {
const db = yield* Database;
// Get the app to retrieve webhook secret
const apps = yield* db.listApps();
const app = apps.find((a) => a.provider === "stripe");
if (!app) {
return Response.json({ error: "App not found" }, { status: 404 });
}
// Verify webhook signature
const isValid = yield* verifyWebhookSignature({
payload: body,
signature,
secret: app.webhookSecret,
});
if (!isValid) {
return Response.json({ error: "Invalid signature" }, { status: 401 });
}
// Parse and handle the event
const event = JSON.parse(body);
switch (event.type) {
case "payment_intent.succeeded": {
const paymentIntent = event.data.object;
// Find and update the payup
const payups = yield* db.listPayups({ appId: app.id });
const payup = payups.find((p) =>
p.providerPaymentId === paymentIntent.id
);
if (payup) {
yield* db.updatePayup(payup.id, {
status: "completed",
providerData: paymentIntent,
});
}
break;
}
case "payment_intent.payment_failed": {
const paymentIntent = event.data.object;
const payups = yield* db.listPayups({ appId: app.id });
const payup = payups.find((p) =>
p.providerPaymentId === paymentIntent.id
);
if (payup) {
yield* db.updatePayup(payup.id, {
status: "failed",
providerData: paymentIntent,
});
}
break;
}
}
return Response.json({ received: true });
});
return runWithInfra(layer, program);
}Ready to get started?
Check out the full API documentation or try the live demo to see the package in action.