Skip to content
← All systems

Instagram prospecting · search, triage, draft

Shipped

Instagram Outreach

Takes it to the last safe step, then stops.

Finds business accounts by searching for them in a real browser, records what each account says about itself, probes the link in its bio, triages the results on a board, and drafts a personalised DM for the ones worth messaging.

19.6K
TypeScript LOC
3 / 2
Worker jobs / crons
5
Pipeline stages
0
Sends performed

The hard part

A sweep of one city is several hundred browser searches spread across a night. The laptop closes at 2am. What survives?


All of it. The plan lives in a database table, and a one-minute cron asks a single question — may another search start yet? That question is safe to ask repeatedly, safe to miss entirely, and always resumes from where the last answer left off. Held as one long-running job, a worker restart or a queue expiry would have ended the night silently, with no way to tell how far it got.

Design decisions

01

There is no sender, and that is the design

Instagram's API does not permit unsolicited DMs, so an automated sender means driving the web UI with a logged-in session — against their terms, the fastest way to get an account restricted, and a real risk taken for a message a human can paste in ten seconds. The tool finds the account, drafts the message from what that business says about itself, and hands it over.

02

An unregistered table has no data path at all

Tenant tables are declared in a tenancy registry and reached only through the scoped handle, which supplies the workspace filter itself. Add a tenant table, forget to register it, and there is simply no way to query it — a compile-time nuisance instead of a runtime leak.

03

Bio links are verified, not assumed

A background job probes every link in a bio and records live, dead, parked, or missing. That one field is most of the triage: a parked domain is a warmer lead than a working site.

04

Split out to protect its neighbour

This began as the Instagram half of OutreachEngine and was separated so neither product's migrations, queues or deploys can break the other. They share no database, and run on deliberately different ports so both can be up at once.

Stack

  • Next.js
  • TypeScript
  • Drizzle ORM
  • PostgreSQL 16
  • pg-boss
  • Playwright
  • shadcn/ui
  • Tailwind

My part in it

Sole architect and engineer.


What is not done

Drafting is where it stops. Every message that has gone out from it was sent by hand.