Back to Portfolio
SaaS CI Passing Polyglot
AI · Automation · Full-Stack SaaS
TaskScribe

AI-powered note transcription → structured task extraction → automated client delivery. A production-scale, polyglot SaaS application that eliminates the manual overhead between a contractor's meeting notes and their client's inbox.

Next.js 15TypeScriptPython FastAPIPostgreSQLPrisma ORMRedis + arqOpenAI WhisperClaude APIResendDocker ComposeGitHub ActionsS3 / MinIOHMAC-SHA256ZodPydantic
45
Files Scaffolded
2
Decoupled Services
8
Architecture ADRs
CI Passing
7
DB Entities
JSON
Structured LLM Output
🌐

apps/web

Next.js 15 App Router. System of record: owns auth (Auth.js), database (Prisma + Postgres), public API, client email delivery. The only service that writes the DB.

TypeScript
🧠

services/ai

Python FastAPI + arq worker. Stateless: transcribes audio via Whisper, extracts structured tasks via LLM, POSTs results back via HMAC-signed callback. No DB access.

Python 3.11
🏗️

packages/shared

Cross-service contract: Zod schemas (TypeScript) mirrored as Pydantic models (Python). The seam between services is typed in both languages — no drifting interfaces.

Shared Contract
Async Pipeline Flow
1
Upload

Client uploads audio. Web app stores blob to S3/MinIO, creates Note row (PENDING), enqueues job to Redis. Returns 202 Accepted immediately — zero blocking.

2
Transcription

arq worker dequeues. Pulls audio from object storage. Runs through Transcriber interface (OpenAI Whisper or faster-whisper — pluggable by config, no rewrite).

3
Structured Extraction

LLM call with JSON schema constraint — model must return { title, summary, items[] }. Output validated by Pydantic before accepted. Retries on malformed output.

4
HMAC Callback

AI service POSTs result to /api/internal/callback. Body signed with HMAC-SHA256, verified in constant time. Untrusted callers get 401, result discarded.

5
Persist + Deliver

Web app writes tasks to Postgres, marks Note COMPLETE. Sends formatted email via Resend. Generates share link for client portal access.

6
Job Audit Trail

Every pipeline run writes a Job row: type, status, attempts, error, timestamps. Failures are a DB state — never a silent void.

Database Schema (7 Entities)
User // Clerk shadow user, FK integrity id · email · name · createdAt Note // core entity id · userId · title · status audioKey · transcript · rawLlm status: PENDING|PROCESSING|COMPLETE|FAILED Task // extracted action items id · noteId · content · priority dueHint · completedAt Delivery // email audit id · noteId · recipientEmail provider · externalId · status ShareLink // signed client access id · noteId · token · expiresAt viewedAt Job // pipeline audit trail id · noteId · type · status attempts · error · startedAt · finishedAt ApiKey // service-to-service auth id · userId · keyHash · lastUsedAt
Architecture Decision Records
ADR-001

Async pipeline — never inline

Transcription takes 10–40s. Holding an HTTP request open that long would break serverless timeouts and make the UI feel broken. Upload returns 202 immediately; a Redis-backed worker does the slow work.

Trade-off: operational cost of a queue + eventually-consistent status model
ADR-002

Polyglot (TS web + Python AI)

TypeScript for the product (types from DB to UI). Python for the AI pipeline (Whisper, model SDKs, audio libs). Each language where it's strongest; separate scaling and failure domains.

Trade-off: two runtimes, one internal HTTP hop
ADR-003

Single DB owner (web only)

Only apps/web touches Postgres. The AI service returns results over a callback; the web app persists them. One migration owner, unambiguous schema authority, simpler auditing.

Trade-off: one extra HTTP callback vs a direct DB write
ADR-004

HMAC-signed internal callbacks

The AI worker signs the request body with HMAC-SHA256. The web app verifies in constant time before trusting the payload. Internal services are never trusted blindly — not even on a private network.

Trade-off: shared-secret auth vs mTLS (right level of security for V1)
ADR-006

Schema-constrained LLM extraction

Freeform prompts return unparseable prose. Instead, extraction uses JSON-schema constrained output so the model must return a typed structure. Validated by Pydantic before accepted.

Trade-off: occasional retry on malformed output vs deterministic storage
ADR-007

Pluggable providers behind interfaces

Transcription and extraction providers change on price/quality. The Transcriber and Extractor interfaces let you swap Whisper for Deepgram or change LLM vendors via config — no rewrite required.

Trade-off: small indirection layer up front; pays back on first vendor change