Outreach — Multi-Tenant LinkedIn Automation SaaS
A Next.js 16 multi-tenant SaaS for LinkedIn outreach — credential-driven Unipile integration, Prisma 7 on Neon, JWT sessions, and a fully UI-driven flow for connect, search, invite, and DM.
Overview
Outreach is a multi-tenant SaaS that lets users run LinkedIn campaigns — connect accounts, search prospects, send connection invites, and start DMs — entirely from the UI. No vendor secrets in env vars: the Unipile DSN, access token, and LinkedIn login all flow in through forms and are stored per-user, so a single deployment can serve any number of operators.
The app is built on Next.js 16 App Router with React 19, Prisma 7 + the new @prisma/adapter-pg driver against Neon Postgres, Tailwind v4, and JWT cookie sessions signed with bcrypt-hashed passwords. Anti-throttling and scheduling are handled with Upstash QStash using idempotency keys and jittered windows.
Why this exists
LinkedIn outreach tooling is a minefield of brittle automation, anti-bot detection, and per-user credential management. This project takes a different posture:
- Per-tenant credentials, not platform credentials — every Unipile + LinkedIn pairing is scoped to a user account.
- UI-driven 2FA / OTP / CAPTCHA resolution — when LinkedIn returns a checkpoint, the dashboard prompts the user for the code and the server submits it back through Unipile.
- Anti-throttling discipline — outreach respects LinkedIn's rate limits via tiered daily caps and a jittered 7–22 UTC window so behavior looks human across timezones.
Architecture
┌─────────────────────┐ ┌──────────────────────┐
│ Next.js 16 (App) │────▶│ Route Handlers │
│ React 19 + TW v4 │ │ app/api/** │
└─────────────────────┘ └─────────┬────────────┘
│
┌───────────────────────┼─────────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ Prisma 7 / pg │ │ Unipile Node │ │ Upstash QStash │
│ Neon Postgres │ │ SDK (per user) │ │ Schedules + retries │
└─────────────────┘ └─────────────────┘ └─────────────────────┘- Auth —
/api/auth/register,/login,/logout,/me. JWT signed withJWT_SECRET, set as an HTTP-only cookie, verified on every protected route. - Settings —
/api/settings(GET/POST) stores the per-user Unipile DSN + access token in Postgres. - LinkedIn lifecycle —
/api/linkedin/connect,/connect/checkpoint,/hosted-auth,/search,/invite,/message.lib/unipile.tslazily resolves the user's Unipile client on each request.
Flow
- Register at
/register→ land on/settings. - Save Unipile settings — paste DSN (e.g.
api8.unipile.com:13859) and access token. - Connect a LinkedIn account at
/dashboardeither via:- Username + password — if LinkedIn returns a 2FA / OTP / CAPTCHA checkpoint, the dashboard prompts for the code and resolves it inline.
- Hosted auth — Unipile-hosted page that bounces back to the dashboard.
- Run outreach at
/outreach— pick a connected account, run classic people search, send connection invites with a custom message, or start a DM. Every action is recorded in theOutreachtable.
Highlights
Driver-adapter Prisma on Neon
Migrated from Prisma's classic engine to Prisma 7 + @prisma/adapter-pg for native Postgres driver support. prisma generate runs on postinstall, so Vercel deploys regenerate the client on every push.
Anti-throttling with QStash
Instead of running outreach inline (and burning serverless time), background work is enqueued to Upstash QStash with:
- Idempotency keys so retries can't double-invite the same prospect.
- Jittered scheduling in a 7–22 UTC window to avoid burst patterns that trip anti-bot heuristics.
- Tiered daily caps per account to stay inside LinkedIn's connect / message limits.
Unipile classic search
LinkedIn's classic people search isn't exposed on the Unipile SDK's resource classes, so search is sent through client.request.send directly to POST /linkedin/search. The route layer normalizes the response shape so the UI doesn't need to know that.
Email + UX
- Transactional email via Resend.
- Toasts via Sonner, dialogs via Radix UI, motion via Framer Motion / Motion.
- Tailwind v4 with
tw-animate-cssandclass-variance-authorityfor a typed component-variant system.
Tech Stack
| Layer | Choices |
|---|---|
| Framework | Next.js 16 App Router, React 19, TypeScript 5 |
| Styling | Tailwind CSS 4, tw-animate-css, class-variance-authority |
| Data | Prisma 7 with @prisma/adapter-pg, Neon Postgres |
| Auth | bcryptjs + JWT cookie sessions |
| Outreach | unipile-node-sdk |
| Background jobs | Upstash QStash |
| Resend | |
| UI primitives | Radix UI, Lucide, Motion, Sonner |
| Deploy | Vercel |
// more projects
Let's work together
Have a project in mind? Reach out and let's build something great.