Skip to main content

Building a Compliance Engine for Alcohol Shipping Across All 50 States and DC

May 15, 2026 · Updated October 2026 · 10 min read

First written in May 2026 and updated in October 2026 with current figures.

Beverage-alcohol compliance in the US is a combinatorial problem: 50 states plus DC, two distribution channels (direct-to-consumer and three-tier wholesale), and constant rule changes, with every cell in that matrix carrying its own requirements. The source text is scattered across state statutes, federal rulings and revenue-department bulletins on more than 50 government sites, so much of the checking is still done by hand. The same problem reaches wineries, breweries, distilleries and cideries.

Ratify is a personal project I built: a compliance engine for alcohol producers and the compliance professionals who serve them, covering wine, beer and spirits shipping in all 51 US jurisdictions (50 states plus DC) through 753 provisions of state law. This post is about the decisions behind it: why the compliance service and the dashboard run separately, why the yes-or-no on an order comes from written rules and never from an AI model, how quoted legal text is copied from the source document instead of written by a model, and how every answer stays checkable.

Why the Backend and the Dashboard Run Separately

The default modern starting point is a Next.js monolith on Vercel. I rejected that early. Three constraints made it the wrong fit:

  • Function duration ceiling. Vercel functions cap at 800 seconds. Long-running batch jobs can run for many minutes. Some compliance workflows need to span tens of minutes.
  • No persistent workers. Regulatory monitoring needs scheduled jobs that run continuously, not invocation-by-invocation serverless functions.
  • No built-in API gateway. A compliance API needs rate limiting, API versioning, and middleware patterns. Bolting that on top of Next.js is a worse experience than getting it from FastAPI.

The split:

  • Railway / FastAPI (Python) carries the compliance engine, AI extraction pipeline, background workers, and integrations (Commerce7, Offset Commerce). Standard Docker containers, portable across hosts.
  • Vercel / Next.js (TypeScript) carries the dashboard and edge-cached server components.
  • Supabase Postgres holds the rules corpus and each workspace's data, with Row-Level Security walling off each workspace. The assistant pulls matching rule rows with plain SQL and hands them to the Citations API; vector search waits until the corpus outgrows it.

Two deployment targets instead of one, accepted in exchange for long-running workers, persistent queues and Python's AI libraries. FastAPI gives Python's full set of AI libraries (the Anthropic SDK, Pydantic) without shoehorning into Node.js. Vercel stays for what Vercel is good at: fast SSR, edge caching, Server Components.

AI Never in the Critical Path

In plain terms: the yes-or-no answer on an order comes from written rules, never from an AI model. AI sits next to that decision, never inside it. Compliance checks and tax calculations are deterministic rules-engine operations. AI does what AI is good at: natural-language compliance questions, regulatory document extraction, expansion planning, and audit report synthesis. None of these block an order.

Why:

  • Compliance decisions must be reliable, fast (under 100ms), and auditable. LLM latency (1-5 seconds typical) is incompatible with real-time order gating. LLM hallucination risk is incompatible with tax calculations. Regulatory requirements demand deterministic, reproducible results.
  • Reproducibility. The same order under the same rules must produce the same answer on every check. A non-deterministic system fails the most basic compliance test.
  • Failure isolation. An AI provider outage degrades assistive features (the natural-language assistant goes down) but never breaks the core product. Order gating and tax math keep running. Audit answers stay reproducible.

The concrete shape:

  • Real-Time Order Compliance Gate is a pure rules engine. Looks up jurisdiction rules from Postgres, returns allowed/denied with a structured reason code.
  • Tax Calculation is a pure rules engine. Walks tax-rule lookup tables for jurisdiction × product type × channel.
  • AI workloads: natural-language compliance questions, regulatory document extraction, expansion advice, audit report synthesis. None block an order.
flowchart TD
  accTitle: Ratify split architecture with AI beside the critical path
  accDescr: A producer dashboard on Vercel and Next.js calls a FastAPI service on Railway. The deterministic critical path runs the order compliance gate and tax calculation as pure rules engines against Supabase Postgres, with a target of under 100 milliseconds. AI workloads, two-pass Citations extraction and natural-language compliance questions, sit beside the critical path and never block an order; they call Claude Sonnet and Haiku directly (Anthropic only) through the Citations API, and the assistant pulls rule rows from Postgres with plain SQL.
  client["Producer dashboard (Next.js on Vercel)"] --> api["FastAPI on Railway"]
  subgraph critical["Critical path: deterministic, target under 100ms"]
    gate["Order compliance gate"]
    tax["Tax calculation"]
  end
  subgraph assistive["Beside the path: AI assistive, never blocking"]
    extract["Two-pass Citations extraction"]
    qa["Natural-language compliance Q and A"]
  end
  api --> gate
  api --> tax
  api --> extract
  api --> qa
  extract --> claude["Claude Sonnet 4.6 / Haiku 4.5 (Anthropic only, Citations API)"]
  qa --> claude
  gate --> db[("Supabase Postgres: jurisdiction rules + RLS")]
  tax --> db
  extract --> db
  qa -->|"rule rows by plain SQL"| db

Two-Pass Citations Extraction

A compliance product is only useful if its rules are correct, and "correct" means traceable to a verbatim source. Naive single-pass LLM extraction produces plausible-looking JSON with no audit trail: no way to retrace which span of the source document produced which extracted value, no way to defend a conclusion in a compliance review, no way to ground a regression test.

