Open-Source Cold Email & Warmup Platform
Warmbly는 메일함을 예열하고 캠페인 전송·답변 triage를 지원하는 오픈소스 콜드 이메일 플랫폼입니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A sales manager uploads a call recording; twenty seconds later they get the
transcript, the talk ratio, a SPIN score and the moments they let slip.
This repository is the frontend. Transcription and analysis run on a separate
Railway service, and the data lives in Supabase.
npm run build type-checks and compiles; npm run typecheck does the former
alone. Deploy target is Vercel — the four variables above are all it needs.
Two things live in the database rather than in this repo, and the app cannot
work without them. Both are in supabase/ and are safe to re-run.README 전문 보기 ↗A sales manager uploads a call recording; twenty seconds later they get the transcript, the talk ratio, a SPIN score and the moments they let slip.
This repository is the frontend. Transcription and analysis run on a separate Railway service, and the data lives in Supabase.
npm install
cp .env.local.example .env.local # then fill in the Supabase values
npm run dev
| Variable | What it is |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Anon key. Never the service key — row level security is what separates one company's calls from another's, and a service key in the bundle would defeat it |
NEXT_PUBLIC_API_URL | Railway processing API |
NEXT_PUBLIC_API_AUTH | 1 once that API verifies Supabase JWTs; 0 (default) until then |
npm run build type-checks and compiles; npm run typecheck does the former
alone. Deploy target is Vercel — the four variables above are all it needs.
Two things live in the database rather than in this repo, and the app cannot
work without them. Both are in supabase/ and are safe to re-run.
profile-on-signup.sql — a trigger that
creates a profiles row for each new auth.users record. Every RLS policy
keys off my_company_id(), which reads from profiles; with no profile row
that returns NULL, every policy fails, and a signed-in person sees an app with
nothing in it. New profiles get company_id = NULL and the lowest role, so
signing up grants nobody access to a company's calls — an owner attaches them
afterwards with the update at the bottom of that file.fix-seller-stats-rls.sql — sets
security_invoker on the seller_stats view. Without it the view runs as its
owner, RLS on the underlying tables is never evaluated, and every company's
figures are readable by anyone holding the publishable key — which ships in
the browser. Verify with an unauthenticated request: it must return [].app/[locale] so next-intl
owns the URL. Uzbek is the default; the choice is kept in a cookie.app/globals.css holds the whole design
system: semantic colour tokens on :root, the same names redefined under
.dark, and @theme inline mapping them to utilities. Nothing is hardcoded,
so both themes come from one set of declarations.components/SessionProvider.tsx resolves
the session, the profile row and the company, then hands them to the app
shell. No session goes to sign-in; a profile with no company_id gets its own
screen rather than an empty dashboard.lib/api.ts. Every failure is
mapped onto a small vocabulary (limit, not_found, too_large, server,
network) that the UI turns into a localised sentence — no server prose ever
reaches a user.The ribbon (components/Ribbon.tsx) is the product. One component at three
sizes: 6px in a list row, 10px averaged on a seller, 14px and interactive on the
call detail page, where clicking a slice seeks the audio. Its slice elements are
memoised separately from the playhead, because a long call is a few hundred
slices and the player moves the playhead four times a second.
Speaker roles come from one convention, encoded once in lib/speakers.ts:
the person who speaks first is the salesperson. The diarizer only ever hands
back SPEAKER_00, SPEAKER_01; resolve the map once per call and pass it down.
Never render a raw label.
Red and blue mean one thing each. Red is the salesperson, blue is the client, everywhere. The single exception is the SPIN block, where an axis scoring four or below turns red to mark it weak — and that exception does not get extended anywhere else.
Dates and numbers are not formatted with Intl for Uzbek. Chrome ships no
uz locale data and silently falls back to the ICU root format (2026 M09 4)
and to English number conventions (1,234.5), while Node has the Uzbek data —
so the same value rendered differently on the server and in the browser, and
what the browser showed was wrong for the market. Dates are composed from month
names in the message files (lib/useDateFormat.ts, pinned to Asia/Tashkent) and
numbers use ru-RU, whose grouping and decimal marks match Uzbek usage and
which every engine ships.
Live status has two paths. Supabase Realtime on the calls table is the
fast one; polling GET /calls/{id} every five seconds for unsettled calls is
the guarantee, and it stops as soon as everything has settled.
Flowbite supplies the modal, dropdown, drawer, table, badge, avatar and form
controls. components/flowbiteTheme.ts remaps every surface it paints onto the
design tokens, and clears Flowbite's own strings for those keys — merging alone
leaves its dark:bg-gray-700 sitting next to our bg-canvas, because
tailwind-merge sees no conflict between a variant and a bare utility.
Motion is rationed. The ribbon draws itself in once on the detail page,
left to right over about half a second; SPIN bars grow from zero; the total
score counts up. That is all of it, and prefers-reduced-motion turns each one
off.
The build is clean and the app runs on Vercel unchanged, but three things have to be set up outside the repository or it will look broken.
1. Environment variables. .env.local is gitignored, so a fresh deploy has
none. In Vercel → Settings → Environment Variables, add all four from
.env.local.example. Without them every screen renders
the "could not reach the data" notice. NEXT_PUBLIC_API_AUTH must stay 0:
the processing service replies Access-Control-Allow-Headers: content-type,
so a browser preflight carrying Authorization would be refused and every
upload would fail.
2. Supabase auth URLs. Authentication → URL Configuration: set the Site URL to the deployed origin and add it to Redirect URLs. Otherwise the confirmation link in every sign-up email still points at localhost, and nobody can finish registering.
3. The database. A new Supabase project has none of this. Run
supabase/schema.sql then
supabase/api-keys.sql.
Serverless functions cap the request body at 4.5 MB, and the platform rejects
the request before any of this code runs. That affects exactly one path:
POST /api/v1/calls, the endpoint customers call with an API key, because it
proxies the audio.
It proxies deliberately. The processing service trusts whatever company_id
it is handed, so letting a customer post to it directly would let them file
recordings into — and then read them back out of — any company whose id they
could guess. The proxy is what makes the API key mean anything.
The dashboard's own uploader is not affected: it posts from the browser straight to the processing service and keeps the full 200 MB. Raising the API ceiling means either verifying JWTs on the processing service, so scoped credentials can be handed out, or hosting that one route somewhere without the cap.