Vibe Prospecting
Professional prospecting inside the chat you already use. 50+ B2B data sources to build lead lists, research accounts, and find decision makers.
Cold email to coffee chat: personal emails from your Gmail, every reply in one place, and Google Meet in one click
MarketingTypeScript · PythonGitHub @UmerQureshi21OnlineOwner 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
Streamlines Cold Emais
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗Write one email template, paste in a list of people, and send each person a personalized copy from your Gmail. Sent emails, contacts and companies are stored in Postgres, so the app knows who you've already emailed.
| Part | Stack | Location |
|---|---|---|
| UI | Next.js 16 (App Router, Tailwind 4) | frontend/ |
| API | FastAPI + SQLAlchemy 2 | backend/ |
| Database | Postgres 14, port 5442 | ~/Desktop/personal-projects/psqlConnections/cold-emailer-5442 |
./dev.sh
This starts Postgres if it isn't running, then the API on :8000 and the UI on http://localhost:3000 (landing page; the app itself is at /compose).
To start Postgres yourself instead, follow the same steps as your other clusters:
cd ~/Desktop/personal-projects/psqlConnections/cold-emailer-5442
pg_ctl -D ./pgdata -o "-p 5442" -l logfile start
psql -p 5442 -d cold_emailer
cd backend && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cd ../frontend && npm install
Tables are created automatically when the API starts.
http://localhost:8000/api/gmail/callbackbackend/credentials.json.When connecting, tick every box on Google's screen:
Connected before the Inbox existed? The Inbox tab shows a Reconnect Gmail button. Click it once to grant the two new permissions.
While the Google app is in "Testing" mode, Google expires the login after 7 days. When that happens, click Connect Gmail again.
"Couldn't reach Google" (or a batch saying "Can't reach Gmail right now") means your internet dropped, not that the login expired, so there's no need to reconnect. A batch waits and carries on by itself once the connection is back, and never sends an email twice. For the Inbox or a Meet link, just try again.
backend/.env (create the file if it doesn't exist; git ignores it):
HUNTER_API_KEY=your-key
./dev.sh.The free plan has 50 credits a month. Per Hunter's credit rules:
Your remaining credits show in the top nav and update after every search.
Popsicle is invite-only: only the emails in ALLOWED_EMAILS can ever get in (for now, just yours). Accounts are created from the terminal, never from the website. From the backend/ folder:
.venv/bin/python -m app.manage new-password # a 64-character random password, saved to backend/.owner-password
.venv/bin/python -m app.manage create-user [email protected] --password-file .owner-password
.venv/bin/python -m app.manage create-user [email protected] # or type a password yourself (twice, not shown)
.venv/bin/python -m app.manage list-users # also flags accounts not in ALLOWED_EMAILS
.venv/bin/python -m app.manage set-password [email protected] [--password-file .owner-password]
.venv/bin/python -m app.manage delete-user [email protected]
new-password makes a long random one and saves it to backend/.owner-password. Git ignores that file, only your user can read it, and it's never printed or overwritten, so the password lives only on your machine (put a copy in your password manager too). Only its scrypt hash is stored in the database. Knowing your email isn't enough: logging in needs this password, and Sign up can't set a new one for an account that already has one.ALLOWED_EMAILS (comma-separated, e.g. [email protected]) is the last word, checked on the server:
create-user refuses an email that isn't on the list.ALLOWED_EMAILS and invited from the terminal without a password yet; everyone else is told Popsicle is invite-only.AUTH_REQUIRED=false, the default). The login page still works if you want to try it.{{company}}.full_name and email to start; email is required). Add a column with + variable and remove one with its ×.{{variable}} at the cursor. Valid placeholders show in red; ones that don't match a variable get a wavy underline.
full_name variable, {{first_name}} and {{last_name}} are filled in automatically.Your draft is saved in the browser, so a refresh doesn't lose it.
The bar at the top of the email editor shows which template you're working on. A red dot marks unsaved changes ("Edited" for a saved template, "Not saved" for a new draft).
A template stores the subject and body. Loading one adds any variables it uses as columns in the recipients table, without removing yours.
Open the Find people tab:
barista. Hunter matches whole words, so engineer also finds "Engineering"; bare words like developer are left out of the presets because they'd also match business-development roles. Listing more titles doesn't cost more credits.role column from their job titles.There's also a quicker Find people button inside Compose that searches one company and adds people to the current batch. Its Look them up section finds a single person's email from their name or LinkedIn URL.
Get new people. Already emailed the first 10 people at a company and want more? Click Get 10 new people on that company. Popsicle goes through Hunter's results from the top and skips everyone you've already emailed, until it has 10 people you haven't. Pages it has already fetched are saved, so going back over them is free; usually only the one page with new people uses a credit. Tick Hide people I've already seen to also skip anyone Find people has shown you before (whether or not you emailed them). When Hunter has nobody new left for that search, Popsicle says so and suggests a wider location or no job title.
Results are saved for 30 days, so repeating a search costs nothing. Use refresh on saved results to search Hunter again.
If a company search finds no one, Popsicle checks (for free) how many people Hunter has there in total and tells you whether the filters are the reason. It then offers one-click Search anywhere or Remove the job title retries.
The Companies tab is your list of companies to reach, with each one's logo (from Hunter, free).
The Inbox tab lists everyone you've emailed. Open someone to see the whole conversation with them, your emails and their replies, in order.
{{meet_link}} becomes the link. Popsicle creates a Google Calendar event with a Google Meet link, then emails it as a reply in your conversation. By default they also get a Google Calendar invite; untick that to send only the email. Meetings show at the top of the conversation with the link, a copy button and a link to the calendar event.Instead of choosing a time yourself, send someone a link to a page where they pick a 30-minute slot that suits them. The call is then set up exactly like Send Meet link.
{{booking_link}} in your email. Each person gets their own link, made when you click Send. The preview shows where it goes.Each link works once and expires after 60 days. Anyone opening an old or made-up link just sees "This booking link doesn't work anymore", and the page is rate-limited. Booking-link emails sent from the Inbox count toward the daily limit, like Meet emails.
The Look up tab finds a single person's email. Either paste their LinkedIn profile URL on its own (Hunter works out who they are and where they work), or enter their full name plus their company or website domain.
tiny-startup.io is the most reliable input.Recent lookups stay on the page so you can come back to them.
The sun/moon button at the top right of the app switches between the light and dark palettes. Your choice is remembered in this browser and applied before the page is drawn, so there's no flash. The landing page is always dark: its light trails read best on a dark background. To change that, edit FIXED_THEME in frontend/src/lib/themeScript.ts.
Colours are defined once in frontend/src/app/globals.css, each with one job (paper for surfaces, cloud for the background, ink for text, steel for secondary text, night for dark accents, plus the brand reds); the dark palette swaps their values.
Companies, Contacts, the Inbox and Sent load 30 at a time. The next 30 load by themselves as you scroll to the end of a list, or with Load more. Searches, filters and the counts on each tab cover everything, not only what's loaded.
Popsicle is built to run as one site: the frontend (Vercel) forwards /api/* to the backend (Railway), so the browser only ever talks to the frontend's address and login cookies work in every browser.
Popsicle counts as local only when its addresses and its database are all on your machine; anything else gets the deployed rules: login always on, the session cookie https-only, the API docs hidden. The backend refuses to start if AUTH_REQUIRED=false, if ALLOWED_EMAILS, TOKEN_ENCRYPTION_KEY or PROXY_SECRET is missing, or if BACKEND_URL or FRONTEND_URL is still a localhost address.
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # TOKEN_ENCRYPTION_KEY
python -c "import secrets; print(secrets.token_urlsafe(32))" # PROXY_SECRET
Keep both somewhere safe. Losing TOKEN_ENCRYPTION_KEY means reconnecting Gmail.
backend/backend/railway.json (the config file doesn't follow the root directory). It sets the start command, the /api/health check, and exactly one always-on server: sending runs in background threads of that one process, so don't add replicas.https://popsicle-production.up.railway.app.| Setting | Value |
|---|---|
DATABASE_URL | ${{Postgres.DATABASE_URL}} (Railway's postgresql:// address works as-is) |
FRONTEND_URL, BACKEND_URL | Both the Vercel address, e.g. https://popsicle.vercel.app. Google returns to BACKEND_URL/api/gmail/callback, which the frontend forwards to the backend |
ALLOWED_EMAILS | Your email: the only one that can log in (comma-separate more later) |
TOKEN_ENCRYPTION_KEY, PROXY_SECRET | From step 1 |
HUNTER_API_KEY | Your Hunter key |
GOOGLE_CLIENT_SECRETS_JSON | The whole contents of backend/credentials.json (Railway has no secret files). Locally the file is used instead |
frontend.BACKEND_ORIGIN (the Railway address from step 2) and PROXY_SECRET (the same value as the backend's). With BACKEND_ORIGIN set, the site calls its own /api; there's no need to set NEXT_PUBLIC_API_URL.In the Google Cloud OAuth client, add https://<your-vercel-address>/api/gmail/callback to Authorized redirect URIs (keep the localhost one). Leaving the app in Testing mode is fine; the login still expires every 7 days.
pg_dump -p 5442 -Fc cold_emailer > popsicle.dump
pg_restore --no-owner --no-acl -d "<Railway Postgres public URL>" popsicle.dump
Afterwards, cancel any batches still scheduled in your local database, or ./dev.sh would send them too.python -m app.manage new-password (in backend/) and copy it with pbcopy < .owner-password. Then in the backend service's shell on Railway (railway ssh), run python -m app.manage create-user [email protected] and paste it at both (hidden) prompts.https://<railway-address>/api/health answers; any other path on the Railway address says "Use Popsicle through its website". That's the proxy secret working.Logging in. Wrong passwords are limited per visitor (5 per 15 minutes) and per account (20). A browser you've logged in from before isn't held to the per-account limit, so someone guessing your password can't lock you out of your own browser.
Long jobs. Checking Gmail for replies runs in the background (the Inbox shows it until it's done), and verifying addresses, pasting companies and finding missing logos are sent a few at a time, so no request runs long enough to be cut off by the hosting proxy.
Sending safely across restarts. Each batch is claimed in the database by the server process sending it, and the claim is renewed while it works. During a redeploy, when the old and new servers overlap, a batch is only ever sent by one of them. An email that was being handed to Gmail when a server stopped is marked failed ("may have gone out, check Gmail's Sent folder") rather than resent. Waiting and scheduled batches whose server stopped are picked up again within a minute. The daily limit holds even when several batches send at once (it's checked and taken in one locked step), and Google Meet link emails count toward it. A Gmail connection in progress is kept in the database, so it finishes even if a redeploy happens in the middle.
Popsicle shows data it doesn't control: replies from strangers, Hunter's people data, pasted tables, Google's links. It's handled as follows:
http(s); a javascript: or data: link is refused when saved and dropped when shown.Origin) are refused.evals or unpickles data, and fills {{placeholders}} in one pass (a value containing {{email}} isn't filled in again). The only outside service the backend calls is Hunter, at a fixed address, so it can't be made to fetch arbitrary URLs.None of the tests reach Hunter or Gmail: Hunter calls and sends are faked, so no credits are spent and no email goes out. Each suite fails if coverage drops below 90%. GitHub Actions runs all of them on every push (.github/workflows/test.yml).
Backend (pytest). The tests use a cold_emailer_test database on the 5442 cluster, which they create at the start and drop at the end, so your real data is never touched. Postgres must be running.
cd backend
.venv/bin/pip install -r requirements-dev.txt # once
.venv/bin/pytest
To use another server, set TEST_DATABASE_URL. Its database name must end in _test.
Frontend (Vitest and React Testing Library). The backend is faked at the network level.
cd frontend
npm test # watch mode
npm run test:coverage # one run, with the coverage report
End-to-end (Playwright). Real browser runs of logging in, Find people, Look up and sending. The app is built into frontend/.next-e2e and talks to a fake API inside the browser, so it can run while ./dev.sh is up.
cd frontend
npx playwright install chromium # once
npm run test:e2e
Gmail flags accounts that suddenly send a lot, or whose emails bounce, and starts putting their mail in spam. Popsicle guards against both.
Daily send limit
Checking addresses exist
companies 1─* contacts 1─* emails *─1 campaigns *─* attachments
campaigns *─1 templates
gmail_accounts (the connected sender)
hunter_lookups (cached Hunter.io responses)
users, auth_sessions (invite-only accounts and their login sessions)
sending_settings (daily limit and minimum gap), email_verifications (Hunter verdicts, kept 30 days)
conversation_messages, mail_threads (Gmail conversations with people emailed), meetings (Google Meet calls set up)
pending/sent/failed/skipped/cancelled) and the Gmail message ID.status (not_started/emailed/replied/not_interested) for the target list.full_name, role/title and linkedin are picked up automatically).sent. They're skipped by default; you can change this with the person-check icon.The schema is created with create_all. Columns added later (like campaigns.scheduled_for and companies.status) are added at startup by app/migrations.py, which never touches existing data. Switch to Alembic if migrations get more involved.
API docs are at http://localhost:8000/docs.