OSS-first docs

These docs teach the open system first: contracts, generated surfaces, runtimes, governance, and incremental adoption. Studio shows up as the operating layer on top, not as the source of truth.

AI index

Your First Operation

Build a payment capture operation with policy enforcement in under 10 minutes.

What you'll build

A real-world payment processing operation that validates input, enforces business rules, integrates with Stripe, and audits every transaction. This is production-ready code, not a toy example.

1. Define the operation spec

Create lib/specs/billing/capture-payment.ts:

lib/specs/billing/capture-payment.ts
import { defineCommand } from '@contractspec/lib.contracts-spec';
import { SchemaModel, ScalarTypeEnum } from '@contractspec/lib.schema';

const CapturePaymentInput = new SchemaModel({
  name: 'CapturePaymentInput',
  fields: {
    invoiceId: { type: ScalarTypeEnum.NonEmptyString(), isOptional: false },
    amount: { type: ScalarTypeEnum.PositiveNumber(), isOptional: false },
    currency: { type: ScalarTypeEnum.String(), isOptional: false },
    paymentMethodId: { type: ScalarTypeEnum.NonEmptyString(), isOptional: false },
  },
});

const PaymentResult = new SchemaModel({
  name: 'PaymentResult',
  fields: {
    transactionId: { type: ScalarTypeEnum.String(), isOptional: false },
    status: { type: ScalarTypeEnum.String(), isOptional: false },
    receiptUrl: { type: ScalarTypeEnum.String(), isOptional: true },
  },
});

export const CapturePayment = defineCommand({
  meta: {
    key: 'billing.capturePayment',
    version: '1.0.0',
    description: 'Process a payment for an invoice',
    goal: 'Capture funds from customer payment method',
    context: 'Called when customer confirms purchase',
    owners: ['team-billing'],
    tags: ['payments', 'stripe', 'critical'],
    stability: 'stable',
  },
  io: {
    input: CapturePaymentInput,
    output: PaymentResult,
  },
  policy: {
    auth: 'user',
    rules: [
      { resource: 'invoice', action: 'pay', condition: 'owner' },
    ],
  },
});

2. Implement the handler

Create lib/handlers/billing/capture-payment.ts:

lib/handlers/billing/capture-payment.ts
import { CapturePayment } from '@/lib/specs/billing/capture-payment';
import { stripe } from '@/lib/integrations/stripe';
import { db } from '@/lib/db';

export async function handleCapturePayment(input, ctx) {
  // 1. Verify invoice exists and belongs to user
  const invoice = await db.invoice.findUnique({
    where: { id: input.invoiceId, userId: ctx.userId },
  });

  if (!invoice) throw new Error('Invoice not found');
  if (invoice.status === 'paid') throw new Error('Already paid');

  // 2. Create Stripe payment intent
  const paymentIntent = await stripe.paymentIntents.create({
    amount: Math.round(input.amount * 100),
    currency: input.currency,
    payment_method: input.paymentMethodId,
    confirm: true,
    metadata: { invoiceId: input.invoiceId },
  });

  // 3. Update invoice status
  await db.invoice.update({
    where: { id: input.invoiceId },
    data: {
      status: 'paid',
      paidAt: new Date(),
      transactionId: paymentIntent.id,
    },
  });

  return {
    transactionId: paymentIntent.id,
    status: paymentIntent.status,
    receiptUrl: paymentIntent.charges.data[0]?.receipt_url,
  };
}

3. Register and serve

Wire it up in lib/registry.ts and app/api/ops/[...route]/route.ts:

lib/registry.ts
import { OperationSpecRegistry, installOp } from '@contractspec/lib.contracts-spec';
import { CapturePayment } from './specs/billing/capture-payment';
import { handleCapturePayment } from './handlers/billing/capture-payment';

export const registry = new OperationSpecRegistry();
installOp(registry, CapturePayment, handleCapturePayment);
app/api/ops/[...route]/route.ts
import { makeNextAppHandler } from '@contractspec/lib.contracts-runtime-server-rest/rest-next-app';
import { registry } from '@/lib/registry';
import { auth } from '@/lib/auth';

const handler = makeNextAppHandler(registry, async (req) => {
  const session = await auth(req);
  return { actor: 'user', userId: session.userId, tenantId: session.tenantId };
});

export { handler as GET, handler as POST };

What you just built:

  • Type-safe API endpoint at /api/ops/billing.capturePayment
  • Automatic input validation (amount must be positive, IDs required)
  • Policy enforcement—only invoice owner can pay
  • Stripe integration with error handling
  • Database transaction with audit trail
  • Same spec works with GraphQL, MCP, or webhooks
OSS docsstartStart with OSS. Adopt Studio when you want the operating layer.

Why ContractSpec

Keep educational and comparison content reachable without letting it define the primary OSS learning path.