Why a monorepo
The partners dashboard and the embeddable web SDK had three things in common: a chat component (LLM-backed support agent), analytics emission (every interaction is a span), and i18n. Three copy-paste-shaped problems.
Turborepo with pnpm workspaces lets both apps consume the same package source. A change
to the chat UI ships to both surfaces in one PR. The dependency graph is explicit in
turbo.json, so CI rebuilds only what changed.
The packages
- @pagrin/ai-chat — React component + hook backed by the Vercel AI SDK. Streaming responses, tool-call rendering, conversation persistence via callbacks (the app decides where to store).
- @pagrin/analytics — OpenTelemetry wrapper. Every chat message, every CTA click, every page view emits a span. Exporters are pluggable: Sentry, Honeycomb, Vercel Analytics, or a local development exporter.
- @pagrin/i18n — typed translation keys with a
validate-translationsscript that flags drift between locales in CI, plus atranslatescript that pipes new keys through an LLM for a first-draft translation. - @pagrin/ui — design system primitives (Button, Input, Modal, Toast) on top of Radix with Tailwind-styled defaults.
What I'd do differently
I overinvested in i18n tooling early — the translate script was clever but only ran twice
before we shipped a hardcoded English MVP for the first three partners. Tooling that
doesn't get used twice in production is debt, not infrastructure.