Lab
한국어 English

Tyquill은 자료 수집, 초안 생성, 장문 편집을 한 곳에서 처리하는 뉴스레터 작성 workspace입니다.

초기에는 SNS 콘텐츠 보조 제품으로 시작했지만, 리서치 후 뉴스레터 작성 문제에 집중하도록 범위를 좁혔습니다.

2인 팀에서 backend와 AI Agent 개발을 맡았고, 제품 기획, 배포, 사용자 온보딩도 함께 담당했습니다.

Type
풀스택 SaaS
Surface
Editor, API, extension, NestJS agent
Focus
수집부터 초안 생성까지
Scope
Backend, AI Agent, 제품 기획, 온보딩
스택
  • NestJS
  • React
  • TipTap
  • LangGraph
  • Vertex AI
  • PostgreSQL
  • WXT

Architecture

Tyquill은 clipping용 Chrome extension, React editor workspace, NestJS backend로 구성됩니다. backend는 product API와 LangGraph 생성 흐름을 함께 담당합니다.

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 extension과 web app은 NestJS server를 호출합니다. product API와 LangGraph generation은 같은 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가 main backend와 generation runtime을 맡습니다
  • Python agent repo는 별도 repo로 존재하지만 현재 product path에서는 generation에 사용하지 않습니다
Capture layer extension은 page나 선택 영역을 capture하고 readable content를 추출한 뒤 Markdown으로 변환해 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"]
                        
  • extension은 WXT entrypoints, content script, shadow-root UI, service modules를 사용합니다
  • captured content는 generation 전에 scrap material로 저장됩니다
Application and backend layer web app은 routes, API clients, state stores, TipTap editor를 담당합니다. NestJS server는 auth, persistence, queue, article modules, 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"]
                        
  • client repo에는 routes, services, stores, i18n, hooks, TipTap templates가 있습니다
  • server module map에는 auth, users, scraps, articles, article archive, article chat, content, folders, queue, repurpose, uploaded files, writing styles가 포함됩니다
Generation layer newsletter generation은 NestJS server 안의 NewsletterAgentService와 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와 PDF content는 aggregation 전에 병렬로 준비됩니다
  • 흐름은 draft를 reflection한 뒤 writing style example이 있으면 rewrite하고 title을 생성합니다
  • server는 LangGraph stream event를 frontend progress event로 매핑합니다
Editing and repurpose layer generated draft는 editor로 돌아갑니다. 사용자는 이를 수정하고 archive, article chat, repurpose로 이어갈 수 있습니다.

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은 최종 copy가 아니라 편집 가능한 material로 다룹니다
  • generation, archive, article chat, repurpose 기능은 같은 article state를 공유합니다

Go-to-market

비개발 작업도 함께 했습니다. target을 좁히고, writer를 모집하고, outbound message와 landing/ad channel을 테스트했습니다.

  • SNS 관리 제품에서 뉴스레터 작성 제품으로 범위를 좁힘
  • 1차 target을 개인 newsletter writer와 founder-led content team으로 설정
  • cold email, cold DM, early-access form, newsletter community, Google/Meta Ads를 테스트
  • writer feedback을 바탕으로 clipping, writing-style setting, draft generation을 조정
  • Starter, Creator, Business, Enterprise pricing tier를 작성

문제

뉴스레터 작성은 읽기 큐, 브라우저 탭, 메모, AI prompt, editor로 흩어지기 쉽습니다. 사용자가 research, prompting, editing, publishing tool 사이를 반복해서 오가면서 4-6시간짜리 writing cycle로 길어지는 문제가 있었습니다.

담당 범위

  • 2인 팀에서 backend와 AI Agent 개발을 담당
  • Web/PDF scrap부터 AI 초안 생성, Maily/Substack export까지 이어지는 흐름 구축
  • 긴 생성 작업을 Job 상태 모델과 callback event로 분리
  • Langfuse로 prompt version, workflow input, model response를 추적
  • 제품 기획, early-access 온보딩, outbound copy 작성에 참여

제품 전략

  • 시장 조사 후 small-brand SNS assistant에서 newsletter writer product로 범위를 좁힘
  • writer의 기존 글 예시를 반영할 수 있도록 writing-style profile을 설계
  • MVP 범위를 Chrome extension clipping, writing-style profile, LangGraph generation, draft archive, TipTap editing으로 좁힘
  • Maily, Stibee, Substack, Mailchimp와 ChatGPT, Jasper, Writesonic, HubSpot AI를 비교해 차별점을 정리

검증과 GTM

  • LinkedIn과 Instagram을 통해 early-access lead를 수집하고, 응답자가 실제 newsletter를 발행하는지와 사용 platform을 함께 확인
  • cold email, cold DM, service-newsletter outreach로 writer를 모집
  • validation 결과를 target, free/paid boundary, pricing, output quality requirement로 정리
  • 3주 MVP를 만들고 체험단 feedback에서 newsletter writing time이 약 20% 줄었다는 평가를 기록

내가 만든 것

  • scraps, articles, generation, archive, chat, repurpose를 위한 NestJS API module
  • generation, reflection, style rewrite, title generation을 담당하는 LangGraph.js workflow
  • regeneration 시 변경된 scrap context를 반영하기 위한 scrap change tracking
  • prompt tuning과 품질 디버깅을 위한 Langfuse observability
  • 초기 분리 agent service를 backend module 구조로 정리하고 AWS Fargate에서 Railway로 옮겨 월 hosting 비용을 약 $200에서 약 $5로 절감

구현

  • Web/PDF scrap, 초안 생성, export로 이어지는 backend와 AI agent 개발
  • 긴 생성 작업을 위한 Job 상태 모델과 callback 흐름
  • prompt version, workflow input, model response를 추적하는 Langfuse observability
  • early-access 온보딩, writer 인터뷰, outbound copy