Turning Hours of Gamer Rants into Searchable Gold with n8n and Supabase
Introduction
Besides writing about software architecture and tinkering with homelabs, I also run a podcast: GamerTV Podcast.
It's a Spanish-language podcast about videogames, told from the perspective of two 40-something grumpy gamers — yours truly (Darth Seldon) and my co-host Mr. Chilangiux. We rant, reminisce, and occasionally, against all odds, say something that resembles a coherent point about the gaming world.
But here's the problem: podcasts are fantastic to listen to, and genuinely impossible to search. When you want to find that one spicy rant about Final Fantasy VII remakes, or that deep dive into retro emulation you know we recorded... good luck scrubbing through hours of two grown men yelling about chocobos to find it.
That's why we're building this workflow: to automate transcription and storage of our episodes using n8n, so later we can layer on semantic search and finally make our own content discoverable — to us, mostly, since we're the ones who keep forgetting what we said.
This post builds on our earlier article, Build Your Own Local AI Automation Hub with n8n + Ollama, where we set up local Docker containers for n8n and Ollama. Now we're extending that stack with Whisper ASR transcription and Supabase vector storage — because apparently one homelab automation post is never enough, it's a lifestyle now.
Why n8n?
If you've ever played with Zapier or IFTTT, you know automation tools can glue services together. n8n takes that idea and gives it steroids and a self-hosting license:
- Open source.
- Self-hosted with Docker, so nobody can quietly sunset the free tier on you.
- Extensible with dozens of nodes to connect APIs, databases, and services, most of which I discover exist mid-project, out of desperation.
For this project, we're using n8n nodes for:
- GitHub Node — fetches new audio files and commits transcripts.
- Airtable Node — logs progress, saves settings.
- HTTP Request Node — calls Whisper ASR and Ollama APIs.
- Function Node (JavaScript) — custom logic (checking file extensions, chunking transcripts).
- Supabase Node — stores transcripts and embeddings.
- File Node — handles
.mp3,.txt,.srtdata streams.
The real magic? You design the workflow visually, and n8n executes it step by step, without you personally babysitting a cron job at 2am like it's a newborn.
Setting up Whisper ASR with Docker Compose
We'll use a lightweight Whisper ASR web service container. Add this to your docker-compose.yml alongside your existing n8n and Ollama containers:
services:
whisper-asr:
image: rhasspy/whisper-asr-webservice:latest
container_name: whisper-asr
ports:
- "9000:9000"
environment:
- ASR_MODEL=base
restart: unless-stoppedThis exposes an HTTP API at http://host.docker.internal:9000/asr. n8n's HTTP Request node can send .mp3 files and get back transcripts in .txt or .srt — the machine equivalent of politely asking someone to “just write down what we said,” except it actually does it, and doesn't complain about our audio quality.
Example call from n8n:
POST http://host.docker.internal:9000/asr?task=transcribe&language=es&output=txtBut you can also use the OpenAPI URL: http://localhost:9000/docs — genuinely useful if you'd rather click buttons than memorize query parameters like a masochist.
Using Ollama for Embeddings
From our previous post, we already have Ollama running in Docker. This time, instead of using it for chat, we'll call the embedding API with the Llama3.1:8b model — same tool, completely different job, like discovering your hammer is also, somehow, a very good letter opener.
POST http://host.docker.internal:11434/api/embeddings
Content-Type: application/json
{
"model": "llama3.1:8b",
"input": "This is a transcript chunk."
}Or you can use n8n's Supabase vector node directly with Ollama embeddings baked in. Note that you can swap in any embedding model (OpenAI comes to mind), but you'll need to adjust your chunk sizes accordingly — more on that another time, filed under “problems I will absolutely have later.”
These embeddings get stored in Supabase via the pgvector extension.
Airtable Setup
Airtable is our tracker and settings store:
- Settings Table — model names, thresholds, API keys.
- Episodes Table — episode ID, filename, language, processed flags.
- Progress Table — workflow runs, transcript/vectorization status.
n8n updates Airtable at every stage, so we always know which episodes are ready without either of us having to remember anything, which, given our track record, is the whole point.
Setting up Airtable itself is simple enough that I won't bore you with the click-by-click — but if enough of you actually want it, I'll come back and write it up properly.
Supabase Setup
Supabase is our storage backend. Create a new project and run this in the SQL editor:
-- Enable the pgvector extension to work with embedding vectors
create extension vector;
-- Create a table to store your documents
create table documents (
id bigserial primary key,
content text, -- corresponds to Document.pageContent
metadata jsonb, -- corresponds to Document.metadata
embedding vector(1536) -- 1536 works for OpenAI embeddings, change if needed
);
-- Create a function to search for documents
create function match_documents (
query_embedding vector(1536),
match_count int default null,
filter jsonb DEFAULT '{}'
) returns table (
id bigint,
content text,
metadata jsonb,
similarity float
)
language plpgsql
as $$
#variable_conflict use_column
begin
return query
select
id,
content,
metadata,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where metadata @> filter
order by documents.embedding <=> query_embedding
limit match_count;
end;
$$;This table stores transcripts as documents content, metadata (episodeId, timestamps, etc.), and embeddings — plus a function to actually search over all of it, which is the entire point of this exercise and not just an elaborate way to back up podcast audio.
Relational, Vector, and Node Databases
Since we're mixing approaches, here's a quick cheat sheet:
- Relational (Postgres, MySQL) → structured metadata and transcripts.
- Vector (pgvector, Pinecone) → semantic embeddings for similarity.
- Node/Graph (Neo4j) → relationships, like who appeared in which episodes.
Supabase with pgvector lets us combine relational and vector in one database, which is the database equivalent of a Swiss Army knife that doesn't also stab you when you close it wrong.
Rather than dump the full workflow export here (it's a genuinely enormous JSON file once you include every node, position, and credential placeholder — nobody wants to scroll through that, including me, and I wrote it), here's the actual logic behind the Function Node that decides which episodes still need work, the piece doing the real thinking in this whole pipeline:
const allFiles = [];
const mediaFiles = [];
const companionFiles = new Map(); // Map to track TXT/SRT companions
const mediaCompanions = new Map(); // Map to track MP3/MP4 companions
// Collect and categorize all files
for (const item of $input.all()) {
const tree = item.json.tree;
tree.forEach(file => {
if (file.type === 'blob') {
const filePath = file.path;
const fileName = filePath.split('/').pop();
const directory = filePath.substring(0, filePath.lastIndexOf('/'));
const extension = fileName.toLowerCase().split('.').pop();
const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
const fileObj = {
path: filePath,
directory: directory,
fileName: fileName,
nameWithoutExt: nameWithoutExt,
extension: extension,
sha: file.sha,
url: file.url,
size: file.size
};
// Collect MP3
if (extension === 'mp3') {
mediaFiles.push(fileObj);
// Track media companions (MP3/MP4 pairs)
const baseKey = `${directory}/${nameWithoutExt}`;
if (!mediaCompanions.has(baseKey)) {
mediaCompanions.set(baseKey, []);
}
mediaCompanions.get(baseKey).push(extension);
}
// Track subtitle companions (TXT/SRT)
if (extension === 'txt' || extension === 'srt') {
const baseKey = `${directory}/${nameWithoutExt}`;
if (!companionFiles.has(baseKey)) {
companionFiles.set(baseKey, []);
}
companionFiles.get(baseKey).push(extension);
}
}
});
}
// Find media files with missing companions
const mediaWithMissingCompanions = mediaFiles.filter(media => {
const baseKey = `${media.directory}/${media.nameWithoutExt}`;
const subtitleCompanions = companionFiles.get(baseKey) || [];
const mediaCompanionsList = mediaCompanions.get(baseKey) || [];
// Check if missing subtitle files OR missing media companion
const missingSubtitles = !subtitleCompanions.includes('txt') && !subtitleCompanions.includes('srt');
const missingMediaCompanion = (media.extension === 'mp3' && !mediaCompanionsList.includes('mp4')) ||
(media.extension === 'mp4' && !mediaCompanionsList.includes('mp3'));
// Return true if missing any companion
return missingSubtitles || missingMediaCompanion;
}).map(media => {
const baseKey = `${media.directory}/${media.nameWithoutExt}`;
const subtitleCompanions = companionFiles.get(baseKey) || [];
const mediaCompanionsList = mediaCompanions.get(baseKey) || [];
return {
json: {
...media,
mediaType: media.extension.toUpperCase(),
missingCompanions: {
// Subtitle companions
hasTxt: subtitleCompanions.includes('txt'),
hasSrt: subtitleCompanions.includes('srt'),
missingSubtitles: !subtitleCompanions.includes('txt') && !subtitleCompanions.includes('srt'),
// Media companions
hasMp3: mediaCompanionsList.includes('mp3'),
hasMp4: mediaCompanionsList.includes('mp4'),
missingMp3: media.extension === 'mp4' && !mediaCompanionsList.includes('mp3'),
missingMp4: media.extension === 'mp3' && !mediaCompanionsList.includes('mp4'),
// Summary
existingSubtitleCompanions: subtitleCompanions,
existingMediaCompanions: mediaCompanionsList,
missingTypes: [
...(!subtitleCompanions.includes('txt') && !subtitleCompanions.includes('srt') ? ['subtitles'] : []),
...(media.extension === 'mp3' && !mediaCompanionsList.includes('mp4') ? ['mp4'] : []),
...(media.extension === 'mp4' && !mediaCompanionsList.includes('mp3') ? ['mp3'] : [])
]
}
}
};
});
return mediaWithMissingCompanions;It walks the full GitHub file tree, groups files by their base name (so episode12.mp3, episode12.txt, and episode12.srt all get recognized as the same episode), and flags anything missing a transcript or subtitle companion. That's the entire “is this episode done yet” check, handled by one function, instead of either of us keeping a mental tally we would definitely get wrong.
Workflow Walkthrough
- GitHub List Repo Files — scan repository for
.mp3files. - Function Node — check if transcripts (
.txt,.srt) are missing. - Airtable Lookup/Create — register the episode.
- Whisper ASR (HTTP Request Nodes) — transcribe audio into TXT and SRT.
- GitHub Commit — push transcripts back into the repo.
- Ollama Embeddings — create embeddings with Llama3.1:8b.
- Supabase Insert — persist transcript, metadata, embeddings.
- Airtable Update — mark episode as
TranscribedandVectorized.
Architecture
We'll look at this from two perspectives, because one diagram is never enough to make an architecture feel official:
- C4 Architecture Diagram — systems and their relationships.
- n8n Flow Diagram — execution path inside n8n.
C4 Architecture Diagram
@startuml
title Podcast Ingestion Pipeline with n8n
!include <C4/C4_Container>
AddContainerTag("worker", $legendText="Worker/Automation")
AddContainerTag("store", $legendText="Storage")
AddContainerTag("tracker",$legendText="Tracker/Config")
AddContainerTag("ml", $legendText="ML/Embeddings")
Person(admin, "Maintainer", "Ops/dev who runs the pipeline")
System_Boundary(n8n, "n8n Workflow") {
Container(githubNode, "GitHub Node", "Fetch/commit audio & transcripts", $tags="worker")
Container(airtableNode, "Airtable Node", "Track progress", $tags="tracker")
Container(whisperNode, "HTTP Whisper ASR Nodes", "Send audio → Get TXT/SRT", $tags="ml")
Container(ollamaNode, "Ollama Node", "Generate embeddings (Llama3.1:8b)", $tags="ml")
Container(supabaseNode, "Supabase Node", "Insert transcripts + embeddings", $tags="store")
}
@enduml
n8n Workflow Flow Diagram

You'll need to swap in your own username, repo, Airtable table names, and Supabase/GitHub credentials — just import the workflow and update those settings. You can also cherry-pick individual nodes if you only need part of this, no shame in taking the parts that work and quietly ignoring the rest.
Conclusion
With n8n as the automation hub, plus Airtable, Supabase, Whisper ASR, and Ollama, we now have a fully automated ingestion pipeline for our podcast:
- n8n orchestrates GitHub, Airtable, Whisper, and Supabase nodes.
- Airtable tracks state and progress.
- Whisper ASR provides high-quality transcripts.
- Ollama generates embeddings.
- Supabase stores text and vectors.
This workflow makes it easy to transcribe and enrich every episode automatically, freeing us from the extremely glamorous task of manually re-listening to ourselves to remember what we said.
Next up: a dedicated post on how to actually search this vector database with n8n — turning this ingestion pipeline into a full-blown semantic podcast search engine, so we can finally answer “wait, which episode did we talk about that in” without descending into an argument about it on air.
Happy Coding!!!