Work / lmn.bar

lmn.bar

DTC decision dashboard

Product · DTC analytics

Visit live site

Multi-tenant SaaS console for DTC operators — Shopify + Meta + Google + Klaviyo unified, with a Claude-powered agent grounded in tenant data via Composio MCPs.

lmn.bar — Senior PM AI case study

AI-native operations console for DTC brands. Streams Claude-generated recommendations grounded in live Shopify, Meta Ads, Google Ads, and Klaviyo data via Composio MCPs, with guardrails, audit, and cost controls.

What it is, in one paragraph

lmn.bar is a multi-tenant SaaS console that gives DTC operators one place to see revenue, contribution margin, ROAS by channel, and inventory health, plus a streaming AI agent that can read across their connected tools and propose (or, where allowed, take) actions. Each tenant maps to one Shopify store and connects Meta Ads, Google Ads, and Klaviyo through Composio MCPs. Claude (Sonnet 4.5 default, Opus 4.1 / Haiku 4.5 fallback) does the reasoning over tool-bound schemas, with server-side guardrails on every parsed recommendation.

Problem it solves

Revenue, ad spend, and inventory live in different tools, so operators rebuild the same pivots in spreadsheets every week.
Channel ROAS and contribution margin require stitching Shopify orders + COGS + Meta + Google by hand.
Creative performance decisions (scale, kill, evergreen, refresh) are gut-feel because nobody has a comparable per-ad score.
Stockouts and broken-run inventory hit revenue before anyone notices.
Generic AI chat tools can summarize a CSV but can't safely act on Shopify or change a Meta budget; agents that can act don't have guardrails or audit.

What lmn.bar solves, concretely

One financial truth across Shopify + ads. Revenue, COGS, ad spend, contribution margin, and unit economics in one view, over time and in totals.
ROAS by channel and per-creative health score. Meta and Google ads with ROAS vs target, evergreen status, and a 1-week vs 2–4-week trend so spend slippage is visible early.
Inventory awareness tied to revenue. Stock levels, low-stock, coverage, and broken-run alerts at variant level.
Grounded AI recommendations, not chat fog. Streaming Claude output with mandatory tool calls against the user's own data, parsed into structured recommendations and validated against tenant guardrails.
Safe action surface. Guardrails on max_scale_percent, min_roas, max_daily_spend_increase, plus custom rules; every action is audit-logged; agent endpoint defaults to read-only MCP tools.
Resilience and cost control. Memory + DB metrics cache, HMAC-verified Shopify webhooks that invalidate on order events, per-tenant Claude cost tracking with budget alerts and optional hard block, fallback to cached or metric-derived recommendations on Claude failure.

Tech stack

Frontend
Next.js 14+ (App Router, Server Components, TypeScript strict)
React 18, Tailwind CSS, shadcn/ui + Radix UI primitives
TanStack Query for client data fetching and dedupe
Recharts for charts, Phosphor Icons, Sonner toasts
react-markdown + remark-gfm for streaming agent output
Backend
Next.js API Routes (Node runtime), Vercel deploy
Supabase Postgres with Row Level Security for multi-tenant isolation
Supabase Auth with multi-tenant profiles + memberships
36+ tracked migrations in supabase/migrations/
Stripe billing with webhook reconciliation
Resend + React Email for transactional, Loops for lifecycle
Attio CRM sync, Pipedream, Notion, GHL webhook integrations
AI layer (Claude + Composio MCPs)
Anthropic SDK with snapshot-versioned model IDs and a MODEL_FALLBACK_ORDER
Default claude-sonnet-4-5-20250929; Haiku 4.5 for fast tasks; Opus 4.1 for deep reasoning
Streaming agent endpoint POST /api/agent returns a ReadableStream; supports per-tenant rate limits and structured recommendation JSON appended at end of stream
Multi-round tool calling: tool-use detection in stream, execution via Composio MCP, conversation continuation
Composio SDK as the MCP layer for Shopify, Meta Ads, Google Ads, Klaviyo (Stripe deferred)
Tool definitions in lib/claude/tools/ mapping MCP actions to Claude input_schema; getAllTools({ readOnlyForAgent: true }) strips mutating tools by default
Dynamic tool filtering by which MCPs the tenant has actually connected
Prompt builder with prompt cache (5-min TTL), guardrails injector, metrics injector, brand-context injector, and tool injector
Response pipeline: response-parserrecommendation-formatterinsight-extractorresponse-processor (parses streamed text into validated recommendations against contracts)
Server-side recommendation guardrails re-validate parsed output, same as /api/actions
Reliability and ops
Rate limiter + p-queue request queue (4 req/min default, configurable)
Retry with exponential backoff on 429/5xx via withClaudeRetry
Error classifier + user-friendly messages + monitoring (logClaudeError, getErrorStatistics)
Fallback recommendations from cache or computed from cached metrics when Claude is unavailable
Cost tracker per tenant + model, budget alerts with optional hard block
Cache stack: in-memory 30s + Postgres 5min for metrics; ShopifyQL fresh-only for MVP correctness; HMAC-verified Shopify webhooks (orders/create, paid, updated, cancelled, refunds/create) trigger tenant cache invalidation
Single full-fetch + client-side filter/sort/pagination for inventory and ads tables (instant tab/sort/page changes)
Quality, analytics, and CI
Vitest for unit, integration, and component tests; mocks via lib/contracts/mocks.ts
GitHub Actions CI: type, lint, format, test, build; pre-push git hook runs the same
PostHog as primary analytics + error tracking (client + server, with instrumentation.ts flushing on edge), Data Pipeline destinations to Meta CAPI and Google Enhanced Conversions
Meta Pixel with event_id deduplication shared with PostHog/CAPI
hCaptcha on marketing forms
Architecture principles (from .cursorrules)
Interface-Driven Development via lib/contracts/ (database, api, services, mocks) so multiple work streams progress in parallel against a typed boundary
Mock-first development; switch to real APIs when ready
Multi-tenant isolation enforced server-side via RLS and a tenant context utility
Security first: input validation (Zod), guardrails enforced server-side, audit log on all actions

