NoteStack-Test / docs /DESIGN.md
Arslan1997's picture
added elevenlabs grant
00a4363
|
Raw History Blame
9.26 kB

Notestack: Design Doc

Status: draft v0.1 (2026-09-22) Owner: Firebird Technologies

Note: Section 0 of the product brief was not included in the handoff. This doc reconstructs the product framing from Sections 5 to 12 plus the kickoff decisions. Replace Section 1 with the original Section 0 text when available.

1. Product

Notestack turns a writer's archive (Substack, Ghost, Medium, any RSS feed or URL) into a grounded research notebook and a launch engine. Writers can:

  • Ask questions of their own archive and get answers with citations to their posts.
  • Generate a two-host audio overview of a post or notebook (ElevenLabs).
  • Generate videos (explainer 16:9, vertical short 9:16, audiogram 1:1) rendered with Remotion.
  • Generate a Launch Kit per post: threads, LinkedIn, Substack Notes, Bluesky, hooks, SEO pack, quote cards, carousels, all in the writer's own voice.
  • Schedule everything on the Launchpad calendar and resurface evergreen posts.

Every factual statement Notestack generates must trace back to the writer's own sources.

2. Kickoff decisions

Area Decision
Backend FastAPI + SQLAlchemy 2 + Alembic (same stack as blog2video)
Worker arq on Redis (async jobs, progress events over Redis pub/sub, SSE to clients)
Database Postgres 16 + pgvector
Frontend React + Vite + TypeScript, plain CSS with design tokens
Renderer Node service running Remotion (renderer/)
Storage Cloudflare R2 (S3 API) for raw HTML, audio, video, stills, compiled DSPy programs
Email Resend, behind a provider interface (mirrors blog2video EmailService)
Auth Google sign-in + email 6-digit code signup + password login, JWT access/refresh with token_version revocation (mirrors blog2video)
LLM Provider agnostic through DSPy + LiteLLM. Default Z.ai GLM-5.3 (fast tier GLM-5.3-Flash) via its OpenAI compatible endpoint. Embeddings use a separate provider (OpenAI text-embedding-3-small by default). Any LiteLLM model string works by env var.
Billing Three tiers defined in code (app/services/plans.py). BILLING_ENABLED=false for now: everyone gets the top tier limits free, checkout returns 409, pricing page shows "Free during early access".
Blog Typed blogPosts.ts content array, /blogs and /blogs/:slug, same shape as blog2video

3. Architecture

            +-------------------+        SSE / REST        +--------------------+
 browser -->|  frontend (Vite)  |------------------------->|  api (FastAPI)     |
            +-------------------+                          |  auth, notebooks,  |
                     ^                                     |  billing, storage  |
                     | presigned GET                       +---------+----------+
                     |                                               | enqueue (arq)
            +--------+----------+                          +---------v----------+
            | Cloudflare R2     |<-------------------------|  worker (arq)      |
            | notestack-assets  |   put objects            |  ingest, chunk,    |
            +--------+----------+                          |  embed, DSPy, TTS  |
                     ^                                     +---------+----------+
                     | upload final render                           | POST /render
            +--------+----------+   progress callbacks     +---------v----------+
            | renderer (Remotion)|------------------------>|  api /internal/... |
            +-------------------+                          +--------------------+
     Postgres + pgvector (api, worker)        Redis (queue, pub/sub, cache)

3.1 Storage pipeline (R2)

All binary and large text payloads live in R2. Postgres stores only keys and metadata.

Key layout (app/services/storage.py::keys):

ws/{workspace_id}/sources/{source_id}/documents/{document_id}/raw.html
ws/{workspace_id}/uploads/{upload_id}/{filename}
ws/{workspace_id}/artifacts/{artifact_id}/{variant}.{ext}      audio, video, stills
ws/{workspace_id}/tts-cache/{sha256(script|voice|settings)}.mp3
system/dspy/{module}/{version}.json                              compiled programs

Flows:

  1. Server writes (ingestion, TTS, compiled programs): worker calls storage.put_bytes().
  2. Browser uploads (logos, voice consent samples): POST /api/storage/uploads returns a presigned PUT URL and an upload_id; the browser PUTs directly to R2; POST /api/storage/uploads/{id}/complete verifies with a HEAD request and records size and content type.
  3. Renderer writes: renderer receives a presigned PUT URL in the render request, so it never holds R2 credentials.
  4. Reads: GET /api/storage/objects/{key} checks workspace ownership and 302 redirects to a presigned GET (default 1 hour). Public stills (quote cards) can later move to a public bucket with a custom domain.

