soatech

Reference build

Building wintura.ai

wintura.ai is a live B2B SaaS that turns messy inbound briefs — email, PDF, Slack, voice — into structured, signable proposals, isolated per tenant. It is Soatech’s reference build, and it runs the exact stack this studio builds on. This page is the evidence, not a brochure. It’s live — open it alongside this write-up and check the claims.

open wintura.ai

Why a studio has its own product

wintura.ai and Soatech are run by the same people, and it is the reference build by design. Most studios can only show client work under NDA, or nothing at all. wintura was built end to end so the studio’s production claims are inspectable rather than asserted — and because it is the studio’s own product, every layer is open to you, which is the rest of this page.

Running it in production is also what keeps the stack current: the patterns Soatech brings to your codebase are ones it maintains live, not slides.

Architecture

One constraint shaped everything: every read and write must be provably scoped to a single tenant, and the model’s output must never be trusted unchecked. The request path, end to end:

wintura.ai — architecturelive

Ingest

Email
PDF
Slack
Voice

NextAuth v5

sets tenant context

per-tenant boundary — RLS-scoped

Extraction pipeline

extract → validate → ground in history

PostgreSQL, Row-Level Security

tenant_id = current_setting()

Act

draft → review → e-sign + Stripe

White-label portal

per-tenant delivery

Observability: SentryZero-retention APIsGDPR, EU SCCsPlaywright e2e

Multi-tenancy with Row-Level Security

Tenant isolation is enforced at the database with PostgreSQL Row-Level Security, not with application-layer WHERE clauses. The reason is the failure mode: app-layer filtering depends on every query, forever, remembering to filter — one missed clause is a cross-tenant leak. RLS makes the database itself refuse to return another tenant’s rows, whatever the application asks.

The application sets the tenant context once per request; the policies do the rest. This is the single most important decision in a multi-tenant SaaS, and the one AI-generated code most often gets wrong.

policy — proposals table

create policy tenant_isolation
  on proposals
  using ( tenant_id = current_setting('app.tenant')::uuid );

-- context set once per request, inside a transaction:
set local app.tenant = '<tenant-uuid>';

The LLM extraction pipeline

Input arrives as email, PDF, Slack messages, or voice. The pipeline extracts structured fields, but the model’s output is never trusted directly:

  • outputs are validated against a schema before they touch the database; malformed or low-confidence extractions are flagged for human review, not silently saved;
  • retrieval is grounded in the tenant’s own accepted proposals, so suggestions reflect that customer’s real history rather than a generic prior;
  • model APIs are zero-retention, so tenant content is not kept for training.

The engineering here is mostly in the guardrails, not the prompt. That is what separates a demo from something you can put in front of a paying customer.

Payments and signatures

Stripe handles card and ACH. Proposals are signed inline, and the signature is bound to an audit trail — who signed what, when, against which version — so an accepted proposal can be reconstructed and defended later. Payment state and proposal state are reconciled, not assumed.

Auth, portals, and what is tested

NextAuth v5 for authentication; white-label client portals per tenant. Coverage is Playwright end-to-end on the paths that carry money and data — the ones where a regression is expensive — not a vanity percentage. Sentry surfaces errors in production.

Compliance posture

GDPR-aligned, EU Standard Contractual Clauses, zero-retention model APIs. For an EU B2B buyer this is a purchase precondition, not a nice-to-have, so it was designed in rather than bolted on.

What we would do differently

Two things, and both are about day-two ergonomics rather than the original call. Row-Level Security was right and we’d choose it again — but its real cost isn’t writing the policies, it’s that a request which forgets to set the tenant context fails silently, returning nothing instead of erroring. That shows up as a confusing empty screen, not a stack trace. We’d build the typed tenant-context wrapper and the test fixtures that fail loudly on a missing context on day one — before the first such bug, not after it.

And we reached for the model slightly early. Some of what the first version handed to the LLM was ordinary structured data that deterministic parsing and templating handle more cheaply and more predictably. We’d move the inference call later in the pipeline — parse and validate what is unambiguous first, and spend a model call only on the fields that are genuinely ambiguous. The guardrails still matter most; the refinement is to give them less to guard.

How this maps to your product

If you are a funded B2B SaaS with an AI feature stuck between “works in a branch” and “safe in front of customers,” the gap you are looking at is the one this product crosses: tenant isolation, LLM reliability, payments, auditability. That is what an Embedded Architect ships into your codebase.