Tyquill is a newsletter writing workspace for collecting sources, generating drafts, and editing long-form articles.
It started as a general SNS assistant, then narrowed to the repeated work of newsletter writing after user and market research.
In a two-person team, I owned the backend and AI agent work, and also handled product planning, deployment, and user onboarding.
- Type
- Full-stack SaaS
- Surface
- Editor, API, extension, NestJS agent
- Focus
- Collection to draft generation
- Scope
- Backend, AI agent, product planning, onboarding
- Stack
Architecture
Tyquill has three main parts: a Chrome extension for clipping, a React editor workspace, and a NestJS backend. The backend runs the product APIs and the LangGraph generation flow.
Overview
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ffffff", "primaryTextColor": "#1b1b1b", "primaryBorderColor": "#1b1b1b", "lineColor": "#555555", "secondaryColor": "#f6f7f8", "tertiaryColor": "#ffffff", "fontFamily": "Inter, sans-serif"}} }%%
flowchart LR
subgraph captureGroup["tyquill-ext-client"]
sources["Web pages"] --> extension["WXT content script"]
extension --> clipper["Readability and Turndown"]
end
subgraph appGroup["tyquill-client"]
app["Vite React app"] --> editor["TipTap editor"]
app --> services["TanStack Query services"]
end
subgraph serverGroup["tyquill-server"]
api["NestJS API modules"] --> db["PostgreSQL via MikroORM"]
api --> agent["agents module"]
agent --> workflow["ai-workflows LangGraph.js"]
workflow --> vertex["Google Vertex AI"]
end
clipper --> api
services --> api
workflow --> draft["Newsletter title and content"]
draft --> editor
editor --> output["Article, archive, repurpose"]
Drill down
Product boundary layer The extension and web app both call the NestJS server. Product APIs and LangGraph generation live in that server.
Product boundary layer
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ffffff", "primaryTextColor": "#1b1b1b", "primaryBorderColor": "#1b1b1b", "lineColor": "#555555", "secondaryColor": "#f6f7f8", "tertiaryColor": "#ffffff", "fontFamily": "Inter, sans-serif"}} }%%
flowchart TD
extension["tyquill-ext-client"] --> capture["Capture and clipping surface"]
client["tyquill-client"] --> workspace["Editor workspace"]
capture --> server["tyquill-server"]
workspace --> server
client --> web["Vite React workspace"]
extension --> clipper["WXT clipping surface"]
server --> backend["NestJS backend modules"]
server --> agent["NestJS agents and ai-workflows"]
agent --> langgraphNode["LangGraph.js workflow"]
- tyquill-server is the main backend and generation runtime
- The Python agent repo exists separately, but the current product path does not use it for generation
Capture layer The extension captures pages or selected text, extracts readable content, converts it to Markdown, and sends it to the backend.
Capture layer
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ffffff", "primaryTextColor": "#1b1b1b", "primaryBorderColor": "#1b1b1b", "lineColor": "#555555", "secondaryColor": "#f6f7f8", "tertiaryColor": "#ffffff", "fontFamily": "Inter, sans-serif"}} }%%
flowchart TD
page["Web page or selection"] --> contentScript["entrypoints content script"]
contentScript --> clipper["webClipper"]
clipper --> readability["Mozilla Readability"]
clipper --> turndown["Turndown Markdown"]
readability --> result["content, metadata, plain text"]
turndown --> result
result --> extServices["scrap and upload services"]
extServices --> serverApi["NestJS scraps and content API"]
serverApi --> postgres["PostgreSQL"]
postgres --> workspace["Client library and scrap views"]
- The extension uses WXT entrypoints, a content script, shadow-root UI, and service modules
- Captured content is saved as scrap material before generation
Application and backend layer The web app owns routes, API clients, state stores, and the TipTap editor. The NestJS server owns auth, persistence, queues, article modules, and generation.
Application and backend layer
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ffffff", "primaryTextColor": "#1b1b1b", "primaryBorderColor": "#1b1b1b", "lineColor": "#555555", "secondaryColor": "#f6f7f8", "tertiaryColor": "#ffffff", "fontFamily": "Inter, sans-serif"}} }%%
flowchart TD
routes["authenticated routes"] --> query["TanStack Query services"]
routes --> editor["TipTap editor"]
routes --> state["Zustand stores"]
query --> api["NestJS API"]
api --> auth["auth and users"]
api --> content["scraps, folders, tags, uploads"]
api --> writing["articles, archive, chat, repurpose"]
api --> queue["queue, notifications, slack bot"]
api --> db["PostgreSQL via MikroORM"]
editor --> collaboration["TipTap collaboration and Yjs"]
- The client repo includes routes, services, stores, i18n, hooks, and TipTap templates
- The server module map includes auth, users, scraps, articles, article archive, article chat, content, folders, queue, repurpose, uploaded files, and writing styles
Generation layer Newsletter generation runs inside the NestJS server through NewsletterAgentService and NewsletterWorkflowLanggraphService.
Generation layer
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ffffff", "primaryTextColor": "#1b1b1b", "primaryBorderColor": "#1b1b1b", "lineColor": "#555555", "secondaryColor": "#f6f7f8", "tertiaryColor": "#ffffff", "fontFamily": "Inter, sans-serif"}} }%%
flowchart TD
request["generation request"] --> agentService["NewsletterAgentService"]
agentService --> graphService["NewsletterWorkflowLanggraphService"]
graphService --> prepare["prepareScrapContent"]
graphService --> processPdf["processPdfContent"]
prepare --> aggregate["aggregator"]
processPdf --> aggregate
aggregate --> generate["generateNewsletter"]
generate --> route["conditional route"]
route --> reflector["articleReflector"]
reflector --> generate
route --> rewrite["rewriteWritingStyle"]
route --> title["generateTitle"]
rewrite --> title
title --> result["title, content, warnings"]
generate --> vertex["Gemini on Vertex AI"]
reflector --> vertex
rewrite --> vertex
title --> vertex
graphService --> langfuse["Langfuse tracing"]
result --> stream["streaming progress events"]
- Scrap content and PDF content are prepared in parallel before aggregation
- The flow reflects on the draft, optionally rewrites it with the user's writing style, then generates a title
- The server maps LangGraph stream events into progress events for the frontend
Editing and repurpose layer Generated drafts return to the editor. Users can revise, archive, ask questions about the article, and repurpose the text.
Editing and repurpose layer
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ffffff", "primaryTextColor": "#1b1b1b", "primaryBorderColor": "#1b1b1b", "lineColor": "#555555", "secondaryColor": "#f6f7f8", "tertiaryColor": "#ffffff", "fontFamily": "Inter, sans-serif"}} }%%
flowchart TD
result["generated title and content"] --> articleService["articles service"]
articleService --> editor["TipTap editor"]
editor --> manualEdit["manual revision"]
manualEdit --> archive["article archive"]
manualEdit --> chat["article chat"]
manualEdit --> repurpose["repurpose workflow"]
repurpose --> formats["email, social, blog formats"]
archive --> library["workspace library"]
chat --> feedback["contextual feedback"]
- AI output is treated as editable material rather than final copy
- Generation, archive, article chat, and repurpose features share the same article state
Go-to-market
I also worked on positioning and early GTM: narrowing the target, recruiting writers, writing outbound messages, and testing landing/ad channels.
- Narrowed the product from SNS management to newsletter writing
- Set the first target users as individual newsletter writers and founder-led content teams
- Tested cold email, cold DM, early-access forms, newsletter communities, and Google/Meta ads
- Used writer feedback to adjust clipping, writing-style settings, and draft generation
- Drafted Starter, Creator, Business, and Enterprise pricing tiers
Problem
Newsletter writing is split across reading queues, browser tabs, notes, AI prompts, and editors. The workflow often becomes a 4-6 hour writing cycle because writers repeatedly move between research, prompting, editing, and publishing tools.
My role
- Owned backend and AI agent development in a two-person team
- Built the flow from Web/PDF scrap to draft generation and Maily/Substack export
- Modeled long-running generation work with job state and callback events
- Tracked prompt versions, workflow input, and model responses with Langfuse
- Helped with product planning, early-access onboarding, and outbound copy
Product strategy
- Moved from a small-brand SNS assistant to a newsletter product after research showed a clearer writing problem
- Designed writing-style profiles so generated drafts could follow examples from the writer
- Focused the MVP on Chrome extension clipping, writing-style profiles, LangGraph generation, draft archive, and TipTap editing
- Compared newsletter platforms and AI writing tools including Maily, Stibee, Substack, Mailchimp, ChatGPT, Jasper, Writesonic, and HubSpot AI
Validation and GTM
- Collected early-access leads through LinkedIn and Instagram, including whether respondents already published newsletters and which platform they used
- Used cold email, cold DM, and service-newsletter outreach to recruit writers
- Used validation findings to narrow the target, free/paid boundary, pricing, and output quality requirements
- Built a three-week MVP and recorded tester feedback that Tyquill reduced newsletter writing time by roughly 20%
What I built
- NestJS API modules for scraps, articles, generation, archive, chat, and repurpose
- LangGraph.js workflow for generation, reflection, style rewrite, and title generation
- Scrap change tracking so regeneration can use updated context
- Langfuse observability for prompt tuning and quality debugging
- Deployment and cost cleanup from a separated agent service to a backend module, including a move from AWS Fargate to Railway that reduced monthly hosting from about $200 to about $5
Built
- Backend and AI agent work across Web/PDF scrap, draft generation, and export
- Job state model and callback flow for long-running generation tasks
- Langfuse tracing for prompt versions, workflow input, and model responses
- Early-access onboarding, writer interviews, and outbound copy