Capture you own.
Self-hosted screen recording + AI meeting notes. Open-source alternative to Loom + Granola, in one product.
Active dev. Invite-based multi-user. The live instance at loom.dissonance.cloud is what I use daily as my own Loom + Granola replacement.
Two products in one self-hosted codebase, gated by a single env flag (ENABLE_GRANOLA):
- Screen recording (Loom-shape). Screen + camera + mic + system audio, captured in the browser via the companion Chrome extension. Branded share pages with Slack/Discord link previews, comments, view tracking, AI-generated titles, summaries, chapters, action items, and hover-scrub thumbnails.
- AI meeting notes (Granola-shape). Real-time audio transcription from a native macOS desktop app. Live notepad while the meeting runs. Manual AI notes generation when the user is ready. Attendee tracking, folder organization, speaker attribution.
Both surfaces use the same Postgres / R2 / Deepgram / Claude pipeline. Same media_objects table with a type column, so adding a third product (something like MacWhisper, say) reuses everything.
| Loom | Granola | Cap | Loomola | |
|---|---|---|---|---|
| Screen recording | ✓ | — | ✓ | ✓ |
| AI meeting notes | — | ✓ | — | ✓ |
| Self-host | — | — | ✓ | ✓ |
| Pricing (you) | $12.50–$20 / user / month | $20 / user / month (Pro) | Free + paid managed | Free; you pay your own infra (~$15–25/mo total across Supabase + R2 + Deepgram + Anthropic on light usage) |
| Transcription | proprietary | proprietary (real-time, no audio kept) | local Whisper | Deepgram (best-in-class hosted, ~$0.0043/min) |
| AI summaries | basic | yes | basic | Claude Sonnet 4.6 with structured Zod output |
| Custom branding on share pages | enterprise tier | — | — | ✓ (logo + accent + Google Font + CTA + footer per brand profile) |
| Status | Atlassian-owned; raised prices ~100× in Feb 2026 | Active, free tier locks notes >30d | 18k★ active OSS | Active OSS, invite-based multi-user |
I got excited the day I found Cap. Shared it around, dug in, spent a few hours trying to get the transcription path working on real meeting audio. It wasn't behaving the way I expected. I submitted my findings to the repo, a couple of other people chimed in saying they were seeing the same thing, and no fix arrived. Solo OSS with finite time, totally fair. Real respect for what Richie has built, no shade at all.
But I needed something that worked, and I also wanted Granola-shape meeting notes living alongside the screen recordings. That combination didn't exist anywhere I could find on the OSS side, so I started building Loomola. It's been my daily driver for a couple of months now.
If you pay for both Loom AND Granola today, this is the package that replaces both at once.
Web. Next.js 15 (App Router), React 19, TypeScript, Tailwind 4. Supabase for Postgres, Auth, and Realtime. Drizzle ORM. pg-boss for the job queue, so no Redis. Cloudflare R2 for object storage with zero egress on the free tier. Deepgram async transcription with signed callbacks. Anthropic Claude via the Vercel AI SDK with Zod-validated structured output. Plyr 3.x for the player. Mailgun for the comment and first-view notification emails.
Desktop (macOS). Native SwiftUI app for the audio meeting notes plus a premium composite video recorder. AVAudioEngine, ScreenCaptureKit, AVAssetWriter, CIContext. Bundles Inter and JetBrains Mono. About 10k lines of Swift across capture, UI, and orphan recovery.
Chrome extension (extension/). Manifest V3. Injects a frameless /bubble iframe into every tab the user is on, so the camera bubble survives tab switches during a screen recording. Single source of truth for the bubble pixels.
Deploy. Single node:22-alpine container with ffmpeg baked in for thumbnails and preview sprites. Doppler manages env at boot. Migrations run automatically. I use Coolify on a $7/mo Hostinger VPS, but any Docker host works.
This is the path for a friend cloning the repo and running their own Loomola.
Do not copy Ian's .env.local; create fresh accounts and keys.
Not a developer? You don't have to follow the steps below by hand. Open this repository in an AI coding assistant (Claude Code, Cursor, Codex, etc.) and say:
Help me set up my own private copy of Loomola from scratch. Follow the README's "Self-host Quickstart" using the Docker Compose path with a free Supabase project. Walk me through it one step at a time. This is my own fresh instance — don't push anything to the repo, and don't use anyone else's infrastructure (no Doppler, no Coolify, no existing domain).
The assistant will read this README and walk you through creating accounts, filling in config, launching, and testing. Everything below is the same path written out for humans — read on if you'd rather do it yourself.
Two choices up front:
- Loom-only or Loom + Granola. Start with
ENABLE_GRANOLA=falseif you only want screen recordings. Set it totruewhen you also want the macOS audio-meeting-notes product. - Local or deployed.
http://localhost:3000is enough to open the app and test auth/UI — and, withTRANSCRIBE_PROVIDER=openai-whisper, for the full record-to-transcript pipeline. Deepgram transcription callbacks, public share links, and social unfurls still need a public HTTPS app URL. Use a deploy, ngrok, or Cloudflare Tunnel if you want those.
Database & auth — two options:
- Free Supabase cloud (default, recommended). A free Supabase project gives you Postgres + auth in a couple of clicks, and it's what the quickstart below uses. Loomola's media (video/audio) is served from your own object storage, never through Supabase, so the only data crossing Supabase is small database query results — a single user stays comfortably inside the free tier. (Free-tier notes: ~5 GB/mo egress, and free projects pause after ~7 days with no activity — daily use keeps it awake. Limits change; check Supabase's current free tier.)
- Self-hosted Supabase = $0 forever (advanced). Want nothing external and no usage caps at all? Run Supabase on your own VPS with Coolify — same software, no app changes, just your VPS. See
docs/self-hosting-supabase.md. Best for people already comfortable with Coolify/VPS admin.
Needs Docker Desktop (or Docker Engine + Compose) installed and running. Bundled MinIO for storage; you bring a free Supabase project (Postgres + auth) and an Anthropic key for AI summaries. For transcription you choose OpenAI Whisper (works on localhost immediately) or Deepgram (needs a public URL) — see the note after the commands.
Getting your Supabase values: create a free project at supabase.com, then from the dashboard copy the Project URL, anon key, and service-role key (Settings → API) and the database connection string (Settings → Database). Those four fill the Supabase + DATABASE_URL lines in .env.compose. You don't need to create a user — Loomola's first-run screen does that. (Prefer no external account at all? See the self-hosted-Supabase option above.)
git clone https://github.com/Deducer/loomola.git
cd loomola
cp .env.compose.example .env.compose
# Fill in: Supabase URL/keys/DATABASE_URL, Anthropic, a transcription
# provider (see below), and a random MINIO_ROOT_PASSWORD (openssl rand -hex 32).
docker compose --env-file .env.compose up -d --buildTranscription on localhost — important: the default TRANSCRIBE_PROVIDER=deepgram needs a public HTTPS URL (Deepgram calls back to your app), so on http://localhost:3000 recordings will hang in "transcribing." For a local first run, set TRANSCRIBE_PROVIDER=openai-whisper in .env.compose and put your key in OPENAI_API_KEY — Whisper runs in-process with no callback. Switch to Deepgram once you're on a public URL and want speaker labels / unlimited length.
Open http://localhost:3000. The first visit walks you through creating your admin account. Migrations run automatically at boot, and the container fails fast with a readable list if a required variable is missing. To verify every external service is wired correctly:
npm install && npm run doctor # live checks against your confignpm run doctor needs Node 22 installed locally (the Docker path otherwise doesn't). It's optional — the container already validates required env at boot and fails fast with a readable list, and docker compose logs app shows the same. Use doctor when you want a one-line-per-service health check.
The manual path below (Quickstart B) gives you npm run dev for development.
ghcr.io/deducer/loomola is published on every push to main and on
release tags. Caveat that matters: Next.js inlines NEXT_PUBLIC_*
(your Supabase URL and anon key) into the JavaScript bundle at build
time, and the public image is built with placeholders — so sign-in in
the browser cannot work with it as-is. There is no runtime override.
- Self-hosting? Use
docker compose up— it builds the image with your values from.env.compose. This is the supported path. - Want your own prebuilt image? Fork the repo and run the
"Publish Docker image" workflow with your
NEXT_PUBLIC_*values as inputs; it publishes to your fork's GHCR namespace. - The public image is still useful as a build-cache source and for poking at the container layout.
On macOS:
xcode-select --install
brew install node@22 ffmpeg gitUse Chrome or another Chromium browser for screen recording. Safari/Firefox are not supported for the full recorder because the browser capture APIs are Chrome-only.
All of these have free tiers that are enough for one person:
- Supabase for Postgres, Auth, and Realtime: supabase.com
- Cloudflare R2 for object storage: cloudflare.com/r2
- Deepgram for transcription: deepgram.com
- Anthropic for title/summary/chapter generation: console.anthropic.com
- OpenAI for embeddings/search when Granola is enabled: platform.openai.com
- Mailgun for notification/contact emails: mailgun.com
Supabase setup:
- Create a project.
- Copy the Project URL, anon key, service-role key, and database connection string.
- In Supabase Auth, make sure Email/password auth is enabled.
- No manual user creation needed: the first time you open the app it walks you through creating your admin account in-browser.
- In URL Configuration, set Site URL to your app origin. Add redirect URLs for
http://localhost:3000/auth/callbackand, if deployed,https://your-domain.com/auth/callback.
Cloudflare R2 setup:
- Create a bucket, for example
loomola. - Create R2 S3 API credentials with read/write access to that bucket.
- Add a CORS policy to the bucket. Include both local and production origins if you use both:
[
{
"AllowedOrigins": ["http://localhost:3000", "https://your-domain.com"],
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]ExposeHeaders: ["ETag"] matters. Browser multipart uploads read each R2 part's ETag; without it, uploads fail even though the PUT request may look successful.
git clone https://github.com/Deducer/loomola.git
cd loomola
npm install
cp .env.example .env.localFill in .env.local. Generate the random secrets with:
openssl rand -hex 32Use that for VIEW_UNLOCK_SECRET, VISITOR_HASH_SALT, DEEPGRAM_CALLBACK_SIGNING_SECRET, INTEGRATION_API_TOKEN, and MCP_TOKEN if you enable MCP.
Deleted recordings go to an in-app trash and are permanently purged (storage + database) after TRASH_RETENTION_DAYS days — default 30. See docs/self-hosting.md → Deletion and the trash.
For local UI-only testing, NEXT_PUBLIC_APP_URL=http://localhost:3000 is fine. For real transcription, set it to a public HTTPS URL:
# Example with ngrok
ngrok http 3000
# Then set NEXT_PUBLIC_APP_URL=https://the-ngrok-url.ngrok-free.appRestart npm run dev after changing NEXT_PUBLIC_APP_URL; Deepgram receives the callback URL when the transcribe job is created. Alternatively, set TRANSCRIBE_PROVIDER=openai-whisper to transcribe synchronously without any public callback URL — localhost works as-is.
deepgram (default) |
openai-whisper |
|
|---|---|---|
| How it runs | Async — Deepgram calls your instance back | Synchronous inside the job — no callback |
Works on http://localhost / LAN, no tunnel |
No — needs a public HTTPS app URL | Yes |
| Speaker labels (diarization) | Yes | No — whole transcript is one speaker |
| Recording length | No practical limit | ~1 hour (OpenAI 25MB audio cap); longer recordings fail with a clear reason and can be retried after switching providers |
Live in-meeting transcript drawer (ENABLE_GRANOLA) |
Yes | Still requires a Deepgram key |
| Keys needed | DEEPGRAM_API_KEY + DEEPGRAM_CALLBACK_SIGNING_SECRET |
OPENAI_API_KEY |
Set TRANSCRIBE_PROVIDER=openai-whisper if you are self-hosting on a LAN or
just don't want to run a tunnel for local dev transcription. Everything
downstream (titles, summaries, chapters, action items, search) is identical.
A local whisper.cpp provider behind the same interface is a planned follow-up.
npm run db:migrate
npm run devOpen http://localhost:3000, sign in with the Supabase user you created, and record a short test.
Expected flow:
/recordcreates an upload row.- Browser uploads media parts directly to R2.
- Server completes the multipart upload.
- Deepgram transcribes the recording and calls back to your app.
- pg-boss workers generate the title, summary, chapters, action items, thumbnails, preview sprite, and downloads.
If the recording stays in transcribing, the most common cause is NEXT_PUBLIC_APP_URL pointing at localhost or another URL Deepgram cannot reach.
The desktop app is optional for Loom-style browser recording, but it is the best way to use the Granola-style audio meeting notes and the native recorder.
Make sure .env.local contains:
LOOM_API_BASE_URL=http://localhost:3000
LOOM_SUPABASE_URL=<your Supabase project URL>
LOOM_SUPABASE_ANON_KEY=<your Supabase anon key>
For a deployed instance, use LOOM_API_BASE_URL=https://your-domain.com.
Then:
cd desktop
./scripts/install-local-app.shThat builds, locally signs, installs, and launches /Applications/Loomola.app.
Grant Camera, Microphone, Screen Recording, and Accessibility permissions when macOS asks. If you change Screen Recording permission, quit and relaunch the app.
Desktop details live in desktop/README.md.
The web recorder works without the extension, but the extension gives you the frameless Loom-style camera bubble.
Load the extension/ folder as an unpacked Chrome extension at chrome://extensions (Developer mode on → Load unpacked → select extension/). No source editing needed — point it at your instance from the built-in options page (right-click the extension icon → Options) and enter your app origin. More detail is in extension/README.md.
Loomola supports multiple accounts on one instance. As the admin, open
/settings/users on your instance to send invite links (7-day expiry,
single use). Each user sees only their own recordings, folders, and notes.
If email isn't configured, copy the invite link from the UI and send it
yourself. Password resets are self-serve from the sign-in page.
The container supports Doppler as an optional secrets manager: when the host sets a single DOPPLER_TOKEN env var, Doppler injects everything else before migrations and server.js run. Without it, env vars pass through directly.
Recommended production path with Doppler:
- Create a Doppler project/config for Loomola.
- Copy the same values from
.env.localinto Doppler. - In your host/Coolify app, set only
DOPPLER_TOKENas the runtime secret. - Set Docker build args for
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY, andNEXT_PUBLIC_APP_URLbecause Next.js inlines public env vars at build time. - Point DNS at the host, enable HTTPS, and set
NEXT_PUBLIC_APP_URL=https://your-domain.com. - Add the production origin to R2 CORS and Supabase Auth URL settings.
The container no longer requires Doppler: with DOPPLER_TOKEN set it injects
secrets at boot (the maintainer's setup); without it, env vars pass through
directly (docker compose / docker run --env-file).
For ongoing ops — health monitoring, backups, upgrades, and a full
troubleshooting table — see docs/self-hosting.md.
| Symptom | Likely cause | Fix |
|---|---|---|
| Anything at all | Misconfigured service | Run npm run doctor — it live-checks DB, storage, Supabase, Deepgram, and LLM keys with one line per service |
DATABASE_URL is not set |
.env.local missing or command run from the wrong folder |
Run commands from repo root and fill .env.local |
Upload fails with missing ETag |
R2 CORS does not expose ETag |
Add ExposeHeaders: ["ETag"] to bucket CORS |
Recording stuck in transcribing |
Deepgram cannot reach NEXT_PUBLIC_APP_URL |
Use deployed HTTPS, ngrok, or Cloudflare Tunnel — or set TRANSCRIBE_PROVIDER=openai-whisper (no callback needed) |
| Whisper recording fails with "over OpenAI's 25MB limit" | Recording longer than ~1 hour | Switch to TRANSCRIBE_PROVIDER=deepgram and press Retry on the recording |
| Login fails | Supabase user does not exist or is not confirmed | Open your instance — first visit routes to /setup to create the admin account |
| Desktop app talks to Ian's prod | LOOM_API_BASE_URL left at default |
Set LOOM_API_BASE_URL or LOOM_DESKTOP_API_BASE_URL before installing |
| Extension pill says not detected | Extension not pointed at your instance | Right-click extension icon → Options → enter your app origin and save |
GitHub Releases are created automatically on every v* tag push. Each release includes:
- macOS
.dmg— signed and notarized when the repo secrets are configured; unsigned.zipotherwise. Download it from the Release page, or build locally viadesktop/scripts/install-local-app.sh. - GHCR Docker image —
ghcr.io/deducer/loomola:<version>. See the GHCR caveat in the Prebuilt image section above for theNEXT_PUBLIC_*constraint.
For the tag and CHANGELOG convention, see docs/releasing.md.
- Stage 1 (M1–M11): full Loom-shape product. Recording flow, R2 multipart uploads, Deepgram pipeline, AI title/summary/chapters/action-items, share pages with social previews, comments, view tracking, trim and downloads, polish.
- Stage 2 (G-M1–G-M17): full Granola-shape product. Audio capture, real-time transcripts, AI summaries, attendees, folder organization, multi-folder Phase 1, speaker recognition v1.
- Stage 3: security hardening. CSP, HSTS, signed Deepgram callbacks with single-use nonces, rate limits, time-bounded unlock cookies.
- Stages 4–7: macOS desktop app. Premium composite recorder, Granola-grade visual shell, live notes side panel, pause/resume, orphan recovery.
- Stage 8: Granola-grade desktop note workspace.
- Stage 9: reliability. Orphan recovery, Coolify brownout detection, boot-warmed pg-boss.
- Stage 10 (open-source readiness): one-command self-host (
docker compose), bundled MinIO,npm run doctor, first-run admin setup, password reset, invite-based multi-user, failure UX with Retry, real/api/health, pluggable transcription (Deepgram + Whisper), configurable Chrome extension with options page, notarized desktop release workflow, GHCR image publishing, v1.0.0. - Migration tools: Granola to Loomola CLI (
migrate/) using the official Granola Business API. Imports notes, transcripts, summaries, attendees, folders, speaker attribution. Loom import is the next major build.
See ROADMAP.md for the full status table.
For user-facing release notes, see CHANGELOG.md.
- Team workspaces / sharing between users. Invite-based multi-user exists (each user manages their own recordings, folders, and notes in isolation). Shared folders, cross-user permissions, and team-level workspaces are not built yet.
- iOS / Android / Windows desktop. macOS only for native capture. The web
/recordflow works on any Chrome. - Loom's advanced editing surface. Basic trim (start / end) is shipped. Filler-word removal, edit-by-transcript, AI silence-removal, speed ramps, cursor-zoom, drawing — none built yet.
- Voice-biometric speaker recognition. ("Here's this teammate's voice across all my recordings.") Spec'd, deferred. See
docs/superpowers/specs/2026-05-04-speaker-recognition-design.md. - Loom migration tool. The Granola migrator (
migrate/) is shipped; Loom import is a separate piece of work. - Custom share-page domains per brand profile (something like
videos.acme.commapping to a Loomola brand). Pairs with Brand Layer 2.
- Invite-based multi-user. The live instance at loom.dissonance.cloud is invite-only. Self-hosting gives you full control: the first-run flow creates your admin account, and you invite others from
/settings/users. - Actively maintained. I use Loomola every day, so improvements ship often. I'm not making any uptime guarantees, but the project isn't going dormant. If something breaks for you, file an issue.
- Not affiliated with Loom, Atlassian, Granola, or Cap. Just a builder who doesn't want to pay $40 per seat per month.
- License is AGPL-3.0, same as Cap. You can self-host freely. Anyone running a modified version as a hosted service has to publish their changes. The paid walkthrough video is a separate product, not part of the AGPL-licensed code.
Ian Cross, solo builder. Find me on X: @theiancross. Live instance at loom.dissonance.cloud.
Issues and PRs welcome. I'm using Loomola every day and contributing to it almost as often, so things keep moving. Happy to entertain help, suggestions, or "this is broken in my setup" reports.