Back to Blog
Case Studies

Wintura.ai: A Production Reference Build in Next.js 16

How we shipped a B2B SaaS with multi-tenant RLS, Claude Sonnet 4.6 AI pipeline, sealed PDF audit trails, and 24 Playwright e2e tests in 6 weeks.

alvilika16 min read

What "Reference Build" Means

A wintura.ai reference build is a fully deployed B2B SaaS application that demonstrates production-grade patterns: multi-tenant Row-Level Security, AI pipeline integration with Claude Sonnet 4.6, sealed PDF audit trails for compliance, and comprehensive end-to-end test coverage. Unlike prototype demos or MVP mockups, a reference build handles real users, real payments, and real security threats — the same patterns required by any AI-generated codebase transitioning from demo to production.

This post documents the complete technical implementation of wintura.ai, shipped in 6 weeks with 24 Playwright e2e test files. The same production hardening patterns are available via the Production Lift for any Bolt, Lovable, v0, or Cursor codebase.

The Business Context: Why Wintura Exists

Wintura.ai solves a specific problem in the window installation industry: proposal generation for sales teams. A typical window replacement proposal requires:

  1. Site assessment data (window dimensions, frame condition, installation complexity)
  2. Product selection from manufacturer catalogs
  3. Labor calculations based on installation type
  4. Financing options with compliance-safe disclosures
  5. Professional formatting for customer presentation

Before Wintura, sales teams assembled these proposals manually — copying data between spreadsheets, calculating totals in their heads, formatting documents by hand. A single proposal took 45-90 minutes. Errors were common. Formatting was inconsistent. Compliance language was often missing.

Wintura automates the entire workflow. The salesperson enters site data and product selections. Wintura generates a complete, professionally formatted proposal with AI-written customer-facing copy, accurate calculations, and compliant disclosures. Time per proposal: under 5 minutes.

The business model is B2B SaaS: window installation companies pay monthly for team access, with usage tracked per proposal generated.

The Technical Stack (Verified May 2026)

Every technology choice in Wintura was made for a specific reason. Here's the complete stack with rationale:

Core Framework

ComponentVersionRationale
Next.js16.1.6App Router, RSC by default, Turbopack build. The Q2 2026 production standard.
React19.2.3Server Components everywhere possible; "use client" only for state/interactivity.
TypeScript5.8.3Strict mode. params is Promise<...> in Next 16 — always await params in dynamic routes.
Tailwind CSS4.1.7@tailwindcss/postcss with @theme inline block in globals.css.

Database & Auth

ComponentVersionRationale
SupabaseLatestPostgres + Auth + Realtime + Storage in one platform.
Row-Level SecurityNativeMulti-tenant isolation at the database layer — queries can't return wrong-tenant data.
NextAuth.js5.0.0-beta.25Credentials provider with Supabase as backend. Session management with secure cookies.

AI Pipeline

ComponentVersionRationale
Claude Sonnet 4.6LatestPrimary LLM for proposal structure and content generation.
Claude Haiku 3.5LatestSecondary LLM for voice refinement (faster, cheaper for styling passes).
Anthropic SDK0.39.0Official TypeScript SDK with streaming support.

Payments & Billing

ComponentVersionRationale
Stripe17.7.0Subscriptions, usage-based billing, webhook handling.
Webhook verificationNativestripe.webhooks.constructEvent with signature verification.
IdempotencyCustomEvent ID tracking prevents duplicate processing.

Testing & Observability

ComponentVersionRationale
Playwright1.52.024 e2e test files covering all critical user journeys.
Sentry9.15.0Error tracking with source maps.
Vercel Analytics1.5.0Core Web Vitals + custom events.

Deployment

ComponentRationale
VercelEdge network, automatic HTTPS, preview deployments, one-click production.
GitHub ActionsCI/CD pipeline: lint → type-check → test → deploy.

Multi-Tenant Architecture: RLS in Practice

The most critical production pattern in Wintura is multi-tenant Row-Level Security. Here's how it works.

The Problem

Wintura serves multiple window installation companies. Each company has its own users, proposals, customers, and billing. Without proper isolation:

  • Company A could see Company B's proposals
  • A malicious query could dump the entire database
  • A bug in one tenant's code could affect all tenants

The Solution: Row-Level Security

Every user-facing table in Wintura has RLS enabled:

-- Enable RLS on proposals table
ALTER TABLE proposals ENABLE ROW LEVEL SECURITY;

