The production stack
This page is the handoff, not a mockup: the Supabase schema and the n8n workflow below are written, downloadable, and ready to run against a fresh project.
Supabase schema + RLS
venues · venue_members · invoices · wines · storage bucket · realtime
create table public.wines (
id uuid primary key default gen_random_uuid(),
venue_id uuid not null references public.venues (id),
invoice_id uuid references public.invoices (id),
name text not null,
vintage text not null default 'NV',
style text check (style in
('red','white','sparkling','rose','dessert')),
qty integer not null default 0,
unit_cost numeric(10,2) not null, -- manager-only
bottle_price numeric(10,2) not null,
glass_price numeric(10,2),
taste text, palate jsonb, pairings text[],
pitch text not null default '',
published boolean not null default false
);
-- venue isolation
create policy "members read wines" on public.wines
for select using (is_member(venue_id));
-- servers never see cost: column-level grant
revoke select on public.wines from authenticated;
grant select (id, name, vintage, style, bottle_price,
glass_price, taste, palate, pairings, pitch, ...)
on public.wines to authenticated;Row-level security isolates venues; a column-level grant plus a floor_list view means a server's session cannot read unit_cost even with the API open in a browser tab. Realtime is enabled on wines, so a publish lands on every phone.
n8n workflow
importable JSON · webhook to Supabase in 8 nodes
- 1Invoice upload webhookPOST /wine-invoice, multipart file + venue_id
- 2Build data URLbinary to base64 for the vision payload
- 3GPT-4o Vision parsestrict json_schema response: line items, taste, palate, pairings, one-sentence pitch
- 4Price and shape rowshouse pricing rule applied in code, not by the model
- 5Create invoice rowSupabase insert, service role
- 6Split wines + create wine rowsone row per parsed line item
- 7Respond to uploadreturns count + review status to the app
Import into any n8n instance, attach OpenAI + Supabase credentials, point the app's dropzone at the webhook URL. The vision call uses a strict JSON schema, so malformed output cannot reach the database.
What this demo runs vs. production
The same React + Tailwind front end you are using, with the vision parse running as a real AI call inside a Next.js route and state in your browser, so anyone can try it with zero accounts. The parse you watched on the manager view is not canned.
Identical pipeline on your infrastructure: Supabase for Postgres, Auth, storage and realtime; the n8n webhook workflow calling GPT-4o Vision; this front end pointed at Supabase with manager and server roles enforced by RLS.