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.
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:
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:
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:
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);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
Installation
Install the CLI and core packages, then prepare a workspace for incremental adoption.
Compatibility
Check the supported runtimes, package managers, and adoption assumptions before rollout.
Why ContractSpec
Keep educational and comparison content reachable without letting it define the primary OSS learning path.