AI / agent architecture (deep dive for interview)

Why this shape, not a generic chat wrapper:

Grounded by tools, not by prompt. The system prompt is built per-request with live tenant metrics, guardrails, brand context, and a filtered tool list reflecting that tenant's connected MCPs. The model can't hallucinate ads it can't see; it has to call a tool.
Server-validated structured output. Streaming text is parsed into structured Recommendation objects (type, confidence, revenue impact, action params), then re-checked against guardrails before they're allowed to surface as actionable. Same validator runs in /api/actions so prompt-injected 'just do it' attempts fail at execution.
Read-only by default. /api/agent defaults to readOnlyMcpTools: true. Mutating actions live behind the actions API with explicit user approval and audit logging.
Streaming UX first-class. Custom streamClaude helper wraps MessageStream into a ReadableStream and runs processAgentResponseAsync so the recommendation JSON is appended at end-of-stream without blocking the visible text.
Cost and reliability are PM concerns, not just infra.
Snapshot-versioned model IDs (no aliases in prod)
Three-tier model strategy by task complexity
Budget per tenant with optional hard block on /api/agent
Three-step fallback: cached recs → metric-derived recs → empty with explanation
Chat with persistence. Conversations and messages persisted in chat_conversations / chat_messages; POST /api/agent returns X-Conversation-Id. Optional 'share AI conversations' analytics flag.

Data model highlights

tenants, tenant_settings, tenant_stores, users, profiles, memberships (multi-tenant + multi-store ready)
mcp_auth_configs (per-tenant Composio Auth Config IDs, with env-var fallback)
shopify_connections, meta_ads_connections, google_ads_connections
metrics_cache, shopifyql_cache with date-range and tenant-scoped uniqueness
recommendations, guardrails, inventory_exclusions
chat_conversations, chat_messages (with archive support)
outbox_events for reliable webhook fan-out
Stripe trial + billing fields on tenants

All tables RLS-scoped to tenant; service-role used only inside vetted server paths (e.g. webhook cache invalidation).

Product surface

Marketing site: hero with hosted product video, features carousel (financial, marketing, inventory, product), pricing, contact, scorecard ('are you a fit?') and operator health-check funnels
Auth: signup, login, forgot/reset password; Shopify install/bootstrap with conflict resolution; OAuth state + base URL utilities
Onboarding: status + completion flow gated on Shopify connect, COGS, billing
Dashboard
Financial overview (KPIs: AOV, CAC, breakeven ROAS, cost-cap ROAS, new vs returning)
Ad performance with creative health cards and per-channel ROAS
Inventory section with broken-run + low-stock awareness
ShopifyQL section with contribution margin chart, total sales chart, financial breakdown
Streaming chat-with-data widget (floating + full panel) with conversation history, archive, rename
Recommendations panel with details modal and action confirmation
Query status widget and last-updated indicator
Settings: connection cards (Shopify, Meta, Google), financial settings, inventory exclusions, privacy, MCP connections health
Admin / dev: health-check page, components showcase, demo and test-agent routes

What I'd talk about in an interview

1. Choosing 'tools over RAG' for a vertical agent.

Because the data lives in live SaaS tools that already have great APIs and constant change, MCPs + Composio give us auth, schema, and execution for free. RAG would freeze a snapshot and need a sync pipeline.

2. Where I drew the safety line.

Default read-only on the agent endpoint, structured output validation, and a single guardrails validator shared between agent recommendations and the actions API. Audit log + budget caps make the surface defensible to a brand.

3. Streaming as a product feature, not just a tech detail.

First-token latency is the perceived 'is this thing alive' signal. Wrapping MessageStream in a ReadableStream and parsing structured recs at end-of-stream lets us show text immediately and surface actions only after server-side guardrail checks.

4. Multi-tenant from day one.

RLS + tenant-scoped Composio Auth Config IDs, with env-var fallback for shared OAuth apps during early adoption. This is the boring choice that makes 'add a second customer' a 1-day task.

5. Cache policy that admits what it doesn't know.

Metrics cache is generous (memory 30s, DB 5min), but ShopifyQL is fresh-only for MVP correctness. Webhooks invalidate on order/refund events. Picking the right consistency for the right surface beats 'cache everything.'

6. Cost is a product surface.

Per-tenant cost tracking, budget alerts, and an optional hard block on /api/agent keep AI economics inside a real margin model, not a 'see you next month' surprise.

7. Interface-Driven Development to ship faster solo.

lib/contracts/ defines the API/service/database boundary. Mocks ship first, real implementations swap in behind them. The same approach scales to a team without rewrites.

Quick stats

Stack tags: Next.js 14, React 18, TypeScript, Supabase, Postgres, RLS, Vercel, Anthropic Claude (Sonnet 4.5 / Opus 4.1 / Haiku 4.5), Composio MCP, Tailwind, shadcn/ui, TanStack Query, Recharts, Stripe, Resend, Loops, Attio, PostHog, Meta Pixel, Vitest
Integrations: Shopify, Meta Ads, Google Ads, Klaviyo (Stripe MCP deferred)
Tests: Vitest unit + integration + component; CI on push/PR via GitHub Actions; pre-push hook runs full pipeline locally
Migrations tracked: 36+ in supabase/migrations/
Repo: lmnbar-v2 (private)