-- Create policy: users can only see their organization's proposals
CREATE POLICY org_isolation ON proposals
  FOR ALL
  USING (organization_id = auth.jwt() ->> 'org_id');

This policy runs at the Postgres level. Even if application code has a bug that omits the tenant filter, the database enforces isolation.

The Implementation Pattern

Wintura uses a three-layer isolation model:

Layer 1: Authentication

// auth.ts
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Credentials({
      async authorize(credentials) {
        const user = await supabase.auth.signInWithPassword({
          email: credentials.email as string,
          password: credentials.password as string,
        });

        if (!user.data.user) return null;

        // Fetch organization membership
        const { data: membership } = await supabase
          .from('organization_members')
          .select('organization_id, role')
          .eq('user_id', user.data.user.id)
          .single();

        return {
          id: user.data.user.id,
          email: user.data.user.email,
          orgId: membership?.organization_id,
          role: membership?.role,
        };
      },
    }),
  ],
  callbacks: {
    jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.orgId = user.orgId;
        token.role = user.role;
      }
      return token;
    },
    session({ session, token }) {
      session.user.id = token.id as string;
      session.user.orgId = token.orgId as string;
      session.user.role = token.role as string;
      return session;
    },
  },
});

Layer 2: Request Context

// middleware.ts
export async function middleware(request: NextRequest) {
  const session = await auth();

  if (!session?.user?.orgId) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Set organization context for Supabase RLS
  const response = NextResponse.next();
  response.headers.set('x-org-id', session.user.orgId);
  return response;
}

Layer 3: Database RLS

-- The RLS policy uses the JWT org_id claim
CREATE POLICY org_isolation ON proposals
  FOR ALL
  USING (organization_id = auth.jwt() ->> 'org_id');

-- Same pattern for all user-facing tables
CREATE POLICY org_isolation ON customers
  FOR ALL
  USING (organization_id = auth.jwt() ->> 'org_id');

CREATE POLICY org_isolation ON products
  FOR ALL
  USING (organization_id = auth.jwt() ->> 'org_id');

Why This Matters

With RLS enabled, even this intentionally broken query is safe:

// BUG: Missing organization filter
const { data: proposals } = await supabase
  .from('proposals')
  .select('*');
// RLS automatically filters to current user's organization

The database layer enforces what the application layer forgot. This is defense in depth.

The AI Pipeline: Two-Model Architecture

Wintura's proposal generation uses a two-model architecture with Claude Sonnet 4.6 and Claude Haiku 3.5.

Why Two Models?

Different parts of proposal generation have different requirements:

TaskRequirementsBest Model
Structure generationComplex reasoning, data synthesis, calculation verificationClaude Sonnet 4.6
Voice refinementFast iteration, style consistency, lower costClaude Haiku 3.5

Using Sonnet for everything works but costs 3-4× more per proposal. Using Haiku for everything produces lower-quality structural reasoning. The two-model approach optimizes for both quality and cost.

The Pipeline

Step 1: Data Assembly

// Gather all inputs for the proposal
const proposalContext = {
  siteData: await getSiteAssessment(proposalId),
  products: await getSelectedProducts(proposalId),
  customer: await getCustomerInfo(proposalId),
  organization: await getOrgSettings(session.user.orgId),
  complianceRules: await getComplianceRules(organization.state),
};

Step 2: Structure Generation (Sonnet 4.6)

const structurePrompt = `
Generate a window replacement proposal with the following structure:
1. Executive summary (3-4 sentences)
2. Site assessment summary
3. Product recommendations with pricing
4. Installation timeline
5. Financing options
6. Compliance disclosures for ${organization.state}

Site data: ${JSON.stringify(proposalContext.siteData)}
Products: ${JSON.stringify(proposalContext.products)}
Customer: ${proposalContext.customer.name}

Output as JSON matching this schema:
${JSON.stringify(proposalSchema)}
`;

const structure = await anthropic.messages.create({
  model: 'claude-sonnet-4-6-20250514',
  max_tokens: 4096,
  messages: [{ role: 'user', content: structurePrompt }],
});

Step 3: Voice Refinement (Haiku 3.5)

const voicePrompt = `
Refine this proposal text for customer presentation.
Maintain all facts and numbers exactly.
Adjust tone to be professional but warm.
Use ${organization.brandVoice || 'professional'} voice.

Original: ${structure.content}
`;

