37 KiB
📄 Product Requirements Document (PRD)
Teklifsat.com – AI-Driven Lead Generation Platform
Version: 1.0 Date: 2026-04-06 Author: Product & Engineering Status: Draft – Awaiting Review
1. Executive Summary & Vision
Vision Statement
"To become Turkey's #1 AI-driven lead generation platform by empowering 1,000 industrial companies within 6 months to scale globally via automated LinkedIn prospecting."
Product Overview
Teklifsat.com is a Multi-Tenant SaaS platform that automates the entire B2B ABM (Account-Based Marketing) pipeline – from LinkedIn company prospecting to AI-powered qualification and personalized multi-contact outreach – enabling Turkish industrial companies to scale into international markets (DE, EN, TR).
Core Value Proposition
- Fully automated pipeline: Search → Enrich → Qualify → Outreach – zero manual research
- AI Qualification: Intelligent lead scoring based on ICP, company profile, and enriched data
- Built-in Translation Engine: Real-time translation of chat messages and dynamic content across EN/DE/TR
- Human-in-the-Loop (HIL): Users retain full control over outreach before sending
Differentiator
Unlike Apollo.io, Lemlist, or Instantly, Teklifsat focuses on end-to-end AI pipeline automation with native multi-language support – purpose-built for companies operating across language barriers.
2. Target Audience & User Personas
Primary Market
Turkish industrial companies (manufacturing, export, B2B services) expanding into German-speaking and English-speaking markets.
User Roles
| Role | Description | Permissions |
|---|---|---|
| Owner | Company admin, creates the tenant | Full access. Manages Team Settings (ICP, Company Profile, Permissions). Can configure who may create campaigns (default: Owner only). |
| SDR / Team Member | Sales representative | Views all campaigns & leads within the tenant. Can create campaigns if Owner grants permission. Sends outreach (HIL). Manages own LinkedIn account connection. |
User Stories
Owner
- As an Owner, I want to define my company's ICP and profile once, so all campaigns use consistent qualification criteria.
- As an Owner, I want to control which team members can create campaigns.
- As an Owner, I want to see all campaign results across my organization.
- As an Owner, I want to upload my company logo so the platform feels branded for my organization.
SDR / Team Member
- As an SDR, I want to review AI-qualified companies (leads) and their associated contacts before sending outreach.
- As an SDR, I want to chat with connected contacts in my preferred language (DE/TR/EN) with automatic translation.
- As an SDR, I want to see the status of connection requests (pending, accepted, no contact) for each contact.
- As a Team Member, I want to upload my profile picture so my colleagues can identify me easily in the dashboard.
3. Functional Requirements
3.1 Campaign Management
| ID | Requirement | Priority |
|---|---|---|
| F-CM-01 | Users can create campaigns with parameters: Industry, City, Country, Company Size (Multiple Selection), Target Lead Count, Name | P0 |
| F-CM-02 | Campaign parameters are immutable after creation (except Name and Target Lead Count) | P0 |
| F-CM-03 | Campaigns can be paused and resumed | P0 |
| F-CM-04 | Campaign has two phases: Pipeline Phase (automated) and Engagement Phase (manual/HIL) | P0 |
| F-CM-05 | Pipeline Phase auto-completes when target reached OR search results exhausted | P0 |
| F-CM-06 | Paused campaigns are excluded from job scheduling | P0 |
| F-CM-07 | Strict Uniqueness: Within a tenant, the combination of industry + country is the unique identifier for a campaign. Users cannot create a second campaign for the same combination; to target more leads, they must use the Top-Up approach (adjusting target_leads on the existing campaign). |
P0 |
| F-CM-08 | Lead Adjustment: Users can adjust target_leads on an existing campaign (increase or decrease). - Increase: The pipeline resumes automatically if it was pipeline_completed and the new target exceeds current_leads. - Decrease: Permitted as long as new_target_leads >= current_leads. If new_target_leads is set equal to current_leads, the campaign transitions to pipeline_completed immediately. |
P0 |
| F-CM-09 | Quota Guard on Top-Up: When increasing target_leads, the system must validate that tenant.current_leads_this_month + (new_target - old_target) does not exceed tenant.monthly_quota. If exceeded, the top-up is rejected with a clear error message. |
P0 |
| F-CM-10 | Quota Guard on Create: When creating a campaign, the system must validate that tenant.current_leads_this_month + target_leads does not exceed tenant.monthly_quota. |
P0 |
Campaign Status Model
Pipeline Phase: pending → running → paused → pipeline_completed
↑ (resumes on lead top-up if pipeline_completed and new target > current_leads)
Engagement Phase: ongoing (after pipeline completes, users engage with leads)
Quota Logic
tenant.monthly_quota = total credits available this month (e.g. 1,000)
tenant.current_leads_this_month = SUM of current_leads across all campaigns of this tenant this month
remaining_quota = monthly_quota - current_leads_this_month
- Quota is based on delivered qualified companies (
current_leads), not ordered lead counts (target_leads) - Monthly quota resets at the start of each calendar month
- The pipeline auto-stops when
tenant.current_leads_this_month >= monthly_quota, even if individual campaign targets are not yet reached - Affected campaigns are paused with status
quota_reacheduntil the next month or quota upgrade
3.2 Lead Pipeline (Background Jobs)
| ID | Requirement | Priority |
|---|---|---|
| F-PL-01 | Company Search: Suche nach Unternehmen über Unipile API (LinkedIn Premium) basierend auf Branche, Standort und Firmengröße. | P0 |
| F-PL-02 | Company Enrichment (Global Cache): Website-Inventur via Firecrawl /map. Ein AI-Agent führt das Scraping durch. Daten werden global in leads gespeichert. Liegt eine Anreicherung vor (< 30 Tage), wird dieser Schritt übersprungen. |
P0 |
| F-PL-03 | Company Qualification (Tenant-Specific): KI bewertet das Unternehmen basierend auf (globalen) Enrichment-Daten, Tenant-ICP und Firmenprofil. Dieser Schritt ist IMMER tenantspezifisch. Billable Event: Sobald qualifiziert, wird 1 Credit abgezogen. | P0 |
| F-PL-04 | People Search (Conditional): Nur für qualifizierte Unternehmen wird eine Suche nach Personen (Contacts) gestartet (Senioritäts-Level: CXO, Director, etc.). Ein Lead (Company) kann mehrere Kontakte haben. | P0 |
| F-PL-05 | Outreach Strategy Generation: Automatische Erstellung der personalisierten Outreach-Strategie/Template für das Unternehmen (mit Platzhaltern für Kontaktnamen). | P0 |
| F-PL-06 | Pipeline runs in batches (5–10 companies per run), loops until target reached. | P0 |
| F-PL-07 | Companies (Leads) are deduplicated globally by their LinkedIn Company URL. General enrichment data is shared across all tenants to save costs. | P0 |
| F-PL-08 | Qualification and Outreach Strategy are strictly campaign-specific (stored on campaign_leads). Enrichment data is globally shared but can be refreshed if TTL expired. |
P0 |
3.3 LinkedIn Engagement (V2)
| ID | Requirement | Priority |
|---|---|---|
| F-LE-01 | Send connection request with name-personalized outreach message via Unipile API (triggered by user click – HIL). | P1 |
| F-LE-02 | Track connection status per Contact via Unipile webhooks: pending → accepted → no_contact. |
P1 |
| F-LE-03 | If no response after 3 weeks: retry connection request once. Still no response → mark contact as no_contact. SDR can then try the next contact in the same company. |
P1 |
| F-LE-04 | After connection accepted: enable chat messaging with the specific contact. | P1 |
| F-LE-05 | Inbox View: Display LinkedIn conversations per contact. | P1 |
| F-LE-06 | Track outreach performance (open rates, response rates) per company and per SDR. | P1 |
| F-LE-07 | Claim Logic: A contact is marked as claimed_by_id when an SDR starts the engagement. |
P1 |
3.4 Translation Layer (V2)
| ID | Requirement | Priority |
|---|---|---|
| F-TL-01 | i18n: Static UI elements in EN, DE, TR | P1 |
| F-TL-02 | Dynamic Content Translation: Company and contact data, enrichment results, outreach messages translated to user's preferred language | P1 |
| F-TL-03 | Outreach Language: Outreach messages generated/translated into contact's language (detected from LinkedIn profile) | P1 |
| F-TL-04 | Chat Translation: Incoming messages translated to user's language. Outgoing messages translated to contact's language before sending. | P1 |
3.5 Team & Tenant Settings
| ID | Requirement | Priority |
|---|---|---|
| F-TS-01 | ICP Definition: Owner defines Ideal Customer Profile per tenant (used for AI qualification) | P0 |
| F-TS-02 | Company Profile: Owner defines own company's products/services (used for AI qualification + outreach) | P0 |
| F-TS-03 | Permissions: Owner configures who can create campaigns (default: Owner only) | P0 |
| F-TS-04 | Team Management: Invite/remove team members (existing feature) | P0 |
| F-TS-05 | Organization Branding: Ability to upload an organization image (Company Logo). | P1 |
3.6 User Settings
| ID | Requirement | Priority |
|---|---|---|
| F-US-01 | User selects preferred language (EN/DE/TR) | P0 |
| F-US-02 | User connects LinkedIn account via Unipile (existing feature) | P0 |
| F-US-03 | Profile Picture: Ability for team members to upload their own profile picture. | P1 |
3.7 Onboarding Automation (V3)
| ID | Requirement | Priority |
|---|---|---|
| F-OB-01 | User provides own website URL → system scrapes and auto-generates company profile via Firecrawl + AI | P2 |
| F-OB-02 | User provides 3 customer URLs → system scrapes and auto-generates ICP via Firecrawl + AI | P2 |
| F-OB-03 | Show progress animation during scraping ("Your profile is being created…") | P2 |
4. Non-Functional Requirements
4.1 Rate Limiting & Resource Management
LinkedIn / Unipile (User-Owned Premium Business Accounts)
All search and engagement actions are performed using the user's own LinkedIn Premium Business account. Platform-owned Sales Navigator accounts are no longer used.
| Action | Premium Business Limit | Handling Strategy |
|---|---|---|
| Search Profiles (Pipeline Phase) | 1,000 total/query | Max 1,000 results per query. Use persistent search_cursor (base64). |
| Daily Search Limit | 1,000 profiles/day | Unipile recommendation. Safe Mode: Default to 200/day. |
| Connection requests (with note) | 80–100/day, ~200/week | User clicks 'Send' (HIL). Max 300 characters. |
| Randomized Delays (Critical) | Required | Spaced calls (30–120s+ delay) rather than fixed intervals to avoid detection. |
| Account Warmup | Required | Start new/inactive accounts with 20% of limits, ramping up over 14 days. |
Important
Account Safety FIRST: Since users connect their primary business accounts, the platform enforces human-like behavior. Avoid fixed-interval polling. Use the
new_relationwebhook for status updates instead of polling the invitation list.
Implementation Requirements
| Requirement | Details |
|---|---|
| Search Account | Each campaign is assigned exactly one search_account_id (the SDR who initiated the search). This account provides the search quota. |
| Engagement Phase (1:N) | While the search is tied to one account, any SDR in the tenant can claim a lead and use their own linkedin_account_id for the outreach/engagement phase. |
| 429 / 500 / 422 Handling | 429/500 (Rate limit) → Pause account search for 24h. 422 (Cannot resend) → Mark lead as invitation_limit_reached, retry next day. |
| Cursor Workflow | The Inngest search loop persists the search_cursor to DB after every batch, enabling reliable pause/resume across sessions. |
Firecrawl
| Resource | Constraint | Handling Strategy |
|---|---|---|
| Concurrency | Max 2 concurrent browsers | Semaphore pattern: acquire/release slot via atomic DB update. Jobs wait + retry if no slot available. |
4.2 Scalability
- Linear Scaling: Total platform capacity increases automatically as more users connect Premium accounts.
- Distributed Load: Background jobs are distributed across user accounts, reducing the risk of a single "platform-wide" ban.
4.3 Multi-Tenancy
- Row Level Security (RLS) on all tenant-scoped tables.
- Lead (Company) Ownership: Companies found within a tenant are shared.
- Contact Ownership: To prevent SDRs from messaging the same person, a contact is marked as
claimed_by_id(SDR User ID) once an outreach or chat is initiated.
4.4 Reliability
- Persistent Cursors: Inngest resume logic uses
campaigns.search_cursorto prevent duplicate profile fetches and credit waste. - Webhook-Driven: Replaces polling for connection acceptance, reducing account activity.
5. Data Model
6. Architecture & Technology Stack
6.1 Unipile Implementation Constraints
- User-Owned Only: Platform Managed Service is removed. All actions are scoped to user-connected accounts.
- Search Logic: Uses the standard
linkedin/companiesorlinkedin/peoplesearch endpoint with thesearch_cursorparameter. - Rate Limit Resilience: Search accounts tracks daily fetches. If a 429/500 occurs, the account is temporarily blocked in our DB (
user.blocked_until). - Distributed Outreach: One campaign can have multiple SDRs engaging leads, each using their own
linkedin_account_id. - Randomized Delays: Minimum 30s-120s delay between profile fetches. Max 100 profiles total per search batch for safety.
- Filter: Headcount (Company Size): Passed as an array of objects
[{"min": x, "max": y}].- Valid Min: 1, 11, 51, 201, 501, 1001, 5001, 10001
- Valid Max: 10, 50, 200, 500, 1000, 5000, 10000
- Filter: Industries & Locations: These parameters must be retrieved as IDs from the Unipile
GET /api/v1/linkedin/search/parametersendpoint before campaign creation. - Search UI Constraints:
- Debounced Input: Minimum 300ms delay/buffer on search inputs to minimize Unipile API calls.
- Campaign Deduplication:
- Logic: Server-side check before creation to prevent multiple campaigns with identical
industries+locations+headcountwithin the same organization. - Action: If a match is found, prompt the user to use the existing campaign or confirm if they truly want a duplicate.
- Logic: Server-side check before creation to prevent multiple campaigns with identical
Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Next.js (App Router) | UI, Server Actions, API Routes |
| Database | Supabase (PostgreSQL) | Data persistence, RLS, Realtime subscriptions |
| Background Jobs | Inngest | Step-based workflows, retries, scheduling, rate limiting |
| LinkedIn API | Unipile | Search, connection requests, messaging, webhooks |
| Web Scraping | Firecrawl | Company website enrichment |
| AI | LLM API (TBD) | Lead qualification, outreach generation, translation |
| Auth | Supabase Auth | User authentication (existing) |
Architecture Diagram
┌─────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ Campaign UI │ Lead Table │ Inbox │ Settings │
└───────┬────────────────┬─────────────────┬──────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────┐ ┌──────────────────┐
│ API Routes │ │ Supabase │ │ Supabase │
│ /api/\* │ │ Realtime │ │ Webhooks (Unipile│
└───────┬───────┘ └───────────┘ │ → connection │
│ │ status updates)│
▼ └──────────────────┘
┌───────────────────────────────────────┐
│ Inngest (Job Orchestration) │
│ │
│ campaign/start → campaign/run (loop) │
│ │
│ Steps: │
│ 1. Search (Unipile) │
│ 2. Enrich (Firecrawl) ──► Semaphore │
│ 3. Qualify (AI) │
│ 4. Generate Outreach (AI) │
│ 5. Update DB + Counters │
│ 6. Sleep (rate limit) │
│ 7. Re-trigger if target not met │
└───────────────┬───────────────────────┘
│
▼
┌───────────────────────────┐
│ Supabase (PostgreSQL) │
│ + Row Level Security │
│ + Resource Limits │
└───────────────────────────┘
```
### Key Patterns
- **Event Recursion:** `campaign/run` processes one batch, then re-triggers itself (instead of `while(true)`)
- **Semaphore:** Firecrawl slot acquisition via atomic DB update
- **Circuit Breaker:** LinkedIn 429 → set `blocked_until` → all jobs auto-pause
- **Scheduler:** Central job that checks resources before dispatching campaign runs
---
## 7. Frontend & UX Design System
### 7.1 Design Philosophy & Aesthetics
To wow the user, the platform follows a **Modern Premium Dark UI** with the following characteristics:
- **Visual Style:** Glassmorphism (semi-transparent cards), subtle gradients (HSL-based), and consistent border radii (standardized via Tailwind variables).
- **Typography:** Modern sans-serif (e.g., *Outfit* or *Inter*) with clear hierarchy.
- **Interactivity:** Micro-animations for feedback (e.g., pulsing states during AI qualification, smooth transitions between list and detail views).
- **Responsive:** Mobile-first layout, ensuring SDRs can monitor campaigns and respond to chats on the go.
### 7.2 Core Views & UX Patterns
#### A. Central Dashboard (Overview)
- **At-a-Glance Stats:** Active Campaigns, Total Companies Qualified, This Month's Quota Usage.
- **Activity Stream:** Recent activity (e.g., "SDR A connected with Director B at Company C").
#### B. Campaign Detail View (ABM Dashboard)
This is the core workspace where the 1:N relationship (Company:Contacts) is mastered:
- **Level 1: The Account (Company):**
- Represented as a Card or Table Row.
- Includes Company Name, Domain, Industry, and an **AI-Qualification Badge** (High/Med/Low Match).
- **AI Reasoning Card:** A dedicated UI section (side-drawer or tooltip) that answers "Why does this match?" based on the `qualification_data`.
- **Enrichment Quick-View:** A summary of the Firecrawl results (Company USP, recent news, etc.).
- **Level 2: The Contacts (People):**
- Expandable list inside the Company Card.
- Profile pictures, job titles, and **one-click outreach button**.
- **Status Indicators:** Color-coded connection states (e.g., Green for Accepted, Yellow for Pending, Red for No Contact).
#### C. Outreach & Engagement Cockpit
- **Strategy Preview:** View the company-specific outreach strategy generated by the AI.
- **Message Personalization Viewer:** Real-time preview of how placeholders ({{firstName}}) are filled before sending.
- **Human-in-the-Loop (HIL) Action Bar:** Prominent "Send Connection Request" button.
#### D. Real-Time Pipeline Tracker
- **Pulse:** Live progress bar during the Search-Batch (5-10 companies). Shows: "Searching companies..." → "Enriching via Firecrawl..." → "AI Qualifying..." → "Finding People...".
- **Visual Feedback:** No static lists during background jobs. Users see the pipeline "working" in real-time via Supabase Realtime subscriptions.
### 7.3 Design Tokens (Mapping to `globals.css`)
- **Primary:** Premium Teal/Blue (instead of default Indigo).
- **Accent:** Amber for "Qualified" but not yet contacted.
- **Neutral:** Slate/Gray for non-interactive backgrounds.
- **Success:** Emerald for accepted connections.
---
## 8. Monetization
| Model | Details |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Pay-per-Lead** | Users are billed per **qualified company (lead)**. |
| **Billable Trigger** | Lead status = `qualified`. Triggered once per company per tenant across campaigns. |
| **Non-billable** | Disqualified companies, companies still in enrichment/qualification phase, search for contacts. |
| **Quota Model** | Each tenant has a `monthly_quota` (credits). Quota tracks `current_leads` (qualified companies), not `target_leads`. |
| **Quota Enforcement** | Pipeline auto-stops when `current_leads_this_month >= monthly_quota`. |
| **Quota Upgrade** | Owner can upgrade their plan to increase `monthly_quota`. |
### Tenant Quota DB Fields
```
tenants:
monthly_quota INT -- e.g. 1000 (set by plan)
current_leads_month INT -- delivered qualified leads this month (resets monthly)
quota_reset_date DATE -- date of last monthly reset
Note
Free tier / trial details TBD. Consider offering first N leads free for onboarding conversion.
Important
The uniqueness constraint
(industry + country)on active campaigns is enforced at the application layer (before DB insert) AND as a partial unique index in PostgreSQL:CREATE UNIQUE INDEX campaigns_industry_country_unique ON campaigns (tenant_id, industry, country);
9. Roadmap & Milestones
Phase 1 – MVP (Core Pipeline & ABM Dashboard)
Goal: End-to-end pipeline working. Users can create campaigns and review/engage companies and their contacts in a high-end UI.
Backend & Infrastructure:
- Database schema (campaigns, leads, lead_contacts) + ABM RLS
- Inngest setup + campaign/run workflow (the Step-Loop)
- Search step (Unipile Company Search)
- Enrichment step (Firecrawl + AI-Agent + Semaphore)
- Qualification & Outreach Strategy generation (AI)
- Contact Search step (Unipile People Search for qualified companies)
Frontend & UI (Next.js):
- Layout System (Modern Dark Navigation, Premium Design Tokens)
- Campaign Overview UI (List/Add Campaigns)
- The "ABM Board": Company Card list with expandable Contacts
- AI-Match Drawer: Showing the "Why?" (AI Reasoning) and Enrichment details
- Real-time Progress Tracking (Supabase Realtime integration)
- SDR Claim & Connection Trigger (HIL Mock/V1 Trigger)
- Organization Logo upload & display
- User Profile Picture upload & display
Phase 2 – LinkedIn Engagement & Translation
Goal: Users can send outreach, track connections, and chat with contacts – across languages.
- Connection request sending via Unipile (HIL – user clicks send)
- Webhook handler for connection status updates (per Contact)
- Connection retry logic (3-week timeout → retry once)
- Inbox / Chat view (Contact-centric)
- Chat translation engine (incoming/outgoing)
- i18n (Static UI: EN, DE, TR)
- Dynamic content translation (company data, enrichment results)
Phase 3 – Scaling & Onboarding Automation
Goal: Reduce onboarding friction, scale infrastructure.
- Auto-onboarding: Website → Company Profile (Firecrawl + AI)
- Auto-onboarding: 3 Customer URLs → ICP generation
- Multi-account LinkedIn pool (round-robin, per-account limits)
- Analytics dashboard (conversion rates, outreach performance)
- Per-tenant quotas and fair scheduling
- Billing integration (pay-per-lead tracking)
10. Risks & Mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| LinkedIn blocks / 429 per account | High | Critical | Circuit breaker per account, blocked_until in DB, random call spacing, multi-account pool (V3) |
LinkedIn 422 cannot_resend_yet (invitation cap) |
High | High | Dedicated error handler, mark lead as invitation_limit_reached, retry next day |
| Outreach message >300 chars truncated | Medium | Medium | Enforce 300-char limit in AI prompt and UI |
| LinkedIn fake-account detection | Low (if using real accounts) | Critical | Policy: only real accounts allowed, gradual ramp-up for new accounts |
| Firecrawl latency (up to 5 min/lead) | Medium | Medium | Parallelization within semaphore limit (max 2), cache already-scraped domains |
| AI qualification accuracy | Medium | High | Manual override in UI, iterative ICP refinement, log qualification reasoning |
| Multi-tenant data leakage | Low | Critical | RLS on all tables, automated tests for tenant isolation |
| Unipile webhook delivery failure | Medium | Medium | Idempotent webhook handler, periodic light polling as fallback (randomized intervals) |
11. Open Questions
| # | Question | Impact |
|---|---|---|
| 1 | Which LLM provider for qualification + outreach? (OpenAI, Anthropic, local?) | Cost, latency, quality |
| 2 | Free tier / trial offering details? | Onboarding conversion |
| 3 | GDPR/DSGVO compliance scope for V1? (Lead opt-out, data deletion) | Legal, EU market |
| 4 | Resolved: All search and engagement actions use the user's connected LinkedIn Premium account (search_account_id). Platform accounts are not used. |
— |
| 5 | Outreach message tone/style configurable per tenant? (See Section 12) | Feature scope |
12. Future Roadmap & Feature Requests (ICE Box)
The following items were identified during architecture review and are deferred for post-MVP implementation:
| Feature | Description | Reason for Delay |
|---|---|---|
| Phase 0: Pre-Qualification | Lightweight AI check (LinkedIn-only data) before expensive Firecrawl/Deep-Scraping enrichment. | Optimization for unit economics. |
| Tone of Voice (DACH) | Configurable "Formal (Sie)" vs "Informal (Du)" setting for outreach generation. | Critical for conversion in German B2B context. |
| Advanced Filtering | Add job_title to campaign filters. (Note: Seniority levels implemented in V1). |
Requires more complex search query mapping in Unipile. |