Open-Source Cold Email & Warmup Platform
Warmbly is the open-source platform for cold email: warm your mailboxes, send campaigns, and triage every reply. Run it on our cloud or self-host the whole thing.
One link. A one-minute conversation. Customers share what they think and get a reward; you get the why.
MarketingTypeScript · JavaScriptGitHub @frostyalce000OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
One shareable link. A one-minute AI interview. The customer gets a discount code; the merchant gets structured customer intelligence.
A merchant creates a link for a product, picks a few questions and a reward, and shares it. A follower opens it on their phone, talks (or types) with EDNA, the AI interviewer, for about a minute, enters their email and gets a discount code. Every interview is transcribed, analysed and rolled up into a dashboard.Read the full README ↗One shareable link. A one-minute AI interview. The customer gets a discount code; the merchant gets structured customer intelligence.
A merchant creates a link for a product, picks a few questions and a reward, and shares it. A follower opens it on their phone, talks (or types) with EDNA, the AI interviewer, for about a minute, enters their email and gets a discount code. Every interview is transcribed, analysed and rolled up into a dashboard.
| Live product | https://dap-link.vercel.app |
| Example DAP Link | https://dap-link.vercel.app/everyday-hoodie |
| Test merchant login | [email protected] / DapLink-Demo-2026 at /login |
| Your own account | /signup (no email confirmation needed) |
Merchant
/your-product). Pause or edit it at any time.Participant
A voice interview with three questions takes about a minute end to end.
flowchart LR
subgraph Browser
P[Public link page<br/>voice or text interview]
D[Merchant dashboard]
end
subgraph Vercel["Next.js on Vercel"]
API[Public API<br/>route handlers]
SA[Dashboard pages +<br/>server actions]
BG["after() background work<br/>analysis · summary · email"]
end
subgraph Supabase
DB[(Postgres)]
AUTH[Auth]
ST[Storage<br/>product photos]
end
OAI_RT[OpenAI Realtime<br/>speech ↔ speech]
OAI[OpenAI<br/>chat + analysis]
RS[Resend]
P -- "start · save turns · complete" --> API
P <-. "WebRTC audio + events<br/>(short-lived secret)" .-> OAI_RT
API -- "mint client secret" --> OAI_RT
API --> DB
API --> BG
BG --> OAI
BG --> RS
BG --> DB
D --> SA
SA --> DB
SA --> AUTH
SA --> ST
set_current_question, finish_interview) are fixed on the server when the secret is created.finish_interview, the client recognises the sign-off. Both fallbacks came out of end-to-end test runs where the model skipped a tool call.POST /complete issues the reward and returns the code straight away. Everything slow runs afterwards with Next.js after():
Each of those is claimed with a conditional UPDATE before it runs, so concurrent serverless invocations can't do the same work twice, and each is self-healing: anything that failed or was rate limited is picked up again by the next completion or the next time the merchant opens the dashboard. No queue or cron is needed at this scale, and the claim pattern moves onto a real queue unchanged.
| Concern | How it is handled |
|---|---|
| Duplicate submissions | Unique indexes on (campaign, participant) and on session in rewards. A double tap or a retry returns the same code. The same email coming back does not get a second code, and the screen does not show the first one again (it was emailed), so knowing someone's email never reveals their code. Repeat responses are flagged and left out of analytics. |
| Rate limiting | Fixed-window counters in Postgres (one atomic upsert), per network address (IPv6 grouped by /64), per session and per merchant. Works across serverless instances with no extra service. |
| Transcript size | At most 60 turns and 20,000 characters per interview, and only the latest 30 turns are sent to the model, so a scripted client cannot fill the database or inflate AI costs. Typed transcripts can only be written by the chat endpoint. |
| Public endpoints | Participants have no account. Each interview gets a random bearer token; only its hash is stored, compared in constant time. |
| AI/API failures | OpenAI calls retry with backoff. Voice failure falls back to text. Failed analysis is retried up to 3 times. A failed chat message can be retried without creating a duplicate. |
| Email rate limits | Temporary failures stay "pending" and are retried later; permanent ones are marked failed. The code is always on screen first, so email never blocks the reward. |
| Spend | Daily ceilings on voice calls per campaign, per merchant and across the app; past one, participants are offered the typed interview instead of an error. Plus a time limit on voice calls and caps on reply length. |
| Part | Choice | Why |
|---|---|---|
| App | Next.js 16 (App Router), React 19, TypeScript | One codebase for the public page, the dashboard and the API. |
| Styling | Tailwind CSS 4 | Fast to build a consistent mobile-first UI; no component library to fight. |
| Database | PostgreSQL on Supabase | Relational data with real constraints, which the duplicate handling relies on. |
| ORM | Drizzle, with SQL migrations in drizzle/ | Typed queries, plain-SQL migrations, light enough for serverless. |
| Merchant auth | Supabase Auth | Sessions, password hashing and cookie handling without writing auth by hand. |
| File storage | Supabase Storage | Product photos, resized in the browser before upload. |
| Hosting | Vercel | Native Next.js host; also supplies IP-derived country and city headers. Functions are pinned to the region next to the database (vercel.json). |
| Resend | Simple API, verified sending domain. |
Everything runs on OpenAI, each model chosen for its job. All are overridable by environment variable.
| Job | Model | Why |
|---|---|---|
| Voice interview | gpt-realtime-2.1 | Speech-to-speech in one model: natural voice, sub-second replies, handles interruptions, can call tools. No separate STT → LLM → TTS chain to add latency. |
| Turn-taking | Semantic VAD | Waits for the person to finish a thought instead of cutting in on the first pause. |
| Live transcription | gpt-4o-mini-transcribe | Runs alongside the call to produce the participant's side of the transcript. |
| Typed interview, question suggestions | gpt-5.4-mini | Fast; reply latency matters here. |
| Analysis and campaign summary | gpt-5.4 | Runs after the participant has left, so quality matters more than speed. |
Structured output. Analysis uses strict JSON-schema output, then is validated again with Zod before it is stored. Lengths and ranges are clipped rather than rejected, so one over-long quote can't fail a whole analysis.
Uncertainty is first-class. Every judgement carries a confidence, and "unclear" is a valid answer everywhere. A 60-second interview often says nothing about price; the dashboard shows "Not enough signal" instead of inventing a number, and labels every inferred value with its confidence and the evidence behind it. The prompts require quotes to be copied verbatim from the participant's own turns.
Counts are computed, not generated. For campaign themes the model returns which responses express each theme; the server maps those to session ids and counts them. Distributions, medians and rates are plain SQL.
Prompt injection. The interviewer's instructions live on the server. The analysis prompts treat transcripts as data, not instructions.
Twelve tables. Migrations are in drizzle/; the annotated schema is src/db/schema.ts.
erDiagram
merchants ||--o{ campaigns : owns
campaigns ||--o{ questions : asks
campaigns ||--o{ sessions : receives
participants ||--o{ sessions : takes
sessions ||--|| consents : "agreed to"
sessions ||--o{ transcript_turns : contains
sessions ||--o{ answers : gives
questions ||--o{ answers : "answered by"
sessions ||--o| rewards : earns
participants ||--o{ rewards : holds
sessions ||--o| session_insights : "analysed into"
campaigns ||--o| campaign_insights : "summarised into"
Design decisions worth knowing:
sessions, consents, transcript_turns, answers and rewards hold what happened. session_insights and campaign_insights hold what a model concluded, with the model name and a schema version. Inferences can be deleted and regenerated without touching what the participant said.participants is global, not per campaign. A person is one row, keyed by email, so future DAP products (Pay, Wallet, Network) can attach their own data to the same participant.jsonb.Permissions-Policy header blocks it.Requirements: Node 20+ and a Postgres database (a free Supabase project is the simplest).
npm install
cp .env.example .env.local # then fill it in
npm run db:migrate # creates the tables
npm run dev # http://localhost:3000
Then sign up at /signup, create a link, and open it.
Deploying to Vercel: import the repository, add the variables from .env.example (all except NEXT_PUBLIC_APP_URL, DATABASE_MIGRATION_URL and DATABASE_POOL_MAX), and deploy. Run npm run db:migrate once from your machine against the same database.
.env.local. Nothing needs to be configured in the Supabase dashboard; the storage bucket is created on first upload.npm run db:dev starts an embedded Postgres on port 5433 (set DATABASE_POOL_MAX=1), and npm run db:seed adds a published demo campaign at /studio-north-hoodie. The public interview works fully; merchant login and photo upload need Supabase.| Command | What it does |
|---|---|
npm run dev / build / start | Develop, build, serve |
npm run typecheck / lint / test | TypeScript, ESLint, unit tests |
npm run db:generate / db:migrate | Create a migration from schema changes, apply migrations |
node scripts/smoke-text-interview.mjs | Plays a participant through a whole typed interview |
node scripts/load-test.mjs | Concurrency test (see below) |
npm test, 23 tests): slugs, input validation, reward codes, the interviewer brief, voice progress matching, analysis schema leniency, network grouping for rate limits, log-safe database errors, formatting.scripts/smoke-text-interview.mjs runs a full interview against a running server, then checks that completing twice returns the same code and that a wrong token is rejected.scripts/load-test.mjs simulates participants going through a campaign at once.Result of node scripts/load-test.mjs http://localhost:3000 studio-north-hoodie 100 40, against a production build and a local Postgres 16 on one laptop:
| Participants completed | 100 of 100, 40 at a time, in 3.3 s |
| Errors | 0 |
| Discount codes | 100 issued, 100 unique |
| Same email from 12 sessions at once | 1 code issued |
| Start / save / complete, p95 | 735 ms / 247 ms / 464 ms |
| AI analysis afterwards | 101 of 101 succeeded on the first attempt |
This measures the application and database under concurrent load. It is not a test of the deployed stack; network distance to Supabase and cold starts will add to the timings there.
after(), not a queue. It is retried and self-healing, but its retry trigger is the next completion or dashboard visit, not a timer.RewardSource in src/lib/rewards.ts is the seam for a Shopify implementation.src/
app/
[slug]/ public DAP Link page
api/public/sessions/ start · turns · chat · complete
(auth)/ login, signup, server actions
dashboard/ campaigns, analytics, responses
components/
participant/ interview UI, voice hook, typed chat
dashboard/ builder form, charts, tables
db/schema.ts all tables, annotated
lib/
interview/ EDNA's brief, voice secret, transcript service
analysis/ schemas, per-session analysis, campaign summary
rewards.ts code issuing (the Shopify seam)
reward-email.ts at-most-once, retried email delivery
rate-limit.ts Postgres fixed-window limiter
public-session.ts participant bearer tokens
drizzle/ SQL migrations
scripts/ dev database, seed, smoke and load tests
tests/ unit tests