const refined = await anthropic.messages.create({
  model: 'claude-3-5-haiku-20241022',
  max_tokens: 4096,
  messages: [{ role: 'user', content: voicePrompt }],
});

Step 4: Validation

// Verify calculations match source data
const validation = validateProposalCalculations(
  refined.content,
  proposalContext.products,
  proposalContext.siteData
);

if (!validation.valid) {
  // Regenerate with explicit calculation instructions
  return regenerateWithCorrections(validation.errors);
}

Cost Analysis

Per proposal generation (Q2 2026 pricing):

StepModelTokens (avg)Cost
StructureSonnet 4.6~2,500 input + ~1,500 output~$0.045
RefinementHaiku 3.5~2,000 input + ~1,000 output~$0.003
Total~$0.048

At $0.05 per proposal, a company generating 500 proposals/month pays ~$25 in AI costs. The subscription price is $199/month — 87% gross margin on AI costs alone.

Sealed PDF Audit Trail

Wintura generates legally compliant proposals with a sealed PDF audit trail. Here's what that means and how it's implemented.

The Compliance Requirement

Window replacement financing involves consumer credit regulations. Lenders require:

  1. Immutable record — The proposal as presented to the customer cannot be modified after signing
  2. Timestamp verification — Proof of when the proposal was generated and signed
  3. Content hash — Cryptographic verification that the document hasn't been altered

The Implementation

Step 1: PDF Generation

// Generate PDF from proposal data
import { renderToBuffer } from '@react-pdf/renderer';
import { ProposalDocument } from '@/components/pdf/ProposalDocument';

const pdfBuffer = await renderToBuffer(
  <ProposalDocument
    proposal={proposal}
    organization={organization}
    customer={customer}
    generatedAt={new Date()}
  />
);

Step 2: Hash and Seal

import { createHash } from 'crypto';

// Generate SHA-256 hash of PDF content
const contentHash = createHash('sha256')
  .update(pdfBuffer)
  .digest('hex');

// Store sealed metadata
await supabase.from('proposal_seals').insert({
  proposal_id: proposal.id,
  content_hash: contentHash,
  sealed_at: new Date().toISOString(),
  pdf_url: await uploadToStorage(pdfBuffer, `proposals/${proposal.id}.pdf`),
});

Step 3: Verification Endpoint

// API route for auditors to verify proposal integrity
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;

  const { data: seal } = await supabase
    .from('proposal_seals')
    .select('*')
    .eq('proposal_id', id)
    .single();

  if (!seal) {
    return Response.json({ error: 'Proposal not found' }, { status: 404 });
  }

  // Download current PDF and verify hash
  const currentPdf = await downloadFromStorage(seal.pdf_url);
  const currentHash = createHash('sha256')
    .update(currentPdf)
    .digest('hex');

  return Response.json({
    proposal_id: id,
    sealed_at: seal.sealed_at,
    integrity_verified: currentHash === seal.content_hash,
    original_hash: seal.content_hash,
    current_hash: currentHash,
  });
}

Why This Matters

Without the sealed audit trail:

  • Proposals could be modified after customer signature
  • Compliance audits would fail
  • The business couldn't serve financing partners

The sealed PDF pattern is required for any B2B SaaS touching regulated industries (finance, healthcare, legal, insurance).

The Test Suite: 24 Playwright E2E Files

Wintura ships with 24 Playwright end-to-end test files covering all critical user journeys. Here's the structure:

Test File Organization

tests/
├── auth/
│   ├── login.spec.ts
│   ├── logout.spec.ts
│   ├── password-reset.spec.ts
│   └── registration.spec.ts
├── proposals/
│   ├── create-proposal.spec.ts
│   ├── edit-proposal.spec.ts
│   ├── delete-proposal.spec.ts
│   ├── duplicate-proposal.spec.ts
│   ├── generate-pdf.spec.ts
│   └── seal-proposal.spec.ts
├── customers/
│   ├── create-customer.spec.ts
│   ├── edit-customer.spec.ts
│   ├── delete-customer.spec.ts
│   └── customer-history.spec.ts
├── products/
│   ├── product-catalog.spec.ts
│   ├── product-selection.spec.ts
│   └── pricing-calculation.spec.ts
├── billing/
│   ├── subscription-upgrade.spec.ts
│   ├── subscription-cancel.spec.ts
│   ├── invoice-history.spec.ts
│   └── payment-method.spec.ts
├── admin/
│   ├── team-management.spec.ts
│   ├── organization-settings.spec.ts
│   └── usage-dashboard.spec.ts
└── accessibility/
    └── wcag-audit.spec.ts

