Spaces:
Runtime error
Download docs/DESIGN.md from firebird-technologies/NoteStack-Test: direct link, hf CLI and curl.
- Browser
- Download file 9.26 kB
-
https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/00a4363f7a11b73a246bf02519a065de62195b64/docs/DESIGN.md
- Command line
-
hf download hf://spaces/firebird-technologies/NoteStack-Test@00a4363f7a11b73a246bf02519a065de62195b64/docs/DESIGN.md
-
curl -L -o DESIGN.md https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/00a4363f7a11b73a246bf02519a065de62195b64/docs/DESIGN.md
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 |
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:
- Server writes (ingestion, TTS, compiled programs): worker calls
storage.put_bytes(). - Browser uploads (logos, voice consent samples):
POST /api/storage/uploadsreturns a presigned PUT URL and anupload_id; the browser PUTs directly to R2;POST /api/storage/uploads/{id}/completeverifies with a HEAD request and records size and content type. - Renderer writes: renderer receives a presigned PUT URL in the render request, so it never holds R2 credentials.
- 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 plusapi_baseworks. - GLM-5.x always reasons (thinking cannot be disabled). Every call sends
thinking: enabledwithreasoning_effort=lowby default; modules that benefit (the groundedness judge) ask formain_lm("high"). Reasoning tokens count againstmax_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
dimensionsparameter so the pgvector column stays fixed. ChangingEMBEDDING_DIMrequires 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.