CodeNotes is a multi-tenant, AI-powered study application that transforms YouTube programming tutorials into structured technical study notes. It is built on a resilient, decoupled architecture designed to withstand external API failures, network partitions, and process crashes without losing work or leaking user data.
The application cleanly separates fast, synchronous user-facing HTTP interactions from durable, asynchronous background processing:
flowchart TD
subgraph Client["Client Tier (Browser)"]
UI["Next.js App Router (React 19)"]
Polling["TanStack Query (Status Poller)"]
end
subgraph API["Application Tier (Next.js 16)"]
HTTP_Gen["POST /api/generate"]
HTTP_Status["GET /api/generate/status"]
HTTP_Notes["GET /api/notes & /api/notes/:id"]
HTTP_Metrics["GET /api/metrics"]
HTTP_Health["GET /api/health"]
AuthMid["Proxy Middleware (Supabase Auth)"]
end
subgraph Execution["Durable Execution Tier (Inngest)"]
InngestHandler["/api/inngest Endpoint"]
FnGen["Durable Function: generate-notes"]
FnRecon["Cron Function: reconcile-stale-generations"]
end
subgraph External["External Services"]
Supadata["Supadata API (Primary Transcript)"]
YT["YouTube Scraper (Fallback Transcript)"]
Gemini["Google Gemini 2.5 Flash"]
end
subgraph Data["Persistence Tier (PostgreSQL)"]
T_Notes["notes (RLS Enforced)"]
T_Idemp["generation_idempotency"]
T_Trans["youtube_transcripts (Shared Cache)"]
T_Rate["rate_limit_buckets"]
end
subgraph Monitoring["Observability Stack"]
Prom["Prometheus (Scraper)"]
Graf["Grafana Dashboard"]
SentryService["Sentry (Sanitized Errors)"]
end
UI -->|1. Submit YouTube URL| HTTP_Gen
UI -->|2. Poll job status| HTTP_Status
UI -->|Read library| HTTP_Notes
HTTP_Gen -->|Authenticate & Check Limit| AuthMid
HTTP_Gen -->|Insert 'pending' record| T_Idemp
HTTP_Gen -->|Dispatch event| InngestHandler
InngestHandler --> FnGen
InngestHandler --> FnRecon
FnGen -->|Claim 15m lease| T_Idemp
FnGen -->|1. Check cache / fetch| T_Trans
FnGen -->|2. Primary fetch| Supadata
FnGen -->|3. Fallback fetch| YT
FnGen -->|4. Generate notes| Gemini
FnGen -->|5. Atomic RLS transaction| T_Notes
FnGen -->|5. Mark 'completed'| T_Idemp
FnGen -.->|Unrecoverable error| SentryService
FnRecon -->|Scan stale jobs & re-dispatch| T_Idemp
FnRecon -->|Re-dispatch event| InngestHandler
Prom -->|Scrape metrics| HTTP_Metrics
Graf -->|Query PromQL| Prom
- Authentication: Resolves user session via Supabase Auth (
src/lib/auth.ts). - Rate Limiting: Checks PostgreSQL token bucket (
src/lib/rate-limit.ts) to enforce per-user request limits. - Payload Validation: Canonicalizes YouTube URL and extracts video ID (
src/lib/transcript.ts). - Idempotency Check: Determines the idempotency key:
- If the
Idempotency-Keyrequest header is present, its trimmed string value is used. - If the header is omitted, the key is deterministically derived as
sha256("${userId}:${videoId}"). - In the web UI (
src/components/UrlInput.tsx), each submission generates a fresh UUID in theIdempotency-Keyheader. - Looks up
generation_idempotencyby(user_id, key):- If already
completed: Returns200 OKwith cachednoteIdimmediately (no AI work re-executed). Replay of a UI-created job requires reusing that exactIdempotency-KeyUUID. A request without the header uses a different, deterministic key unless the original request also omitted the header. - If
processingorpending: Replays current status with202 Accepted. - If payload mismatch (reusing key with different video ID): Rejects with
422 Unprocessable Entity. - Submitting the same video with a separate UUID creates a distinct generation record: it benefits from transcript caching via
youtube_transcripts(avoiding external caption calls), but runs Gemini inference again to create a new note rather than deduplicating notes.
- If already
- If the
- Durable Reservation: Atomically inserts or updates row with status
pending. - Event Dispatch: Dispatches
notes/generate.requestedwith deterministic event IDgen-${userId}-${idempotencyKey}to Inngest. - Immediate Response: Returns
202 Acceptedwithin ~50ms. The browser client switches to non-blocking polling.
Background execution runs inside Inngest (src/inngest/functions/generate-notes.ts), divided into durable steps:
sequenceDiagram
autonumber
actor User as Client Browser
participant API as Next.js API (/api/generate)
participant DB as PostgreSQL Database
participant Inngest as Inngest Engine
participant Trans as Transcript Providers
participant AI as Gemini 2.5 Flash
User->>API: POST /api/generate (URL, Idempotency-Key)
API->>DB: Check rate limit & existing idempotency row
API->>DB: INSERT generation_idempotency (status='pending')
API->>Inngest: inngest.send('notes/generate.requested')
API-->>User: 202 Accepted (status='pending')
loop Polling (every 1.5s)
User->>API: GET /api/generate/status?key=...
API->>DB: SELECT status, note_id FROM generation_idempotency
API-->>User: JSON { status: 'processing' }
end
Inngest->>DB: Step 1: Claim 15-min lease (status='processing')
Inngest->>Trans: Step 2: Acquire transcript (Cache -> Supadata -> Scraper)
Trans-->>Inngest: Transcript text + Video title
Inngest->>AI: Step 3: Inference with structured JSON schema
AI-->>Inngest: Structured study notes (summary, concepts, code blocks)
Inngest->>DB: Step 4: withUserTransaction(userId) -> INSERT notes, UPDATE idempotency='completed'
User->>API: GET /api/generate/status?key=...
API-->>User: JSON { status: 'completed', noteId: '...' }
User->>API: GET /api/notes/:noteId
API-->>User: Rendered study note
| Step | Scope | Description | Error Handling |
|---|---|---|---|
1. claim-generation-processing |
DB Query | Verifies job isn't already finished or actively claimed by another worker; claims a 15-minute lease (lease_until = NOW() + INTERVAL '15 minutes'). |
Skips execution if active lease held; fails fast on payload mismatch. |
2. fetch-transcript-and-title |
External I/O (Outside DB Tx) | Checks youtube_transcripts table for shared cached transcript. If miss, calls Supadata API with timeout (15s). If Supadata fails, falls back to youtube-transcript. Extracts title via YouTube oEmbed (4s timeout). |
Raises non-retriable TRANSCRIPT_UNAVAILABLE if video has no captions; retries transient network timeouts. |
3. generate-ai-notes |
External I/O (Outside DB Tx) | Sends transcript and title to Google Gemini 2.5 Flash with structured output schema enforcement. Reconstructs syntax-highlighted code blocks, key concepts, and takeaways. | Retries transient 429 / 503 errors (up to 2 retries). Throws NonRetriableError on malformed schema. |
4. persist-notes |
DB Transaction (withUserTransaction) |
Checks out a database client, sets transaction-local user claim, inserts note into notes, updates generation_idempotency to completed, and marks lease released. Takes < 20ms. |
Database connection retries handled by Inngest durable step wrapper. |
onFailure Hook |
Terminal Failure Handler | Invoked when all retries are exhausted or an unrecoverable NonRetriableError occurs. Updates generation_idempotency to failed with classified error code, logs structured failure, and reports sanitized error to Sentry. |
Self-contained, best-effort execution with Sentry flush timeout (1.5s). |
All personal data (saved notes and search queries) is strictly protected by PostgreSQL Row Level Security policies at the database layer.
Application queries touching user-scoped data do not execute as an unrestricted superuser. Instead, they execute through src/lib/db.ts:
BEGIN;
SET LOCAL "request.jwt.claim.sub" = '<userId>';
SET LOCAL ROLE authenticated;
-- User query runs here:
INSERT INTO notes (user_id, title, ...) VALUES (...);
COMMIT;ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE notes FORCE ROW LEVEL SECURITY;
CREATE POLICY "Users can only read own notes" ON notes
FOR SELECT TO authenticated
USING (user_id = auth.uid());
CREATE POLICY "Users can only insert own notes" ON notes
FOR INSERT TO authenticated
WITH CHECK (user_id = auth.uid());
CREATE POLICY "Users can only update own notes" ON notes
FOR UPDATE TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
CREATE POLICY "Users can only delete own notes" ON notes
FOR DELETE TO authenticated
USING (user_id = auth.uid());Because FORCE ROW LEVEL SECURITY is enabled, table owners and application database users cannot bypass RLS policies even if application filtering code omits WHERE user_id = ....
Network hiccups between the web server and Inngest or external APIs can occur in real-world environments. CodeNotes guards against data inconsistency using a three-tier safety net:
- Deterministic Inngest Event IDs:
Events are dispatched with
id: gen-${userId}-${idempotencyKey}. If the application server retriesinngest.send(), Inngest deduplicates the event at ingestion. - 15-Minute Processing Leases:
The
generation_idempotencyrecord holds alease_untiltimestamp. If a worker terminates abruptly midway through generation, the lease expires after 15 minutes, allowing a subsequent attempt or reconciliation to reclaim the job safely. - Automated Cron Reconciliation (
reconcile-stale-generations): An Inngest cron job runs every 5 minutes (src/inngest/functions/reconcile-stale.ts):- Case A: Finds jobs stuck in
pendingfor > 60 seconds (e.g., initialinngest.senddropped or worker failed to pick up) and re-dispatches the Inngest event with metrics tracking. - Case B: Finds jobs stuck in
processingwith expired leases (lease_until < NOW()) and re-dispatches them with deterministic event IDs.
- Case A: Finds jobs stuck in
Transcripts are public YouTube metadata. When User A generates notes for a video, the raw transcript is persisted to the public youtube_transcripts table (src/lib/transcript.ts). When User B subsequently submits the same video:
- The system reuses the cached transcript, saving upstream API quota and reducing latency by ~3–5 seconds.
- User B's generated notes are still independently created and isolated in
notesunder User B's RLS policy.