Example: Proposal Creation Test

// tests/proposals/create-proposal.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Proposal Creation', () => {
  test.beforeEach(async ({ page }) => {
    // Login as test user
    await page.goto('/login');
    await page.fill('[name="email"]', 'test@wintura.ai');
    await page.fill('[name="password"]', process.env.TEST_PASSWORD!);
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL('/dashboard');
  });

  test('creates proposal with valid data', async ({ page }) => {
    await page.goto('/proposals/new');

    // Step 1: Customer selection
    await page.click('[data-testid="customer-select"]');
    await page.click('[data-testid="customer-option-1"]');

    // Step 2: Site assessment
    await page.fill('[name="windowCount"]', '8');
    await page.selectOption('[name="frameCondition"]', 'good');
    await page.selectOption('[name="installationType"]', 'replacement');

    // Step 3: Product selection
    await page.click('[data-testid="product-category-vinyl"]');
    await page.click('[data-testid="product-option-premium"]');

    // Step 4: Generate proposal
    await page.click('[data-testid="generate-proposal"]');

    // Wait for AI generation (with timeout)
    await expect(page.locator('[data-testid="proposal-preview"]'))
      .toBeVisible({ timeout: 30000 });

    // Verify proposal content
    await expect(page.locator('[data-testid="proposal-total"]'))
      .toContainText('$');
    await expect(page.locator('[data-testid="proposal-customer-name"]'))
      .toContainText('Test Customer');

    // Save proposal
    await page.click('[data-testid="save-proposal"]');
    await expect(page).toHaveURL(/\/proposals\/[a-z0-9-]+$/);
  });

  test('validates required fields', async ({ page }) => {
    await page.goto('/proposals/new');

    // Try to generate without customer
    await page.click('[data-testid="generate-proposal"]');

    // Should show validation error
    await expect(page.locator('[data-testid="error-customer-required"]'))
      .toBeVisible();
  });

  test('handles AI generation failure gracefully', async ({ page }) => {
    // Mock AI endpoint to fail
    await page.route('**/api/ai/generate', route => {
      route.fulfill({
        status: 500,
        body: JSON.stringify({ error: 'Service unavailable' }),
      });
    });

    await page.goto('/proposals/new');
    await page.click('[data-testid="customer-select"]');
    await page.click('[data-testid="customer-option-1"]');
    await page.fill('[name="windowCount"]', '8');
    await page.click('[data-testid="generate-proposal"]');

    // Should show error message with retry option
    await expect(page.locator('[data-testid="error-generation-failed"]'))
      .toBeVisible();
    await expect(page.locator('[data-testid="retry-generation"]'))
      .toBeVisible();
  });
});

Test Coverage Philosophy

The 24 test files follow this coverage priority:

  1. Authentication flows — If login breaks, nothing works
  2. Core feature happy paths — The main thing the product does
  3. Payment flows — Revenue-critical paths
  4. Error handling — Graceful degradation under failure
  5. Accessibility — WCAG 2.1 AA compliance

This is the same test coverage philosophy applied in the Production Lift — 15 spec files minimum covering all critical user journeys.

Webhook Security: Stripe Integration

Wintura processes Stripe webhooks for subscription management. Here's the production-grade implementation:

The Problem

Webhook endpoints are public URLs that receive POST requests from external services. Without proper security:

  • Attackers can forge webhook events
  • Duplicate events can charge customers twice
  • Missing events can leave subscriptions in limbo

The Solution

Step 1: Signature Verification

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: Request) {
  const sig = req.headers.get('stripe-signature');
  const body = await req.text();

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig!, webhookSecret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return Response.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // Process verified event
  await handleStripeEvent(event);
  return Response.json({ received: true });
}

Step 2: Idempotency

