Code Examples

Complete, copy-paste ready examples for integrating tachles-pay into your applications.

📦 Installation

Install the core package and its peer dependency:

bash
npm install tachles-pay effect
💡
Effect-TS: Tachles uses Effect for type-safe error handling and dependency injection. If you're new to Effect, check out the official docs.

Optional Dependencies

bash
# 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).

typescript
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);
Type Safety: All database operations return typed Effect values with explicit error types. TypeScript will catch issues at compile time!

Cloudflare Workers

Deploy a payment API to Cloudflare Workers with KV storage. The runtime handles request-scoped dependency injection automatically.

typescript
// 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

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.

typescript
// 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.

💡
React 19 Required: Install React 19 to use these hooks:npm install react@^19 tachles-pay

Data Fetching with use()

React 19's use() hook suspends rendering while data loads. Wrap components in <Suspense> to show loading states.

typescript
// 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).

pay_demo_1...$49.99alice@demo.com
pending
pay_demo_2...$24.99bob@demo.com
pending
pay_demo_3...$99.99carol@demo.com
completed

useTransition Demo

Create payments with non-blocking UI. The form stays responsive during submission.

Created Payments:
No payments yet. Try creating one!

use() + Suspense Demo

Click "Reload" to see Suspense fallback while data loads.

Utility Hooks Demo

Live countdown timer and dynamic formatting.

useFormatCurrency
$49.99
from 4999 cents
useRelativeTime
3m ago
updates in real-time
useCountdown
until expiry
Try the Full Demo: Check out the Live Demo Dashboard to see all these patterns working together with simulated payment data.

🧩 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.

typescript
// 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

tsx
<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.

typescript
// 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);
}
⚠️
Security: Always verify webhook signatures to prevent replay attacks. Never trust webhook payloads without verification!

Ready to get started?

Check out the full API documentation or try the live demo to see the package in action.