The fix is two-pass extraction using Anthropic's Citations API plus Structured Outputs:

  • Pass 1 locates citation spans in the source document, exact verbatim quotes anchored to character offsets.
  • Pass 2 receives the cached document with the pass-1 spans and is prompted to fill a typed schema from those spans.
  • Ephemeral prompt caching keeps the source document cached for five minutes, so the second pass reads it at a tenth of the normal input rate.
  • Model tiering: Claude Sonnet 4.6, with citations, derives rules from statutes, grades extracted values against their sources and answers the assistant's questions; Claude Haiku 4.5 runs the routine two-pass extraction passes.

The two passes are separate calls: the first returns cited spans from the source document, and the second fills a typed schema from those spans. Where a model helps derive a rule from a statute, the quoted legal text is copied straight from the source document and never written by the model, because a convincing invented citation is worse than none.

Outcomes:

  • Every extracted compliance value carries a verbatim citation back to the source document. Replayable in CI.
  • Recorded test cases freeze the cited spans, not just the output values, so a change to a saved source page, the HTML cleaner or an extractor fails a test.
  • Higher per-call latency and cost than single-pass extract. Offset by prompt caching and Haiku-tier routing on cheap paths.

Every answer the engine serves carries its primary legal citation and a confidence tier, and when confidence falls below the threshold it refuses to answer rather than guess. An adversarial audit of 37 sampled high-confidence answers, checked against primary sources located independently of Ratify's own citations, verified all 37, with zero fabricated citations.

Keeping Each Workspace's Data Separate

Each workspace's data must stay separate, in the database and in the AI path. A filter in application code can be missed on one query, and a shared prompt cache could carry one workspace's context into another workspace's answer.

The design has three layers:

  1. Row-Level Security at the database layer. Every workspace-scoped table carries a tenant_id column with a policy that compares it with the workspace claim in the signed-in user's token, so queries made on a user's behalf can't cross from one workspace to another.
  2. A CI check confirms that row-level security stays enabled on every workspace table.
  3. No shared prompt prefix. In the assistant, the workspace id sits at the start of the system prompt, ahead of the cache breakpoint, so a cached prompt prefix is never shared across workspaces.

Rules are stored by jurisdiction type rather than by state: one jurisdiction_rules table holds federal, state, county and city rules, with a country level reserved for international work, so a new level arrives as an enum value, not a new table.

Covering All 51 Jurisdictions, in Batches

The biggest extraction effort covered the wine direct-to-consumer shipping rules of every US jurisdiction, 50 states plus DC, in four batches. The first batch set the pattern and the later ones reused it. Recorded test cases for the wine rules of all 51 jurisdictions check on every build that each cited span still matches its quote in a saved copy of the source page.

Production Observability

  • Sentry for exception tracking, with PII scrubbing in middleware before any payload leaves the FastAPI process.
  • Daily per-workspace and global spending caps on the AI assistant, checked before each paid call.
  • 30+ GitHub Actions workflows, including CI, nightly smoke tests, and security scans. The nightly smoke tests catch upstream regulatory-page format changes before a stale page can turn into a wrong answer.
  • Change tracking for the regulatory data: every detected change in the law is logged, staged, and published on a public changelog, so anyone can check how current the data is.

By the Numbers

  • 753 provisions of state law across all 51 US jurisdictions, covering wine, beer and spirits shipping
  • 37 of 37 sampled high-confidence answers verified, zero fabricated citations
  • Recorded test cases for the wine rules of all 51 jurisdictions, checking every cited span against a saved copy of its source page on every build
  • 788+ test files
  • 130+ database migrations
  • 30+ GitHub Actions workflows
  • Production deployed: API on Railway, web on Vercel, Supabase us-east-1

Lessons

Compliance answers have to be reproducible. The same order under the same rules must get the same answer every time. Putting AI next to the rules engine instead of inside it is the decision that matters most.

Two-pass citations turn LLMs into auditable tools. Freezing the cited spans in recorded test cases, not just the output values, is what makes a change to a saved source page, the HTML cleaner or an extractor fail a test in CI instead of leaking a wrong rule into production.

A jurisdiction-agnostic data model costs nothing on day one. Using jurisdiction_rules with a typed ENUM instead of state_rules adds maybe ten characters of code. Refactoring away from state_rules after every rule lookup is built against it would cost weeks.

Put data isolation in the database. Application checks are not enough on their own: one missed WHERE clause and data crosses workspaces. Row-level security in Postgres catches what application code misses.

Tech Stack

  • Backend: FastAPI (Python 3.12) on Railway
  • Frontend: Next.js 16 on Vercel
  • Database: PostgreSQL on Supabase with Row-Level Security; plain SQL retrieval for the assistant (vector search deferred)
  • AI: Anthropic Claude Sonnet 4.6 (primary), Claude Haiku 4.5 (cheap-path)
  • Citations: Anthropic Citations API with ephemeral prompt caching (5m TTL, 0.10x read rate)
  • Model access: Anthropic only, with daily per-workspace and global caps on assistant spend checked before each paid call
  • Observability: Sentry with PII scrubbing
  • CI/CD: 30+ GitHub Actions workflows (CI, nightly smoke tests, security scans)
  • Integrations: Commerce7 and Offset Commerce (order intake from direct-to-consumer platforms)