async function handleStripeEvent(event: Stripe.Event) {
  // Check if we've already processed this event
  const { data: existing } = await supabase
    .from('processed_webhooks')
    .select('id')
    .eq('event_id', event.id)
    .single();

  if (existing) {
    console.log(`Event ${event.id} already processed, skipping`);
    return;
  }

  // Process the event
  switch (event.type) {
    case 'customer.subscription.created':
      await handleSubscriptionCreated(event.data.object as Stripe.Subscription);
      break;
    case 'customer.subscription.updated':
      await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
      break;
    case 'customer.subscription.deleted':
      await handleSubscriptionDeleted(event.data.object as Stripe.Subscription);
      break;
    case 'invoice.payment_succeeded':
      await handlePaymentSucceeded(event.data.object as Stripe.Invoice);
      break;
    case 'invoice.payment_failed':
      await handlePaymentFailed(event.data.object as Stripe.Invoice);
      break;
  }

  // Mark event as processed
  await supabase.from('processed_webhooks').insert({
    event_id: event.id,
    event_type: event.type,
    processed_at: new Date().toISOString(),
  });
}

Step 3: Error Handling with Retry

async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
  try {
    // Update organization subscription status
    const { error } = await supabase
      .from('organizations')
      .update({
        subscription_status: subscription.status,
        subscription_id: subscription.id,
        current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
      })
      .eq('stripe_customer_id', subscription.customer);

    if (error) throw error;
  } catch (err) {
    // Log error but don't throw — Stripe will retry
    console.error('Failed to process subscription.created:', err);
    // Could also send to Sentry here
    throw err; // Re-throw to trigger Stripe retry
  }
}

Performance: What the Numbers Show

Wintura has been in production since March 2026. Here are the verified performance metrics:

Core Web Vitals (May 2026)

MetricTargetActual
LCP< 2.5s1.8s
INP< 200ms85ms
CLS< 0.10.02

API Response Times

EndpointP50P95P99
/api/proposals (list)45ms120ms280ms
/api/proposals (create)8.2s12.5s18.3s
/api/proposals/[id]/pdf1.2s2.8s4.5s
/api/auth/session12ms35ms85ms

Note: Proposal creation is slow because it includes AI generation. The 8.2s P50 is acceptable for a "generate document" action — users expect AI operations to take time.

Error Rate

PeriodRequestsErrorsError Rate
March 202612,847230.18%
April 202628,392410.14%
May 2026 (to date)45,128520.12%

Error rate trending down as edge cases are discovered and fixed.

The Production Lift Pattern Applied

Wintura implements all five patterns from the Production Lift:

PatternWintura Implementation
Multi-tenant RLSPostgres RLS on all user-facing tables with org_id isolation
Auth hardeningNextAuth v5 + rate limiting + password reset + session management
Webhook verificationStripe signature verification + idempotency tracking
Structured errorsTyped error responses with Sentry integration
E2E test coverage24 Playwright spec files covering all critical journeys

The same five patterns are what transform any Bolt, Lovable, v0, or Cursor prototype into production-ready code.

What Wintura Cost to Build

Wintura was built internally as a reference implementation. If it were a client project, it would fall into the MVP Sprint Standard tier:

  • 6 core flows (proposals, customers, products, billing, team admin, organization settings)
  • Custom design system (not off-the-shelf Tailwind components)
  • Full admin dashboard
  • Analytics integration (Vercel + Sentry)
  • AI pipeline integration

MVP Sprint Standard: €12,900 fixed, 6 weeks — which is exactly what Wintura took to build.

Frequently Asked Questions

Can I see Wintura in production?

Yes. Visit wintura.ai. The demo account lets you explore the interface without creating proposals.

Is this the only project Soatech has shipped?

Wintura is the latest shipped project and the primary reference build. It demonstrates the full production hardening stack. Additional client projects follow the same patterns.

Can you build something similar for my industry?

Yes. The Wintura patterns (multi-tenant RLS, AI pipeline, sealed audit trail, e2e tests) apply to any B2B SaaS. The Technical Blueprint (€2,500, 5 days) scopes your specific requirements before committing to a full build.

What if I already have a prototype?

If you built in Bolt, Lovable, v0, or Cursor and need production hardening, the Production Lift (€3,500, 1 week) applies the same five patterns to your existing codebase.

How do I verify the claims in this post?


Ready to build your own reference implementation? The MVP Sprint Standard (€12,900 fixed, 6 weeks) ships 6 core flows with the same production hardening as Wintura. Or start with the Technical Blueprint (€2,500, 5 days) to scope your requirements first.

wintura-aireference-buildNext.jsClaude-Sonnetmulti-tenantproduction-readyB2B-SaaS

Ready to build something great?

Architect-led, AI-accelerated. Let's turn your idea into a shipped product.

Book a 30-min Blueprint call