The storage service is an S3 client with R2's endpoint, so MinIO in docker-compose stands in for R2 locally with zero code changes.

3.2 LLM layer

app/llm/provider.py builds a dspy.LM from env:

LLM_MODEL=openai/glm-5.3               # any LiteLLM model string
LLM_FAST_MODEL=openai/glm-5.3-flash    # judges, cleanup, cheap passes
LLM_API_BASE=https://api.z.ai/api/paas/v4
LLM_API_KEY=...
LLM_REASONING_EFFORT=low               # low | high | max
EMBEDDING_MODEL=openai/text-embedding-3-small
EMBEDDING_DIM=1024

Z.ai considerations baked in:

  • OpenAI compatible chat endpoint, so the openai/ LiteLLM prefix plus api_base works.
  • GLM-5.x always reasons (thinking cannot be disabled). Every call sends thinking: enabled with reasoning_effort=low by default; modules that benefit (the groundedness judge) ask for main_lm("high"). Reasoning tokens count against max_tokens, so the default is 16K to leave room for the structured answer. Z.ai recommends temperature 1.0 for GLM-5.x.
  • 1M token context on GLM-5.3 means whole-notebook prompts (summaries, podcast scripts) rarely need map-reduce; retrieval is still used for chat to keep citations tight and cost low.
  • Z.ai's international API does not offer an embedding model, so embeddings are configured separately. The chosen model must accept a dimensions parameter so the pgvector column stays fixed. Changing EMBEDDING_DIM requires a migration and re-embed.
  • Token usage from every call is written to usage_events (metering from day one).

Swapping to Anthropic, OpenAI, Gemini or a local model is only an env change.

3.3 Jobs and progress

Every long operation is a row in jobs (kind, status, progress, error, cost) plus an arq task. Workers publish progress to Redis channel job:{id}; GET /api/jobs/{id}/events streams it as SSE. No API request blocks for more than a few seconds.

4. Data model

Implemented in backend/app/models and alembic/versions/0001_initial.py.

  • users, workspaces, workspace_members, subscriptions
  • email_verification_codes, update_emails, update_email_sends
  • sources, documents, chunks (pgvector embedding), notebooks, notebook_documents
  • voice_profiles, voice_consents
  • chats, messages, citations
  • artifacts, jobs (covers render_jobs and tts_jobs via kind), uploads
  • calendar_items, tracked_links, engagement_events
  • dspy_traces, usage_events

5. DSPy program design

See brief Section 6. Signatures live in backend/app/llm/signatures.py with Pydantic output models. Phase 1 ships CleanAndSegment, GroundedAnswer and BuildVoiceProfile. The rest are stubs with typed signatures so the schema contract is fixed early.

6. Plans

Free Writer Studio
Price (when billing turns on) $0 $19/mo $49/mo
Sources 1 3 10
Indexed posts 50 500 5,000
Audio overview minutes / mo 10 60 240
Video render minutes / mo 3 30 120
Launch Kits / mo 5 50 unlimited
Voice profile yes yes yes
Voice cloning (with consent) no yes yes
Brand kit, no watermark no yes yes

While BILLING_ENABLED=false, every workspace resolves to Studio limits.

7. Design system

Strict palette: #000000, #217cff, #FFFFFF and opacity variants only. Tokens in frontend/src/styles/tokens.css. Landing page is a night sky: canvas starfield with slow parallax, small twinkling sparkles, a soft blue nebula haze and a neon horizon line. prefers-reduced-motion renders a static sky. Logo slot (<Logo />) is a white tile that holds the black silhouette mark; drop the final file at frontend/public/logo.svg.

Copy rule: no em dashes anywhere in UI, email or generated output. The generation layer strips them as a last pass (app/llm/postprocess.py).

8. Security and privacy

  • JWT access (72h) + refresh, revocation by bumping users.token_version.
  • One auth provider per account for life (Google or email), same as blog2video.
  • Verification codes hashed, 10 minute TTL, 5 attempts, 60s resend cooldown.
  • R2 keys are always workspace scoped; the API checks ownership before presigning.
  • No training on user content without opt-in (workspaces.training_opt_in, default false).
  • Voice cloning requires a stored consent record with the recorded sample key.